diff --git a/docs/developer/plugin/external-plugin-functional-tests.asciidoc b/docs/developer/plugin/external-plugin-functional-tests.asciidoc index 706bf6af8ed9b..7e5b5b79d06e9 100644 --- a/docs/developer/plugin/external-plugin-functional-tests.asciidoc +++ b/docs/developer/plugin/external-plugin-functional-tests.asciidoc @@ -13,7 +13,7 @@ To get started copy and paste this example to `test/functional/config.js`: ["source","js"] ----------- import { resolve } from 'path'; -import { resolveKibanaPath } from '@kbn/plugin-helpers'; +import { REPO_ROOT } from '@kbn/dev-utils'; import { MyServiceProvider } from './services/my_service'; import { MyAppPageProvider } from './services/my_app_page'; @@ -24,7 +24,7 @@ export default async function ({ readConfigFile }) { // read the {kib} config file so that we can utilize some of // its services and PageObjects - const kibanaConfig = await readConfigFile(resolveKibanaPath('test/functional/config.js')); + const kibanaConfig = await readConfigFile(resolve(REPO_ROOT, 'test/functional/config.js')); return { // list paths to the files that contain your plugins tests diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index 768a67794517f..4f6f995f38f31 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -1,32 +1,38 @@ { "name": "@kbn/dev-utils", - "main": "./target/index.js", "version": "1.0.0", - "license": "Apache-2.0", "private": true, + "license": "Apache-2.0", + "main": "./target/index.js", "scripts": { "build": "tsc", "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" }, "dependencies": { + "@babel/core": "^7.11.1", "axios": "^0.19.0", "chalk": "^4.1.0", + "cheerio": "0.22.0", "dedent": "^0.7.0", "execa": "^4.0.2", "exit-hook": "^2.2.0", "getopts": "^2.2.5", + "globby": "^8.0.1", "load-json-file": "^6.2.0", - "normalize-path": "^3.0.0", + "markdown-it": "^10.0.0", "moment": "^2.24.0", + "normalize-path": "^3.0.0", "rxjs": "^6.5.5", "strip-ansi": "^6.0.0", "tree-kill": "^1.2.2", - "tslib": "^2.0.0" + "vinyl": "^2.2.0" }, "devDependencies": { - "typescript": "4.0.2", + "@kbn/babel-preset": "1.0.0", "@kbn/expect": "1.0.0", - "chance": "1.0.18" + "@types/vinyl": "^2.0.4", + "chance": "1.0.18", + "typescript": "4.0.2" } } diff --git a/packages/kbn-dev-utils/src/babel.ts b/packages/kbn-dev-utils/src/babel.ts new file mode 100644 index 0000000000000..e48fe81d0232c --- /dev/null +++ b/packages/kbn-dev-utils/src/babel.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import File from 'vinyl'; +import * as Babel from '@babel/core'; + +const transformedFiles = new WeakSet(); + +/** + * Returns a promise that resolves when the file has been + * mutated so the contents of the file are tranformed with + * babel, include inline sourcemaps, and the filename has + * been updated to use .js. + * + * If the file was previously transformed with this function + * the promise will just resolve immediately. + */ +export async function transformFileWithBabel(file: File) { + if (!(file.contents instanceof Buffer)) { + throw new Error('file must be buffered'); + } + + if (transformedFiles.has(file)) { + return; + } + + const source = file.contents.toString('utf8'); + const result = await Babel.transformAsync(source, { + babelrc: false, + configFile: false, + sourceMaps: 'inline', + filename: file.path, + presets: [require.resolve('@kbn/babel-preset/node_preset')], + }); + + if (!result || typeof result.code !== 'string') { + throw new Error('babel transformation failed without an error...'); + } + + file.contents = Buffer.from(result.code); + file.extname = '.js'; + transformedFiles.add(file); +} diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 798746d159f60..2871fe2ffcf4a 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -41,3 +41,6 @@ export * from './stdio'; export * from './ci_stats_reporter'; export * from './plugin_list'; export * from './simple_kibana_platform_plugin_discovery'; +export * from './streams'; +export * from './babel'; +export * from './parse_kibana_platform_plugin'; diff --git a/packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts b/packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts new file mode 100644 index 0000000000000..83d8c2684d7ca --- /dev/null +++ b/packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Path from 'path'; +import loadJsonFile from 'load-json-file'; + +export interface KibanaPlatformPlugin { + readonly directory: string; + readonly manifestPath: string; + readonly manifest: { + id: string; + ui: boolean; + server: boolean; + [key: string]: unknown; + }; +} + +export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformPlugin { + if (!Path.isAbsolute(manifestPath)) { + throw new TypeError('expected new platform manifest path to be absolute'); + } + + const manifest = loadJsonFile.sync(manifestPath); + if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { + throw new TypeError('expected new platform plugin manifest to be a JSON encoded object'); + } + + if (typeof manifest.id !== 'string') { + throw new TypeError('expected new platform plugin manifest to have a string id'); + } + + return { + directory: Path.dirname(manifestPath), + manifestPath, + manifest: { + ...manifest, + + ui: !!manifest.ui, + server: !!manifest.server, + id: manifest.id, + }, + }; +} diff --git a/packages/kbn-dev-utils/src/run/flags.ts b/packages/kbn-dev-utils/src/run/flags.ts index 12642bceca15a..54758b4a7dbf8 100644 --- a/packages/kbn-dev-utils/src/run/flags.ts +++ b/packages/kbn-dev-utils/src/run/flags.ts @@ -52,8 +52,8 @@ export function mergeFlagOptions(global: FlagOptions = {}, local: FlagOptions = boolean: [...(global.boolean || []), ...(local.boolean || [])], string: [...(global.string || []), ...(local.string || [])], default: { - ...global.alias, - ...local.alias, + ...global.default, + ...local.default, }, help: local.help, diff --git a/packages/kbn-dev-utils/src/serializers/index.ts b/packages/kbn-dev-utils/src/serializers/index.ts index e645a3be3fe5d..6e0ac0b8be029 100644 --- a/packages/kbn-dev-utils/src/serializers/index.ts +++ b/packages/kbn-dev-utils/src/serializers/index.ts @@ -21,3 +21,4 @@ export * from './absolute_path_serializer'; export * from './strip_ansi_serializer'; export * from './recursive_serializer'; export * from './any_instance_serizlizer'; +export * from './replace_serializer'; diff --git a/packages/kbn-dev-utils/src/serializers/replace_serializer.ts b/packages/kbn-dev-utils/src/serializers/replace_serializer.ts new file mode 100644 index 0000000000000..06096c4bee3a2 --- /dev/null +++ b/packages/kbn-dev-utils/src/serializers/replace_serializer.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createRecursiveSerializer } from './recursive_serializer'; + +type Replacer = (substring: string, ...args: any[]) => string; + +export function createReplaceSerializer( + toReplace: string | RegExp, + replaceWith: string | Replacer +) { + return createRecursiveSerializer( + typeof toReplace === 'string' + ? (v: any) => typeof v === 'string' && v.includes(toReplace) + : (v: any) => typeof v === 'string' && toReplace.test(v), + typeof replaceWith === 'string' + ? (v: string) => v.replace(toReplace, replaceWith) + : (v: string) => v.replace(toReplace, replaceWith) + ); +} diff --git a/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts b/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts index c7155b2b3c51b..c56d63edb9ac4 100644 --- a/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts +++ b/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts @@ -20,67 +20,37 @@ import Path from 'path'; import globby from 'globby'; -import loadJsonFile from 'load-json-file'; -export interface KibanaPlatformPlugin { - readonly directory: string; - readonly manifestPath: string; - readonly manifest: { - id: string; - [key: string]: unknown; - }; -} +import { parseKibanaPlatformPlugin } from './parse_kibana_platform_plugin'; /** * Helper to find the new platform plugins. */ -export function simpleKibanaPlatformPluginDiscovery(scanDirs: string[], paths: string[]) { +export function simpleKibanaPlatformPluginDiscovery(scanDirs: string[], pluginPaths: string[]) { const patterns = Array.from( new Set([ // find kibana.json files up to 5 levels within the scan dir ...scanDirs.reduce( (acc: string[], dir) => [ ...acc, - `${dir}/*/kibana.json`, - `${dir}/*/*/kibana.json`, - `${dir}/*/*/*/kibana.json`, - `${dir}/*/*/*/*/kibana.json`, - `${dir}/*/*/*/*/*/kibana.json`, + Path.resolve(dir, '*/kibana.json'), + Path.resolve(dir, '*/*/kibana.json'), + Path.resolve(dir, '*/*/*/kibana.json'), + Path.resolve(dir, '*/*/*/*/kibana.json'), + Path.resolve(dir, '*/*/*/*/*/kibana.json'), ], [] ), - ...paths.map((path) => `${path}/kibana.json`), + ...pluginPaths.map((path) => Path.resolve(path, `kibana.json`)), ]) ); const manifestPaths = globby.sync(patterns, { absolute: true }).map((path) => - // absolute paths returned from globby are using normalize or something so the path separators are `/` even on windows, Path.resolve solves this + // absolute paths returned from globby are using normalize or + // something so the path separators are `/` even on windows, + // Path.resolve solves this Path.resolve(path) ); - return manifestPaths.map( - (manifestPath): KibanaPlatformPlugin => { - if (!Path.isAbsolute(manifestPath)) { - throw new TypeError('expected new platform manifest path to be absolute'); - } - - const manifest = loadJsonFile.sync(manifestPath); - if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { - throw new TypeError('expected new platform plugin manifest to be a JSON encoded object'); - } - - if (typeof manifest.id !== 'string') { - throw new TypeError('expected new platform plugin manifest to have a string id'); - } - - return { - directory: Path.dirname(manifestPath), - manifestPath, - manifest: { - ...manifest, - id: manifest.id, - }, - }; - } - ); + return manifestPaths.map(parseKibanaPlatformPlugin); } diff --git a/packages/kbn-plugin-generator/src/streams.ts b/packages/kbn-dev-utils/src/streams.ts similarity index 62% rename from packages/kbn-plugin-generator/src/streams.ts rename to packages/kbn-dev-utils/src/streams.ts index 976008e879dd3..6a868f648e78d 100644 --- a/packages/kbn-plugin-generator/src/streams.ts +++ b/packages/kbn-dev-utils/src/streams.ts @@ -20,7 +20,6 @@ import { Transform } from 'stream'; import File from 'vinyl'; -import { Minimatch } from 'minimatch'; interface BufferedFile extends File { contents: Buffer; @@ -33,41 +32,31 @@ interface BufferedFile extends File { * mutate the file, replace it with another file (return a new File * object), or drop it from the stream (return null) */ -export const tapFileStream = ( +export const transformFileStream = ( fn: (file: BufferedFile) => File | void | null | Promise ) => new Transform({ objectMode: true, - transform(file: BufferedFile, _, cb) { - Promise.resolve(file) - .then(fn) - .then( - (result) => { - // drop the file when null is returned - if (result === null) { - cb(); - } else { - cb(undefined, result || file); - } - }, - (error) => cb(error) - ); - }, - }); + transform(file: File, _, cb) { + Promise.resolve() + .then(async () => { + if (file.isDirectory()) { + return cb(undefined, file); + } -export const excludeFiles = (globs: string[]) => { - const patterns = globs.map( - (g) => - new Minimatch(g, { - matchBase: true, - }) - ); + if (!(file.contents instanceof Buffer)) { + throw new Error('files must be buffered to use transformFileStream()'); + } - return tapFileStream((file) => { - const path = file.relative.replace(/\.ejs$/, ''); - const exclude = patterns.some((p) => p.match(path)); - if (exclude) { - return null; - } + const result = await fn(file as BufferedFile); + + if (result === null) { + // explicitly drop file if null is returned + cb(); + } else { + cb(undefined, result || file); + } + }) + .catch(cb); + }, }); -}; diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts index a848d779dc9a2..8a3379211927b 100644 --- a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts +++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts @@ -50,7 +50,7 @@ export function findKibanaPlatformPlugins(scanDirs: string[], paths: string[]) { directory, manifestPath, id: manifest.id, - isUiPlugin: !!manifest.ui, + isUiPlugin: manifest.ui, extraPublicDirs: extraPublicDirs || [], }; } diff --git a/packages/kbn-plugin-generator/src/plugin_types.ts b/packages/kbn-plugin-generator/src/plugin_types.ts new file mode 100644 index 0000000000000..ae5201f4e8dbb --- /dev/null +++ b/packages/kbn-plugin-generator/src/plugin_types.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Path from 'path'; + +import { REPO_ROOT } from '@kbn/dev-utils'; + +export interface PluginType { + thirdParty: boolean; + installDir: string; +} + +export const PLUGIN_TYPE_OPTIONS: Array<{ name: string; value: PluginType }> = [ + { + name: 'Installable plugin', + value: { thirdParty: true, installDir: Path.resolve(REPO_ROOT, 'plugins') }, + }, + { + name: 'Kibana Example', + value: { thirdParty: false, installDir: Path.resolve(REPO_ROOT, 'examples') }, + }, + { + name: 'Kibana OSS', + value: { thirdParty: false, installDir: Path.resolve(REPO_ROOT, 'src/plugins') }, + }, + { + name: 'Kibana OSS Functional Testing', + value: { + thirdParty: false, + installDir: Path.resolve(REPO_ROOT, 'test/plugin_functional/plugins'), + }, + }, + { + name: 'X-Pack', + value: { thirdParty: false, installDir: Path.resolve(REPO_ROOT, 'x-pack/plugins') }, + }, + { + name: 'X-Pack Functional Testing', + value: { + thirdParty: false, + installDir: Path.resolve(REPO_ROOT, 'x-pack/test/plugin_functional/plugins'), + }, + }, +]; diff --git a/packages/kbn-plugin-generator/src/render_template.ts b/packages/kbn-plugin-generator/src/render_template.ts index 18bdcf1be1a6b..894088c119651 100644 --- a/packages/kbn-plugin-generator/src/render_template.ts +++ b/packages/kbn-plugin-generator/src/render_template.ts @@ -23,15 +23,32 @@ import { promisify } from 'util'; import vfs from 'vinyl-fs'; import prettier from 'prettier'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT, transformFileStream } from '@kbn/dev-utils'; import ejs from 'ejs'; +import { Minimatch } from 'minimatch'; import { snakeCase, camelCase, upperCamelCase } from './casing'; -import { excludeFiles, tapFileStream } from './streams'; import { Answers } from './ask_questions'; const asyncPipeline = promisify(pipeline); +const excludeFiles = (globs: string[]) => { + const patterns = globs.map( + (g) => + new Minimatch(g, { + matchBase: true, + }) + ); + + return transformFileStream((file) => { + const path = file.relative.replace(/\.ejs$/, ''); + const exclude = patterns.some((p) => p.match(path)); + if (exclude) { + return null; + } + }); +}; + /** * Stream all the files from the template directory, ignoring * certain files based on the answers, process the .ejs templates @@ -82,7 +99,7 @@ export async function renderTemplates({ ), // render .ejs templates and rename to not use .ejs extension - tapFileStream((file) => { + transformFileStream((file) => { if (file.extname !== '.ejs') { return; } @@ -108,7 +125,7 @@ export async function renderTemplates({ }), // format each file with prettier - tapFileStream((file) => { + transformFileStream((file) => { if (!file.extname) { return; } diff --git a/packages/kbn-plugin-generator/template/README.md.ejs b/packages/kbn-plugin-generator/template/README.md.ejs index 5f30bf0463305..2cd19c904263e 100755 --- a/packages/kbn-plugin-generator/template/README.md.ejs +++ b/packages/kbn-plugin-generator/template/README.md.ejs @@ -7,3 +7,14 @@ A Kibana plugin ## Development See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. + +<% if (thirdPartyPlugin) { %> +## Scripts +
+
yarn kbn bootstrap
+
Execute this to install node_modules and setup the dependencies in your plugin and in Kibana
+ +
yarn plugin-helpers build
+
Execute this to create a distributable version of this plugin that can be installed in Kibana
+
+<% } %> diff --git a/packages/kbn-plugin-generator/template/package.json.ejs b/packages/kbn-plugin-generator/template/package.json.ejs index cbd59894ca47c..ab234b1df2bc5 100644 --- a/packages/kbn-plugin-generator/template/package.json.ejs +++ b/packages/kbn-plugin-generator/template/package.json.ejs @@ -3,6 +3,8 @@ "version": "0.0.0", "private": true, "scripts": { + "build": "yarn plugin-helpers build", + "plugin-helpers": "node ../../scripts/plugin_helpers", "kbn": "node ../../scripts/kbn" } } diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index ba39528a1f809..129c58a4b4174 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -1,43 +1,32 @@ { "name": "@kbn/plugin-helpers", - "version": "9.0.2", + "version": "1.0.0", "private": true, "description": "Just some helpers for kibana plugin devs.", "license": "Apache-2.0", - "main": "target/lib/index.js", - "scripts": { - "kbn:bootstrap": "tsc" - }, + "main": "target/index.js", "bin": { "plugin-helpers": "bin/plugin-helpers.js" }, + "scripts": { + "kbn:bootstrap": "rm -rf target && tsc", + "kbn:watch": "tsc --watch" + }, "dependencies": { - "@babel/core": "^7.11.1", - "argv-split": "^2.0.1", - "commander": "^3.0.0", + "@kbn/dev-utils": "1.0.0", + "@kbn/optimizer": "1.0.0", "del": "^5.1.0", "execa": "^4.0.2", - "globby": "^8.0.1", - "gulp-babel": "^8.0.0", - "gulp-rename": "1.4.0", - "gulp-zip": "5.0.1", + "gulp-zip": "^5.0.2", "inquirer": "^1.2.2", - "minimatch": "^3.0.4", - "through2": "^2.0.3", - "through2-map": "^3.0.0", - "vinyl": "^2.2.0", + "load-json-file": "^6.2.0", "vinyl-fs": "^3.0.3" }, "devDependencies": { - "@types/gulp-rename": "^0.0.33", + "@types/decompress": "^4.2.3", "@types/gulp-zip": "^4.0.1", "@types/inquirer": "^6.5.0", - "@types/through2": "^2.0.35", - "@types/through2-map": "^3.0.0", - "@types/vinyl": "^2.0.4", + "decompress": "^4.2.1", "typescript": "4.0.2" - }, - "peerDependencies": { - "@kbn/babel-preset": "1.0.0" } } diff --git a/packages/kbn-plugin-helpers/bin/plugin-helpers.js b/packages/kbn-plugin-helpers/src/build_context.ts old mode 100755 new mode 100644 similarity index 73% rename from packages/kbn-plugin-helpers/bin/plugin-helpers.js rename to packages/kbn-plugin-helpers/src/build_context.ts index 175ff1019fa2d..62300d5a34e49 --- a/packages/kbn-plugin-helpers/bin/plugin-helpers.js +++ b/packages/kbn-plugin-helpers/src/build_context.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env node - /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -19,10 +17,16 @@ * under the License. */ -const nodeMajorVersion = parseFloat(process.version.replace(/^v(\d+)\..+/, '$1')); -if (nodeMajorVersion < 6) { - console.error('FATAL: kibana-plugin-helpers requires node 6+'); - process.exit(1); -} +import { ToolingLog } from '@kbn/dev-utils'; -require('../target/cli'); +import { Plugin } from './load_kibana_platform_plugin'; +import { Config } from './config'; + +export interface BuildContext { + log: ToolingLog; + plugin: Plugin; + config: Config; + sourceDir: string; + buildDir: string; + kibanaVersion: string; +} diff --git a/packages/kbn-plugin-helpers/src/cli.ts b/packages/kbn-plugin-helpers/src/cli.ts index 18ddc62cba8a6..21b6559f63650 100644 --- a/packages/kbn-plugin-helpers/src/cli.ts +++ b/packages/kbn-plugin-helpers/src/cli.ts @@ -17,59 +17,86 @@ * under the License. */ -import Fs from 'fs'; import Path from 'path'; -import program from 'commander'; +import { RunWithCommands, createFlagError, createFailError } from '@kbn/dev-utils'; -import { createCommanderAction } from './lib/commander_action'; -import { docs } from './lib/docs'; -import { enableCollectingUnknownOptions } from './lib/enable_collecting_unknown_options'; +import { findKibanaJson } from './find_kibana_json'; +import { loadKibanaPlatformPlugin } from './load_kibana_platform_plugin'; +import * as Tasks from './tasks'; +import { BuildContext } from './build_context'; +import { resolveKibanaVersion } from './resolve_kibana_version'; +import { loadConfig } from './config'; -const pkg = JSON.parse(Fs.readFileSync(Path.resolve(__dirname, '../package.json'), 'utf8')); -program.version(pkg.version); +export function runCli() { + new RunWithCommands({ + description: 'Some helper tasks for plugin-authors', + }) + .command({ + name: 'build', + description: ` + Copies files from the source into a zip archive that can be distributed for + installation into production Kibana 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: -enableCollectingUnknownOptions( - program - .command('start') - .description('Start kibana and have it include this plugin') - .on('--help', docs('start')) - .action( - createCommanderAction('start', (command) => ({ - flags: command.unknownOptions, - })) - ) -); + build/{plugin.id}-{kibanaVersion}.zip -program - .command('build [files...]') - .description('Build a distributable archive') - .on('--help', docs('build')) - .option('--skip-archive', "Don't create the zip file, leave the build path alone") - .option( - '-d, --build-destination ', - 'Target path for the build output, absolute or relative to the plugin root' - ) - .option('-b, --build-version ', 'Version for the build output') - .option('-k, --kibana-version ', 'Kibana version for the build output') - .action( - createCommanderAction('build', (command, files) => ({ - buildDestination: command.buildDestination, - buildVersion: command.buildVersion, - kibanaVersion: command.kibanaVersion, - skipArchive: Boolean(command.skipArchive), - files, - })) - ); + `, + flags: { + boolean: ['skip-archive'], + string: ['kibana-version'], + alias: { + k: 'kibana-version', + }, + help: ` + --skip-archive Don't create the zip file, just create the build/kibana directory + --kibana-version, -v Kibana version that the + `, + }, + async run({ log, flags }) { + const versionFlag = flags['kibana-version']; + if (versionFlag !== undefined && typeof versionFlag !== 'string') { + throw createFlagError('expected a single --kibana-version flag'); + } -program - .command('test:mocha [files...]') - .description('Run the server tests using mocha') - .on('--help', docs('test/mocha')) - .action( - createCommanderAction('testMocha', (command, files) => ({ - files, - })) - ); + const skipArchive = flags['skip-archive']; + if (skipArchive !== undefined && typeof skipArchive !== 'boolean') { + throw createFlagError('expected a single --skip-archive flag'); + } -program.parse(process.argv); + const pluginDir = await findKibanaJson(process.cwd()); + if (!pluginDir) { + throw createFailError( + `Unable to find Kibana Platform plugin in [${process.cwd()}] or any of its parent directories. Has it been migrated properly? Does it have a kibana.json file?` + ); + } + + const plugin = loadKibanaPlatformPlugin(pluginDir); + const config = await loadConfig(log, plugin); + const kibanaVersion = await resolveKibanaVersion(versionFlag, plugin); + const sourceDir = plugin.directory; + const buildDir = Path.resolve(plugin.directory, 'build/kibana', plugin.manifest.id); + + const context: BuildContext = { + log, + plugin, + config, + sourceDir, + buildDir, + kibanaVersion, + }; + + await Tasks.initTargets(context); + await Tasks.optimize(context); + await Tasks.writeServerFiles(context); + await Tasks.yarnInstall(context); + + if (skipArchive !== true) { + await Tasks.createArchive(context); + } + }, + }) + .execute(); +} diff --git a/packages/kbn-plugin-helpers/src/config.ts b/packages/kbn-plugin-helpers/src/config.ts new file mode 100644 index 0000000000000..bd5ad8ab6acc7 --- /dev/null +++ b/packages/kbn-plugin-helpers/src/config.ts @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Path from 'path'; + +import loadJsonFile from 'load-json-file'; + +import { ToolingLog } from '@kbn/dev-utils'; +import { Plugin } from './load_kibana_platform_plugin'; + +export interface Config { + skipInstallDependencies: boolean; + serverSourcePatterns?: string[]; +} + +const isArrayOfStrings = (v: any): v is string[] => + Array.isArray(v) && v.every((p) => typeof p === 'string'); + +export async function loadConfig(log: ToolingLog, plugin: Plugin): Promise { + try { + const path = Path.resolve(plugin.directory, '.kibana-plugin-helpers.json'); + const file = await loadJsonFile(path); + + if (!(typeof file === 'object' && file && !Array.isArray(file))) { + throw new TypeError(`expected config at [${path}] to be an object`); + } + + const { + skipInstallDependencies = false, + buildSourcePatterns, + serverSourcePatterns, + ...rest + } = file; + + if (typeof skipInstallDependencies !== 'boolean') { + throw new TypeError(`expected [skipInstallDependencies] at [${path}] to be a boolean`); + } + + if (buildSourcePatterns) { + log.warning( + `DEPRECATED: rename [buildSourcePatterns] to [serverSourcePatterns] in [${path}]` + ); + } + const ssp = buildSourcePatterns || serverSourcePatterns; + if (ssp !== undefined && !isArrayOfStrings(ssp)) { + throw new TypeError(`expected [serverSourcePatterns] at [${path}] to be an array of strings`); + } + + if (Object.keys(rest).length) { + throw new TypeError(`unexpected key in [${path}]: ${Object.keys(rest).join(', ')}`); + } + + log.info(`Loaded config file from [${path}]`); + return { + skipInstallDependencies, + serverSourcePatterns: ssp, + }; + } catch (error) { + if (error.code === 'ENOENT') { + return { + skipInstallDependencies: false, + }; + } + + throw error; + } +} diff --git a/packages/kbn-plugin-helpers/src/lib/run.ts b/packages/kbn-plugin-helpers/src/find_kibana_json.ts similarity index 65% rename from packages/kbn-plugin-helpers/src/lib/run.ts rename to packages/kbn-plugin-helpers/src/find_kibana_json.ts index 2b1a2a63c1074..9340309056830 100644 --- a/packages/kbn-plugin-helpers/src/lib/run.ts +++ b/packages/kbn-plugin-helpers/src/find_kibana_json.ts @@ -17,21 +17,21 @@ * under the License. */ -import { pluginConfig, PluginConfig } from './plugin_config'; -import { tasks, Tasks } from './tasks'; +import Path from 'path'; +import Fs from 'fs'; +import { promisify } from 'util'; -export interface TaskContext { - plugin: PluginConfig; - run: typeof run; - options?: any; -} +const existsAsync = promisify(Fs.exists); + +export async function findKibanaJson(directory: string): Promise { + if (await existsAsync(Path.resolve(directory, 'kibana.json'))) { + return directory; + } -export function run(name: keyof Tasks, options?: any) { - const action = tasks[name]; - if (!action) { - throw new Error('Invalid task: "' + name + '"'); + const parent = Path.dirname(directory); + if (parent === directory) { + return undefined; } - const plugin = pluginConfig(); - return action({ plugin, run, options }); + return findKibanaJson(parent); } diff --git a/packages/kbn-plugin-helpers/src/tasks/start/index.ts b/packages/kbn-plugin-helpers/src/index.ts similarity index 96% rename from packages/kbn-plugin-helpers/src/tasks/start/index.ts rename to packages/kbn-plugin-helpers/src/index.ts index cf34bdbadf416..a05bc698bde17 100644 --- a/packages/kbn-plugin-helpers/src/tasks/start/index.ts +++ b/packages/kbn-plugin-helpers/src/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export * from './start_task'; +export * from './cli'; diff --git a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts new file mode 100644 index 0000000000000..62f83cd672f3d --- /dev/null +++ b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts @@ -0,0 +1,123 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Path from 'path'; +import Fs from 'fs'; + +import execa from 'execa'; +import { createStripAnsiSerializer, REPO_ROOT, createReplaceSerializer } from '@kbn/dev-utils'; +import decompress from 'decompress'; +import del from 'del'; +import globby from 'globby'; +import loadJsonFile from 'load-json-file'; + +const PLUGIN_DIR = Path.resolve(REPO_ROOT, 'plugins/foo_test_plugin'); +const PLUGIN_BUILD_DIR = Path.resolve(PLUGIN_DIR, 'build'); +const PLUGIN_ARCHIVE = Path.resolve(PLUGIN_BUILD_DIR, `fooTestPlugin-7.5.0.zip`); +const TMP_DIR = Path.resolve(__dirname, '__tmp__'); + +expect.addSnapshotSerializer(createReplaceSerializer(/[\d\.]+ sec/g, '