From 4f21aac58be24f32a5b32067cb6213caf2ab1e8c Mon Sep 17 00:00:00 2001 From: Miki Date: Fri, 30 Sep 2022 11:26:23 -0700 Subject: [PATCH] [Plugin Helpers] Facilitate version changes (#2398) Signed-off-by: Miki Signed-off-by: Miki (cherry picked from commit 48fe60b40661c0bb19ed73d3426f5d5ec418887c) --- packages/osd-plugin-helpers/README.md | 93 ++++++++--- packages/osd-plugin-helpers/src/cli.ts | 144 +++++++++++++++++- .../src/{build_context.ts => contexts.ts} | 22 +++ .../osd-plugin-helpers/src/tasks/clean.ts | 2 +- .../src/tasks/create_archive.ts | 2 +- .../osd-plugin-helpers/src/tasks/index.ts | 1 + .../osd-plugin-helpers/src/tasks/optimize.ts | 2 +- .../src/tasks/update_versions.ts | 100 ++++++++++++ .../src/tasks/write_public_assets.ts | 2 +- .../src/tasks/write_server_files.ts | 2 +- .../src/tasks/yarn_install.ts | 2 +- 11 files changed, 337 insertions(+), 35 deletions(-) rename packages/osd-plugin-helpers/src/{build_context.ts => contexts.ts} (77%) create mode 100644 packages/osd-plugin-helpers/src/tasks/update_versions.ts diff --git a/packages/osd-plugin-helpers/README.md b/packages/osd-plugin-helpers/README.md index 3b1dd0c874cc..4a16bc9afe23 100644 --- a/packages/osd-plugin-helpers/README.md +++ b/packages/osd-plugin-helpers/README.md @@ -25,10 +25,13 @@ yarn osd bootstrap ## Usage -This simple CLI has a build task that plugin devs can run from to easily package OpenSearch Dashboards plugins. +This CLI has a `build` command that plugin devs can run to easily package OpenSearch Dashboards plugins. It also has a `version` +command which updates a plugin's `version` and `opensearchDashboardsVersion` in the `opensearch_dashboards.json` and the `version`, +`opensearchDashboards.version`, and `opensearchDashboards.templateVersion` in the `package.json` files to match the version of +OpenSearch Dashboards or ones supplied. -Previously you could also use that tool to start and test your plugin. Currently you can run -your plugin along with OpenSearch Dashboards running `yarn start` in the OpenSearch Dashboards repository root folder. Finally to test +Previously you could also use that tool to start and test your plugin. Currently, you can run +your plugin along with OpenSearch Dashboards running `yarn start` in the OpenSearch Dashboards repository root folder. Finally, to test your plugin you should now configure and use your own tools. ```sh @@ -37,37 +40,76 @@ $ plugin-helpers help Usage: plugin-helpers [command] [options] Commands: - build - Copies files from the source into a zip archive that can be distributed for - installation into production OpenSearch Dashboards installs. The archive includes the non- - development npm dependencies and builds itself using raw files in the source - directory so make sure they are clean/up to date. The resulting archive can - be found at: - - build/{plugin.id}-{opensearchDashboardsVersion}.zip - - Options: - --skip-archive Don't create the zip file, just create the build/opensearch-dashboards directory - --opensearch-dashboards-version, -v OpenSearch Dashboards version that the + build + Copies files from the source into a zip archive that can be distributed for installation into production + OpenSearch Dashboards installs. The archive includes the non-development npm dependencies and builds itself using + raw files in the source directory so make sure they are clean/up to date. The resulting archive can be found at: + + build/{plugin.id}-{opensearchDashboardsVersion}.zip + + Options: + --skip-archive Don't create the zip file, just create the build/opensearch-dashboards directory + --opensearch-dashboards-version, -v OpenSearch Dashboards version that the built plugin will target + + version + Without any options, it would display information about the versions found in the manifest file. With options, it + updates the version and opensearchDashboardsVersion in the opensearch_dashboards.json and the version, + opensearchDashboards.version, and opensearchDashboards.templateVersion in the package.json files to the values + provided or syncs them with the version of OpenSearch Dashboards. The versions are expected to start with #.#.# + where # are numbers. + + Options: + --sync Update the versions to match OpenSearch Dashboards' + --plugin-version Update the plugin's version to the one specified + --compatibility-version Update the plugin's compatibility version to the one specified + Global options: - --verbose, -v Log verbosely - --debug Log debug messages (less than verbose) - --quiet Only log errors - --silent Don't log anything - --help Show this message + --verbose, -v Log verbosely + --debug Log debug messages (less than verbose) + --quiet Only log errors + --silent Don't log anything + --help Show this message + +``` + +### Examples + +To produce build artifacts of a plugin in the `build/opensearch-dashboards` directory, without generating a zip archive, and while targeting OpenSearch Dashboards 3.0.0: +``` +yarn plugin-helpers build --skip-archive --opensearch-dashboards-version="3.0.0" +``` + +To synchronize the versions used in a plugin's `opensearch_dashboards.json` and `package.json` files with the version of OpenSearch Dashboards: +``` +yarn plugin-helpers version --sync +``` +If legacy plugin versions are required: +``` +yarn plugin-helpers version --sync legacy +``` + +To update the compatibility version of the plugin in the `opensearch_dashboards.json` and `package.json` files: +``` +yarn plugin-helpers version --compatibility-version="3.0.0" +// or +yarn plugin-helpers version --compatibility-version 3.0.0 +``` +To synchronize the compatibility version of the plugin with the version of OpenSearch Dashboards but set a specific version for the plugin: +``` +yarn plugin-helpers version --sync --plugin-version 1.1.0 ``` ## Versions -The plugins helpers in the OpenSearch Dashboards repo are available for OpenSearch Dashboards 1.0 and greater. Just checkout the branch of OpenSearch Dashboards you want to build against and the plugin helpers should be up to date for that version of OpenSearch Dashboards. +The plugins helpers in the OpenSearch Dashboards repo are available for OpenSearch Dashboards 1.0 and greater. Just checkout the branch of OpenSearch Dashboards you want to build against and the plugin helpers should be up-to-date for that version of OpenSearch Dashboards. ## Configuration -`plugin-helpers` accepts a number of settings, which can be specified at runtime, or included in a `.opensearch_dashboards-plugin-helpers.json` file if you'd like to bundle those settings with your project. +`plugin-helpers` accepts a number of settings for the `build` command which can be specified at runtime or included in a `.opensearch_dashboards-plugin-helpers.json` file if you'd like to bundle those settings with your project. It will also observe a `.opensearch_dashboards-plugin-helpers.dev.json`, much like OpenSearch Dashboards does, which we encourage you to add to your `.gitignore` file and use for local settings that you don't intend to share. These "dev" settings will override any settings in the normal json config. @@ -84,6 +126,13 @@ Setting | Description `skipInstallDependencies` | Don't install dependencies defined in package.json into build output `opensearchDashboardsVersion` | OpenSearch Dashboards version for the build output (added to package.json) +### Settings for `version` + +Setting | Description +------- | ----------- +`sync` | As the default behavior, it uses the version of OpenSearch Dashboards to update the plugin's `opensearch_dashboards.json` and `package.json` files. +`set` | Defines the version to be used in the plugin's `opensearch_dashboards.json` and `package.json` files. + ## TypeScript support Plugin code can be written in [TypeScript](http://www.typescriptlang.org/) if desired. To enable TypeScript support create a `tsconfig.json` file at the root of your plugin that looks something like this: diff --git a/packages/osd-plugin-helpers/src/cli.ts b/packages/osd-plugin-helpers/src/cli.ts index 158e2578bde9..69a656db668b 100644 --- a/packages/osd-plugin-helpers/src/cli.ts +++ b/packages/osd-plugin-helpers/src/cli.ts @@ -37,10 +37,14 @@ import { RunWithCommands, createFlagError, createFailError } from '@osd/dev-util import { findOpenSearchDashboardsJson } from './find_opensearch_dashboards_json'; import { loadOpenSearchDashboardsPlatformPlugin } from './load_opensearch_dashboards_platform_plugin'; import * as Tasks from './tasks'; -import { BuildContext } from './build_context'; +import { BuildContext, VersionContext } from './contexts'; import { resolveOpenSearchDashboardsVersion } from './resolve_opensearch_dashboards_version'; import { loadConfig } from './config'; +const VERSION_PARAM_MATCH_DASHBOARDS = 'sync'; +const VERSION_PARAM_USE_INPUT_FOR_PLUGIN = 'plugin-version'; +const VERSION_PARAM_USE_INPUT_FOR_COMPATIBILITY = 'compatibility-version'; + export function runCli() { new RunWithCommands({ description: 'Some helper tasks for plugin-authors', @@ -48,11 +52,9 @@ export function runCli() { .command({ name: 'build', description: ` - Copies files from the source into a zip archive that can be distributed for - installation into production OpenSearch Dashboards installs. The archive includes the non- - development npm dependencies and builds itself using raw files in the source - directory so make sure they are clean/up to date. The resulting archive can - be found at: + Copies files from the source into a zip archive that can be distributed for installation into production + OpenSearch Dashboards installs. The archive includes the non-development npm dependencies and builds itself using + raw files in the source directory so make sure they are clean/up to date. The resulting archive can be found at: build/{plugin.id}-{opensearchDashboardsVersion}.zip @@ -64,7 +66,7 @@ export function runCli() { k: 'opensearch-dashboards-version', }, help: ` - --skip-archive Don't create the zip file, just create the build/opensearch-dashboards directory + --skip-archive Don't create the zip file, just create the build/opensearch-dashboards directory --opensearch-dashboards-version, -v OpenSearch version that the `, }, @@ -120,5 +122,133 @@ export function runCli() { } }, }) + .command({ + name: 'version', + description: ` + Without any options, it would display information about the versions found in the manifest file. With options, it + updates the version and opensearchDashboardsVersion in the opensearch_dashboards.json and the version, + opensearchDashboards.version, and opensearchDashboards.templateVersion in the package.json files to the values + provided or syncs them with the version of OpenSearch Dashboards. The versions are expected to start with #.#.# + where # are numbers. + `, + flags: { + string: [VERSION_PARAM_USE_INPUT_FOR_PLUGIN, VERSION_PARAM_USE_INPUT_FOR_COMPATIBILITY], + help: ` + --${VERSION_PARAM_MATCH_DASHBOARDS.padEnd( + 35, + ' ' + )}Update the versions to match OpenSearch Dashboards' + --${VERSION_PARAM_USE_INPUT_FOR_PLUGIN.padEnd( + 35, + ' ' + )}Update the plugin's version to the one specified + --${VERSION_PARAM_USE_INPUT_FOR_COMPATIBILITY.padEnd( + 35, + ' ' + )}Update the plugin's compatibility version to the one specified + `, + allowUnexpected: true, + }, + async run({ log, flags }) { + const pluginDir = await findOpenSearchDashboardsJson(process.cwd()); + if (!pluginDir) { + throw createFailError( + `Unable to find OpenSearch Dashboards Platform plugin in [${process.cwd()}] or any of its parent directories. Has it been migrated properly? Does it have a opensearch_dashboards.json file?` + ); + } + + let dashboardsPackage; + try { + dashboardsPackage = await import(Path.join(process.cwd(), '../../package.json')); + } catch (ex) { + throw createFailError(`Unable to parse the OpenSearch Dashboards' package.json file`); + } + + let pluginPackage; + try { + pluginPackage = await import(Path.join(process.cwd(), 'package.json')); + } catch (ex) { + throw createFailError(`Unable to parse the plugin's package.json file`); + } + + let manifestFile; + try { + manifestFile = await import(Path.join(process.cwd(), 'opensearch_dashboards.json')); + } catch (ex) { + throw createFailError(`Unable to parse the plugin's opensearch_dashboards.json file`); + } + + const dashboardsVersion = dashboardsPackage.version; + const pluginVersion = pluginPackage.version; + const manifestCompatibilityVersion = manifestFile.opensearchDashboardsVersion; + + log.info( + `The plugin is on v${pluginVersion} and requires OpenSearch Dashboards v${manifestCompatibilityVersion}, ${ + manifestCompatibilityVersion === dashboardsVersion ? 'and' : 'but' + } the one found in the directory hierarchy is on v${dashboardsVersion}.` + ); + + let askedToChange = false; + let updatedPluginVersion = pluginVersion; + let updatedCompatibilityVersion = manifestCompatibilityVersion; + + const doSync = flags[VERSION_PARAM_MATCH_DASHBOARDS]; + if (doSync) { + if (!['boolean', 'string'].includes(typeof doSync)) + throw createFlagError(`expected a single --${VERSION_PARAM_MATCH_DASHBOARDS} flag`); + + /* if using legacy versions, the plugin's version has a now-redundant `.0` appended to the semantic + * version number of OpenSearch Dashboards. If OpenSearch Dashboards has a version with a suffix, the + * suffix has to be removed before we append the `.0`. + */ + updatedPluginVersion = + doSync === 'legacy' + ? `${dashboardsVersion.replace(/^(\d+\.\d+\.\d+)(-.*)?$/, '$1')}.0` + : dashboardsVersion; + updatedCompatibilityVersion = `${dashboardsVersion}`; + + askedToChange = true; + } + + const pluginVersionValue = flags[VERSION_PARAM_USE_INPUT_FOR_PLUGIN]; + if (pluginVersionValue) { + if (typeof pluginVersionValue !== 'string') + throw createFlagError(`expected a single --${VERSION_PARAM_USE_INPUT_FOR_PLUGIN} flag`); + if (!/^\d+(\.\d+){2,}(-\S+)?$/.test(pluginVersionValue)) + throw createFlagError( + `expected a valid version starting with #.#.# where # are numbers to follow the --${VERSION_PARAM_USE_INPUT_FOR_PLUGIN} flag` + ); + + updatedPluginVersion = pluginVersionValue; + askedToChange = true; + } + + const compatibilityVersionValue = flags[VERSION_PARAM_USE_INPUT_FOR_COMPATIBILITY]; + if (compatibilityVersionValue) { + if (typeof compatibilityVersionValue !== 'string') + throw createFlagError( + `expected a single --${VERSION_PARAM_USE_INPUT_FOR_COMPATIBILITY} flag` + ); + if (!/^\d+(\.\d+){2,}(-\S+)?$/.test(compatibilityVersionValue)) + throw createFlagError( + `expected a valid version starting with #.#.# where # are numbers to follow the --${VERSION_PARAM_USE_INPUT_FOR_COMPATIBILITY} flag` + ); + + updatedCompatibilityVersion = compatibilityVersionValue; + askedToChange = true; + } + + if (!askedToChange) return; + + const context: VersionContext = { + log, + sourceDir: process.cwd(), + pluginVersion: updatedPluginVersion, + compatibilityVersion: updatedCompatibilityVersion, + }; + + await Tasks.updateVersions(context); + }, + }) .execute(); } diff --git a/packages/osd-plugin-helpers/src/build_context.ts b/packages/osd-plugin-helpers/src/contexts.ts similarity index 77% rename from packages/osd-plugin-helpers/src/build_context.ts rename to packages/osd-plugin-helpers/src/contexts.ts index 000a25f0d283..366a4cc20a8e 100644 --- a/packages/osd-plugin-helpers/src/build_context.ts +++ b/packages/osd-plugin-helpers/src/contexts.ts @@ -43,3 +43,25 @@ export interface BuildContext { buildDir: string; opensearchDashboardsVersion: string; } + +export interface VersionContext { + log: ToolingLog; + sourceDir: string; + pluginVersion?: string; + compatibilityVersion?: string; +} + +interface NestedObject { + [key: string]: NestedObject | string | undefined; +} + +export interface FileUpdateContext { + log: ToolingLog; + file: string; + updates: NestedObject; +} + +export interface ObjectUpdateContext { + original: { [key: string]: any }; + updates: NestedObject; +} diff --git a/packages/osd-plugin-helpers/src/tasks/clean.ts b/packages/osd-plugin-helpers/src/tasks/clean.ts index ce25af8ed9fd..6a90fd01a253 100644 --- a/packages/osd-plugin-helpers/src/tasks/clean.ts +++ b/packages/osd-plugin-helpers/src/tasks/clean.ts @@ -35,7 +35,7 @@ import { promisify } from 'util'; import del from 'del'; -import { BuildContext } from '../build_context'; +import { BuildContext } from '../contexts'; const asyncMkdir = promisify(Fs.mkdir); diff --git a/packages/osd-plugin-helpers/src/tasks/create_archive.ts b/packages/osd-plugin-helpers/src/tasks/create_archive.ts index ecf4379cbabf..dc44cfb3f38d 100644 --- a/packages/osd-plugin-helpers/src/tasks/create_archive.ts +++ b/packages/osd-plugin-helpers/src/tasks/create_archive.ts @@ -38,7 +38,7 @@ import del from 'del'; import vfs from 'vinyl-fs'; import zip from 'gulp-zip'; -import { BuildContext } from '../build_context'; +import { BuildContext } from '../contexts'; const asyncPipeline = promisify(pipeline); diff --git a/packages/osd-plugin-helpers/src/tasks/index.ts b/packages/osd-plugin-helpers/src/tasks/index.ts index 42bbef17970f..897a5cad1fa1 100644 --- a/packages/osd-plugin-helpers/src/tasks/index.ts +++ b/packages/osd-plugin-helpers/src/tasks/index.ts @@ -33,6 +33,7 @@ export * from './clean'; export * from './create_archive'; export * from './optimize'; +export * from './update_versions'; export * from './write_public_assets'; export * from './write_server_files'; export * from './yarn_install'; diff --git a/packages/osd-plugin-helpers/src/tasks/optimize.ts b/packages/osd-plugin-helpers/src/tasks/optimize.ts index d8e43483691b..69eb91d5b107 100644 --- a/packages/osd-plugin-helpers/src/tasks/optimize.ts +++ b/packages/osd-plugin-helpers/src/tasks/optimize.ts @@ -37,7 +37,7 @@ import { promisify } from 'util'; import { REPO_ROOT } from '@osd/utils'; import { OptimizerConfig, runOptimizer, logOptimizerState } from '@osd/optimizer'; -import { BuildContext } from '../build_context'; +import { BuildContext } from '../contexts'; const asyncRename = promisify(Fs.rename); diff --git a/packages/osd-plugin-helpers/src/tasks/update_versions.ts b/packages/osd-plugin-helpers/src/tasks/update_versions.ts new file mode 100644 index 000000000000..0acfa12c9091 --- /dev/null +++ b/packages/osd-plugin-helpers/src/tasks/update_versions.ts @@ -0,0 +1,100 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// @ts-ignore +import { readFile, writeFile } from 'fs/promises'; +import path from 'path'; +import { createFailError } from '@osd/dev-utils'; +import { FileUpdateContext, ObjectUpdateContext, VersionContext } from '../contexts'; + +export async function updateVersions({ + log, + sourceDir, + pluginVersion, + compatibilityVersion, +}: VersionContext): Promise { + // If any of the versions are falsy, we will skip updating them in updateObject + if (pluginVersion && !/^\d+(\.\d+){2,}/.test(pluginVersion)) + throw createFailError('The plugin version should start with #.#.# where # are numbers'); + + if (compatibilityVersion && !/^\d+(\.\d+){2,}/.test(compatibilityVersion)) + throw createFailError( + `The plugin's compatibility version should start with #.#.# where # are numbers` + ); + + const updateManifestFile = updateFile({ + log, + file: path.join(sourceDir, 'opensearch_dashboards.json'), + updates: { + version: pluginVersion, + opensearchDashboardsVersion: compatibilityVersion, + }, + }); + + const updatePackageJson = updateFile({ + log, + file: path.join(sourceDir, 'package.json'), + updates: { + version: pluginVersion, + opensearchDashboards: { + version: compatibilityVersion, + templateVersion: compatibilityVersion, + }, + }, + }); + + await Promise.all([updateManifestFile, updatePackageJson]); + + return true; +} + +async function updateFile({ log, file, updates }: FileUpdateContext) { + log.info('Updating', file); + + let json; + + try { + json = JSON.parse(await readFile(file, 'utf8')); + } catch (ex) { + log.error(ex); + throw createFailError(`Failed to parse ${file}`); + } + + const context: ObjectUpdateContext = { + original: json, + updates, + }; + updateObject(context); + + try { + await writeFile(file, JSON.stringify(json, null, 2), 'utf8'); + } catch (ex) { + log.error(ex); + throw createFailError(`Failed to update ${file}`); + } + + log.success(`Updated`, file); +} + +// Copies values in `updates` onto `obj` only if the keys exist +function updateObject({ original, updates }: ObjectUpdateContext) { + for (const key in updates) { + if (!updates[key]) continue; + + // If `key` is not found in `original`, just skip it + if (key in original) { + // If both are objects, merge them + if (updates[key] === 'object' && typeof original[key] === 'object') { + updateObject({ + original: original[key], + updates: updates[key], + } as ObjectUpdateContext); + // If the updated value is falsy, skip it + } else if (updates[key]) { + original[key] = updates[key]; + } + } + } +} diff --git a/packages/osd-plugin-helpers/src/tasks/write_public_assets.ts b/packages/osd-plugin-helpers/src/tasks/write_public_assets.ts index 6dce2452f42f..229be4a56faf 100644 --- a/packages/osd-plugin-helpers/src/tasks/write_public_assets.ts +++ b/packages/osd-plugin-helpers/src/tasks/write_public_assets.ts @@ -35,7 +35,7 @@ import { promisify } from 'util'; import vfs from 'vinyl-fs'; -import { BuildContext } from '../build_context'; +import { BuildContext } from '../contexts'; const asyncPipeline = promisify(pipeline); diff --git a/packages/osd-plugin-helpers/src/tasks/write_server_files.ts b/packages/osd-plugin-helpers/src/tasks/write_server_files.ts index 56b0001fe96b..39f4f90d8c3f 100644 --- a/packages/osd-plugin-helpers/src/tasks/write_server_files.ts +++ b/packages/osd-plugin-helpers/src/tasks/write_server_files.ts @@ -36,7 +36,7 @@ import { promisify } from 'util'; import vfs from 'vinyl-fs'; import { transformFileWithBabel, transformFileStream } from '@osd/dev-utils'; -import { BuildContext } from '../build_context'; +import { BuildContext } from '../contexts'; const asyncPipeline = promisify(pipeline); diff --git a/packages/osd-plugin-helpers/src/tasks/yarn_install.ts b/packages/osd-plugin-helpers/src/tasks/yarn_install.ts index f3ec5cee801f..c6d7f655042f 100644 --- a/packages/osd-plugin-helpers/src/tasks/yarn_install.ts +++ b/packages/osd-plugin-helpers/src/tasks/yarn_install.ts @@ -35,7 +35,7 @@ import Path from 'path'; import execa from 'execa'; -import { BuildContext } from '../build_context'; +import { BuildContext } from '../contexts'; const winVersion = (path: string) => (process.platform === 'win32' ? `${path}.cmd` : path);