From 8acb23b29f72e29913069386783985bab262d0bf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 17 Oct 2021 13:45:36 +0200 Subject: [PATCH 01/42] cli: initial create command Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 42 ++++++++++++++++++++++ packages/cli/src/commands/index.ts | 18 ++++++++++ 2 files changed, 60 insertions(+) create mode 100644 packages/cli/src/commands/create/create.ts diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts new file mode 100644 index 0000000000..ed4bcc9c20 --- /dev/null +++ b/packages/cli/src/commands/create/create.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed 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 { Command } from 'commander'; + +function parseOptions(optionStrings: string[]): Record { + const options: Record = {}; + + for (const str of optionStrings) { + const [key] = str.split('=', 1); + const value = str.slice(key.length + 1); + if (!key || !value) { + throw new Error( + `Invalid option '${str}', must be of the format =`, + ); + } + options[key] = value; + } + + return options; +} + +export default async (cmd: Command) => { + const selected = cmd.opts().select; + console.log('DEBUG: selected =', selected); + + const options = parseOptions(cmd.opts().option); + console.log('DEBUG: options =', options); +}; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 4e01a8ca9f..06832c466b 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -75,6 +75,24 @@ export function registerCommands(program: CommanderStatic) { .option(...configOption) .action(lazy(() => import('./backend/dev').then(m => m.default))); + program + .command('create') + .storeOptionsAsProperties(false) + .description( + 'Open up an interactive guide to creating new things in your app', + ) + .option( + '--select ', + 'Select the thing you want to be creating upfront', + ) + .option( + '--option =', + 'Pre-fill options for the creation process', + (opt, arr: string[]) => [...arr, opt], + [], + ) + .action(lazy(() => import('./create/create').then(m => m.default))); + program .command('create-plugin') .option( From e607456dbcb77d779ee28af8c1980ca4f9e713be Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 17 Oct 2021 16:59:59 +0200 Subject: [PATCH 02/42] cli: create command factory foundations Signed-off-by: Patrik Oldsberg --- .../src/commands/create/FactoryRegistry.ts | 76 +++++++++++++++++++ packages/cli/src/commands/create/create.ts | 13 +++- .../create/factories/frontendPlugin.ts | 46 +++++++++++ .../src/commands/create/factories/index.ts | 17 +++++ packages/cli/src/commands/create/types.ts | 34 +++++++++ 5 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 packages/cli/src/commands/create/FactoryRegistry.ts create mode 100644 packages/cli/src/commands/create/factories/frontendPlugin.ts create mode 100644 packages/cli/src/commands/create/factories/index.ts create mode 100644 packages/cli/src/commands/create/types.ts diff --git a/packages/cli/src/commands/create/FactoryRegistry.ts b/packages/cli/src/commands/create/FactoryRegistry.ts new file mode 100644 index 0000000000..0f48acc337 --- /dev/null +++ b/packages/cli/src/commands/create/FactoryRegistry.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 inquirer from 'inquirer'; +import { AnyFactory } from './types'; +import * as factories from './factories'; +import partition from 'lodash/partition'; + +export class FactoryRegistry { + private static factoryMap = new Map( + Object.values(factories).map(factory => [factory.name, factory]), + ); + + static async interactiveSelect(preselected?: string): Promise { + let selected = preselected; + + if (!selected) { + const answers = await inquirer.prompt<{ name: string }>([ + { + type: 'list', + name: 'name', + message: 'What do you want to create?', + choices: Array.from(this.factoryMap.values()).map(factory => ({ + name: `${factory.name} - ${factory.description}`, + value: factory.name, + })), + }, + ]); + selected = answers.name; + } + + const factory = this.factoryMap.get(selected); + if (!factory) { + throw new Error(`Unknown selection '${selected}'`); + } + return factory; + } + + static async populateOptions( + factory: AnyFactory, + provided: Record, + ): Promise> { + const [hasAnswers, needsAnswers] = partition( + factory.options, + option => option.name in provided, + ); + + for (const option of hasAnswers) { + const value = provided[option.name]; + + if (option.validate) { + const result = option.validate(value); + if (result !== true) { + throw new Error(`Invalid option '${option.name}'. ${result}`); + } + } + } + + const answers = await inquirer.prompt(needsAnswers); + + return { ...provided, ...answers }; + } +} diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index ed4bcc9c20..2397e7ec58 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -15,6 +15,7 @@ */ import { Command } from 'commander'; +import { FactoryRegistry } from './FactoryRegistry'; function parseOptions(optionStrings: string[]): Record { const options: Record = {}; @@ -34,9 +35,13 @@ function parseOptions(optionStrings: string[]): Record { } export default async (cmd: Command) => { - const selected = cmd.opts().select; - console.log('DEBUG: selected =', selected); + const factory = await FactoryRegistry.interactiveSelect(cmd.opts().select); - const options = parseOptions(cmd.opts().option); - console.log('DEBUG: options =', options); + const providedOptions = parseOptions(cmd.opts().option); + const options = await FactoryRegistry.populateOptions( + factory, + providedOptions, + ); + + await factory.create(options); }; diff --git a/packages/cli/src/commands/create/factories/frontendPlugin.ts b/packages/cli/src/commands/create/factories/frontendPlugin.ts new file mode 100644 index 0000000000..195953e780 --- /dev/null +++ b/packages/cli/src/commands/create/factories/frontendPlugin.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 { createFactory } from '../types'; + +type Options = { + id: string; +}; + +export const frontendPlugin = createFactory({ + name: 'plugin', + description: 'A new frontend plugin', + options: [ + { + type: 'input', + name: 'id', + message: 'Enter an ID for the plugin', + validate: (value: string) => { + if (!value) { + return 'Please enter an ID for the plugin'; + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return 'Plugin IDs must be lowercase and contain only letters, digits, and dashes.'; + } + return true; + }, + }, + ], + async create(options: Options) { + console.log( + `Creating ${this.name} with options ${JSON.stringify(options)}`, + ); + }, +}); diff --git a/packages/cli/src/commands/create/factories/index.ts b/packages/cli/src/commands/create/factories/index.ts new file mode 100644 index 0000000000..4375ba2a45 --- /dev/null +++ b/packages/cli/src/commands/create/factories/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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. + */ + +export { frontendPlugin } from './frontendPlugin'; diff --git a/packages/cli/src/commands/create/types.ts b/packages/cli/src/commands/create/types.ts new file mode 100644 index 0000000000..e5252df6bc --- /dev/null +++ b/packages/cli/src/commands/create/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 { DistinctQuestion } from 'inquirer'; + +export type AnyOptions = Record; + +export interface Factory { + name: string; + description: string; + options: ReadonlyArray & { name: string }>; + create(options: Options): Promise; +} + +export type AnyFactory = Factory; + +export function createFactory( + config: Factory, +): AnyFactory { + return config as AnyFactory; +} From 2ca3f5ff4c3585505a72fdd285382c44451b51d6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 11:22:30 +0100 Subject: [PATCH 03/42] cli: added support for dynamically discovering create options Signed-off-by: Patrik Oldsberg --- .../src/commands/create/FactoryRegistry.ts | 42 ++++++++++++------- .../create/factories/frontendPlugin.ts | 29 ++++++++++++- packages/cli/src/commands/create/types.ts | 3 +- 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/commands/create/FactoryRegistry.ts b/packages/cli/src/commands/create/FactoryRegistry.ts index 0f48acc337..a959896bd0 100644 --- a/packages/cli/src/commands/create/FactoryRegistry.ts +++ b/packages/cli/src/commands/create/FactoryRegistry.ts @@ -53,24 +53,36 @@ export class FactoryRegistry { factory: AnyFactory, provided: Record, ): Promise> { - const [hasAnswers, needsAnswers] = partition( - factory.options, - option => option.name in provided, - ); + let currentOptions = provided; - for (const option of hasAnswers) { - const value = provided[option.name]; - - if (option.validate) { - const result = option.validate(value); - if (result !== true) { - throw new Error(`Invalid option '${option.name}'. ${result}`); - } - } + if (factory.optionsDiscovery) { + const discoveredOptions = await factory.optionsDiscovery(); + currentOptions = { + ...currentOptions, + ...(discoveredOptions as Record), + }; } - const answers = await inquirer.prompt(needsAnswers); + if (factory.optionsPrompts) { + const [hasAnswers, needsAnswers] = partition( + factory.optionsPrompts, + option => option.name in currentOptions, + ); - return { ...provided, ...answers }; + for (const option of hasAnswers) { + const value = provided[option.name]; + + if (option.validate) { + const result = option.validate(value); + if (result !== true) { + throw new Error(`Invalid option '${option.name}'. ${result}`); + } + } + } + + currentOptions = await inquirer.prompt(needsAnswers, currentOptions); + } + + return currentOptions; } } diff --git a/packages/cli/src/commands/create/factories/frontendPlugin.ts b/packages/cli/src/commands/create/factories/frontendPlugin.ts index 195953e780..35e18b9e09 100644 --- a/packages/cli/src/commands/create/factories/frontendPlugin.ts +++ b/packages/cli/src/commands/create/factories/frontendPlugin.ts @@ -14,20 +14,27 @@ * limitations under the License. */ +import { paths } from '../../../lib/paths'; +import { getCodeownersFilePath, parseOwnerIds } from '../../../lib/codeowners'; import { createFactory } from '../types'; type Options = { id: string; + owner?: string; + codeOwnersPath?: string; }; export const frontendPlugin = createFactory({ name: 'plugin', description: 'A new frontend plugin', - options: [ + optionsDiscovery: async () => ({ + codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), + }), + optionsPrompts: [ { type: 'input', name: 'id', - message: 'Enter an ID for the plugin', + message: 'Enter an ID for the plugin [required]', validate: (value: string) => { if (!value) { return 'Please enter an ID for the plugin'; @@ -37,6 +44,24 @@ export const frontendPlugin = createFactory({ return true; }, }, + { + type: 'input', + name: 'owner', + message: 'Enter an owner of the plugin to add to CODEOWNERS [optional]', + when: opts => Boolean(opts.codeOwnersPath), + validate: (value: string) => { + if (!value) { + return true; + } + + const ownerIds = parseOwnerIds(value); + if (!ownerIds) { + return 'The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses (e.g. user@example.com).'; + } + + return true; + }, + }, ], async create(options: Options) { console.log( diff --git a/packages/cli/src/commands/create/types.ts b/packages/cli/src/commands/create/types.ts index e5252df6bc..a797664e37 100644 --- a/packages/cli/src/commands/create/types.ts +++ b/packages/cli/src/commands/create/types.ts @@ -21,7 +21,8 @@ export type AnyOptions = Record; export interface Factory { name: string; description: string; - options: ReadonlyArray & { name: string }>; + optionsDiscovery?(): Promise>; + optionsPrompts?: ReadonlyArray & { name: string }>; create(options: Options): Promise; } From 55e58c55684109303b8559db2ea578bf88e72c5d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 11:24:40 +0100 Subject: [PATCH 04/42] cli: moved bulk of create implementation over to lib Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 2 +- packages/cli/src/{commands => lib}/create/FactoryRegistry.ts | 0 .../src/{commands => lib}/create/factories/frontendPlugin.ts | 0 packages/cli/src/{commands => lib}/create/factories/index.ts | 0 packages/cli/src/{commands => lib}/create/types.ts | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename packages/cli/src/{commands => lib}/create/FactoryRegistry.ts (100%) rename packages/cli/src/{commands => lib}/create/factories/frontendPlugin.ts (100%) rename packages/cli/src/{commands => lib}/create/factories/index.ts (100%) rename packages/cli/src/{commands => lib}/create/types.ts (100%) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index 2397e7ec58..4c1ca68557 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -15,7 +15,7 @@ */ import { Command } from 'commander'; -import { FactoryRegistry } from './FactoryRegistry'; +import { FactoryRegistry } from '../../lib/create/FactoryRegistry'; function parseOptions(optionStrings: string[]): Record { const options: Record = {}; diff --git a/packages/cli/src/commands/create/FactoryRegistry.ts b/packages/cli/src/lib/create/FactoryRegistry.ts similarity index 100% rename from packages/cli/src/commands/create/FactoryRegistry.ts rename to packages/cli/src/lib/create/FactoryRegistry.ts diff --git a/packages/cli/src/commands/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts similarity index 100% rename from packages/cli/src/commands/create/factories/frontendPlugin.ts rename to packages/cli/src/lib/create/factories/frontendPlugin.ts diff --git a/packages/cli/src/commands/create/factories/index.ts b/packages/cli/src/lib/create/factories/index.ts similarity index 100% rename from packages/cli/src/commands/create/factories/index.ts rename to packages/cli/src/lib/create/factories/index.ts diff --git a/packages/cli/src/commands/create/types.ts b/packages/cli/src/lib/create/types.ts similarity index 100% rename from packages/cli/src/commands/create/types.ts rename to packages/cli/src/lib/create/types.ts From 3ca0d3368eb6ec86690941ef7300b312bdfcbd5e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 11:28:54 +0100 Subject: [PATCH 05/42] cli: display create prompt messages in blue and errors in red Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/create/FactoryRegistry.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/lib/create/FactoryRegistry.ts b/packages/cli/src/lib/create/FactoryRegistry.ts index a959896bd0..1b25f5950a 100644 --- a/packages/cli/src/lib/create/FactoryRegistry.ts +++ b/packages/cli/src/lib/create/FactoryRegistry.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import chalk from 'chalk'; import inquirer from 'inquirer'; import { AnyFactory } from './types'; import * as factories from './factories'; @@ -80,7 +81,22 @@ export class FactoryRegistry { } } - currentOptions = await inquirer.prompt(needsAnswers, currentOptions); + currentOptions = await inquirer.prompt( + needsAnswers.map(option => ({ + ...option, + message: option.message && chalk.blue(option.message), + validate: + option.validate && + (async (...args) => { + const result = await option.validate!(...args); + if (typeof result === 'string') { + return chalk.red(result); + } + return result; + }), + })), + currentOptions, + ); } return currentOptions; From 84936dbcbe9fac0244c71ca90da7404a5e5d2a78 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 12:03:23 +0100 Subject: [PATCH 06/42] cli: cmd options and discovery for additional creation context Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 37 +++++++++++++++++-- packages/cli/src/commands/index.ts | 6 +++ .../lib/create/factories/frontendPlugin.ts | 8 ++-- packages/cli/src/lib/create/types.ts | 15 +++++++- 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index 4c1ca68557..07378579d2 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -14,8 +14,11 @@ * limitations under the License. */ +import fs from 'fs-extra'; import { Command } from 'commander'; import { FactoryRegistry } from '../../lib/create/FactoryRegistry'; +import { paths } from '../../lib/paths'; +import { assertError } from '@backstage/errors'; function parseOptions(optionStrings: string[]): Record { const options: Record = {}; @@ -35,13 +38,41 @@ function parseOptions(optionStrings: string[]): Record { } export default async (cmd: Command) => { - const factory = await FactoryRegistry.interactiveSelect(cmd.opts().select); + const cmdOpts = cmd.opts(); - const providedOptions = parseOptions(cmd.opts().option); + const factory = await FactoryRegistry.interactiveSelect(cmdOpts.select); + + const providedOptions = parseOptions(cmdOpts.option); const options = await FactoryRegistry.populateOptions( factory, providedOptions, ); - await factory.create(options); + const rootPackageJson = await fs.readJson( + paths.resolveTargetRoot('package.json'), + ); + const isMonoRepo = Boolean(rootPackageJson.workspaces); + + let defaultVersion = '0.1.0'; + try { + const rootLernaJson = await fs.readJson( + paths.resolveTargetRoot('lerna.json'), + ); + if (rootLernaJson.version) { + defaultVersion = rootLernaJson.version; + } + } catch (error) { + assertError(error); + if (error.code !== 'ENOENT') { + throw error; + } + } + + await factory.create(options, { + isMonoRepo, + defaultVersion, + scope: cmdOpts.scope.replace(/^@/, ''), + npmRegistry: cmdOpts.npmRegistry, + private: Boolean(cmdOpts.private), + }); }; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 06832c466b..a37d5e9ffe 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -91,6 +91,12 @@ export function registerCommands(program: CommanderStatic) { (opt, arr: string[]) => [...arr, opt], [], ) + .option('--scope ', 'The scope to use for new packages') + .option( + '--npm-registry ', + 'The package registry to use for new packages', + ) + .option('--no-private', 'Do not mark new packages as private') .action(lazy(() => import('./create/create').then(m => m.default))); program diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index 35e18b9e09..1b13cff13d 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -16,7 +16,7 @@ import { paths } from '../../../lib/paths'; import { getCodeownersFilePath, parseOwnerIds } from '../../../lib/codeowners'; -import { createFactory } from '../types'; +import { createFactory, CreateContext } from '../types'; type Options = { id: string; @@ -63,9 +63,11 @@ export const frontendPlugin = createFactory({ }, }, ], - async create(options: Options) { + async create(options: Options, context: CreateContext) { console.log( - `Creating ${this.name} with options ${JSON.stringify(options)}`, + `Creating ${this.name} with options ${JSON.stringify( + options, + )} and context ${JSON.stringify(context)}`, ); }, }); diff --git a/packages/cli/src/lib/create/types.ts b/packages/cli/src/lib/create/types.ts index a797664e37..82bdd4978a 100644 --- a/packages/cli/src/lib/create/types.ts +++ b/packages/cli/src/lib/create/types.ts @@ -16,6 +16,19 @@ import { DistinctQuestion } from 'inquirer'; +export interface CreateContext { + /** The package scope to use for new packages */ + scope?: string; + /** The NPM registry to use for new packages */ + npmRegistry?: string; + /** Whether new packages should be marked as private */ + private: boolean; + /** Whether we are creating something in a monorepo or not */ + isMonoRepo: boolean; + /** The default version to use for new packages */ + defaultVersion: string; +} + export type AnyOptions = Record; export interface Factory { @@ -23,7 +36,7 @@ export interface Factory { description: string; optionsDiscovery?(): Promise>; optionsPrompts?: ReadonlyArray & { name: string }>; - create(options: Options): Promise; + create(options: Options, context?: CreateContext): Promise; } export type AnyFactory = Factory; From 14e8162d199cbf80dd1e4dfcce6006758e6c89ea Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 13:08:25 +0100 Subject: [PATCH 07/42] cli: added temp dir utility to create context Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 36 +++++++++++++++++----- packages/cli/src/lib/create/types.ts | 3 ++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index 07378579d2..d39fa2b9d0 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -14,7 +14,9 @@ * limitations under the License. */ +import os from 'os'; import fs from 'fs-extra'; +import { join as joinPath } from 'path'; import { Command } from 'commander'; import { FactoryRegistry } from '../../lib/create/FactoryRegistry'; import { paths } from '../../lib/paths'; @@ -68,11 +70,31 @@ export default async (cmd: Command) => { } } - await factory.create(options, { - isMonoRepo, - defaultVersion, - scope: cmdOpts.scope.replace(/^@/, ''), - npmRegistry: cmdOpts.npmRegistry, - private: Boolean(cmdOpts.private), - }); + const tempDirs = new Array(); + async function createTemporaryDirectory(name: string): Promise { + const dir = await fs.mkdtemp(joinPath(os.tmpdir(), name)); + tempDirs.push(dir); + return dir; + } + + try { + await factory.create(options, { + isMonoRepo, + defaultVersion, + scope: cmdOpts.scope.replace(/^@/, ''), + npmRegistry: cmdOpts.npmRegistry, + private: Boolean(cmdOpts.private), + createTemporaryDirectory, + }); + } finally { + for (const dir of tempDirs) { + try { + await fs.remove(dir); + } catch (error) { + console.error( + `Failed to remove temporary directory '${dir}', ${error}`, + ); + } + } + } }; diff --git a/packages/cli/src/lib/create/types.ts b/packages/cli/src/lib/create/types.ts index 82bdd4978a..629506b5f7 100644 --- a/packages/cli/src/lib/create/types.ts +++ b/packages/cli/src/lib/create/types.ts @@ -27,6 +27,9 @@ export interface CreateContext { isMonoRepo: boolean; /** The default version to use for new packages */ defaultVersion: string; + + /** Creates a temporary directory. This will always be deleted after creation is done. */ + createTemporaryDirectory(name: string): Promise; } export type AnyOptions = Record; From e04cce9cdb5cf02e7c9748c5bb95d9b59391e421 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 15:22:11 +0100 Subject: [PATCH 08/42] cli: wrap create factories up in a bit more built-in logging Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 28 ++++++++++++++++++++++ packages/cli/src/lib/create/types.ts | 3 +++ 2 files changed, 31 insertions(+) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index d39fa2b9d0..7004341c3e 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -21,6 +21,7 @@ import { Command } from 'commander'; import { FactoryRegistry } from '../../lib/create/FactoryRegistry'; import { paths } from '../../lib/paths'; import { assertError } from '@backstage/errors'; +import { Task } from '../../lib/tasks'; function parseOptions(optionStrings: string[]): Record { const options: Record = {}; @@ -77,7 +78,11 @@ export default async (cmd: Command) => { return dir; } + let modified = false; try { + Task.log(); + Task.log(`Creating new ${factory.name}`); + await factory.create(options, { isMonoRepo, defaultVersion, @@ -85,7 +90,30 @@ export default async (cmd: Command) => { npmRegistry: cmdOpts.npmRegistry, private: Boolean(cmdOpts.private), createTemporaryDirectory, + markAsModified() { + modified = true; + }, }); + + Task.log(); + Task.log(`🎉 Successfully created ${factory.name}`); + Task.log(); + } catch (error) { + assertError(error); + Task.error(error.message); + + if (modified) { + Task.log('It seems that something went wrong in the creation process 🤔'); + Task.log(); + Task.log( + 'We have left the changes that were made intact in case you want to', + ); + Task.log( + 'continue manually, but you can also revert the changes and try again.', + ); + + Task.error(`🔥 Failed to create ${factory.name}!`); + } } finally { for (const dir of tempDirs) { try { diff --git a/packages/cli/src/lib/create/types.ts b/packages/cli/src/lib/create/types.ts index 629506b5f7..6a45067e9b 100644 --- a/packages/cli/src/lib/create/types.ts +++ b/packages/cli/src/lib/create/types.ts @@ -30,6 +30,9 @@ export interface CreateContext { /** Creates a temporary directory. This will always be deleted after creation is done. */ createTemporaryDirectory(name: string): Promise; + + /** Signal that the creation process got to a point where permanent modifications were made */ + markAsModified(): void; } export type AnyOptions = Record; From f8adebde77b0a6c299f1463f4a759942fab7be55 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 15:23:32 +0100 Subject: [PATCH 09/42] cli: build out task lib a bit with command execution, adding deps Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/tasks.ts | 81 +++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index e17015d6cf..691e77bd17 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -18,9 +18,14 @@ import chalk from 'chalk'; import fs from 'fs-extra'; import handlebars from 'handlebars'; import ora from 'ora'; +import { promisify } from 'util'; import { basename, dirname } from 'path'; import recursive from 'recursive-readdir'; +import { exec as execCb } from 'child_process'; import { paths } from './paths'; +import { assertError } from '@backstage/errors'; + +const exec = promisify(execCb); const TASK_NAME_MAX_LENGTH = 14; @@ -42,11 +47,11 @@ export class Task { process.exit(code); } - static async forItem( + static async forItem( task: string, item: string, - taskFunc: () => Promise, - ): Promise { + taskFunc: () => Promise, + ): Promise { const paddedTask = chalk.green(task.padEnd(TASK_NAME_MAX_LENGTH)); const spinner = ora({ @@ -56,13 +61,40 @@ export class Task { }).start(); try { - await taskFunc(); + const result = await taskFunc(); spinner.succeed(); + return result; } catch (error) { spinner.fail(); throw error; } } + + static async forCommand( + command: string, + options?: { cwd?: string; optional?: boolean }, + ) { + try { + await Task.forItem('executing', command, async () => { + await exec(command, { cwd: options?.cwd }); + }); + } catch (error) { + assertError(error); + if (error.stderr) { + process.stdout.write(error.stderr as Buffer); + } + if (error.stdout) { + process.stdout.write(error.stdout as Buffer); + } + if (options?.optional) { + Task.error(`Warning: Failed to execute command ${chalk.cyan(command)}`); + } else { + throw new Error( + `Failed to execute command '${chalk.cyan(command)}', ${error}`, + ); + } + } + } } export async function templatingTask( @@ -122,3 +154,44 @@ export async function templatingTask( } } } + +export async function addPackageDependency( + path: string, + options: { + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + }, +) { + try { + const pkgJson = await fs.readJson(path); + + const normalize = (obj: Record) => { + if (Object.keys(obj).length === 0) { + return undefined; + } + return Object.fromEntries( + Object.keys(obj) + .sort() + .map(key => [key, obj[key]]), + ); + }; + + pkgJson.dependencies = normalize({ + ...pkgJson.dependencies, + ...options.dependencies, + }); + pkgJson.devDependencies = normalize({ + ...pkgJson.devDependencies, + ...options.devDependencies, + }); + pkgJson.peerDependencies = normalize({ + ...pkgJson.peerDependencies, + ...options.peerDependencies, + }); + + await fs.writeJson(path, pkgJson, { spaces: 2 }); + } catch (error) { + throw new Error(`Failed to add package dependencies, ${error}`); + } +} From 602cf4d80dabe6a24e4585da37b9a0af09a3b320 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 15:24:08 +0100 Subject: [PATCH 10/42] cli: run templating tasks in strict mode Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/tasks.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index 691e77bd17..9e67f7f30c 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -117,7 +117,9 @@ export async function templatingTask( const destination = destinationFile.replace(/\.hbs$/, ''); const template = await fs.readFile(file); - const compiled = handlebars.compile(template.toString()); + const compiled = handlebars.compile(template.toString(), { + strict: true, + }); const contents = compiled( { name: basename(destination), ...context }, { From 248c8eaa25af0f4147fc383fe553765068530f44 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 15:33:45 +0100 Subject: [PATCH 11/42] cli: allow empty create option values Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index 7004341c3e..946918d77c 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -29,7 +29,7 @@ function parseOptions(optionStrings: string[]): Record { for (const str of optionStrings) { const [key] = str.split('=', 1); const value = str.slice(key.length + 1); - if (!key || !value) { + if (!key || str[key.length] !== '=') { throw new Error( `Invalid option '${str}', must be of the format =`, ); From 7b8d432de5842a420ca711e5895462118176ed12 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 15:39:17 +0100 Subject: [PATCH 12/42] cli: complete frontendPlugin create factory implementation Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/codeowners/codeowners.ts | 2 +- .../lib/create/factories/frontendPlugin.ts | 144 +++++++++++++++++- 2 files changed, 138 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/lib/codeowners/codeowners.ts b/packages/cli/src/lib/codeowners/codeowners.ts index 563bd1052d..b3fac12109 100644 --- a/packages/cli/src/lib/codeowners/codeowners.ts +++ b/packages/cli/src/lib/codeowners/codeowners.ts @@ -55,7 +55,7 @@ export function isValidSingleOwnerId(id: string): boolean { } export function parseOwnerIds( - spaceSeparatedOwnerIds: string, + spaceSeparatedOwnerIds: string | undefined, ): string[] | undefined { if (!spaceSeparatedOwnerIds || typeof spaceSeparatedOwnerIds !== 'string') { return undefined; diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index 1b13cff13d..a85403e8ed 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -14,9 +14,20 @@ * limitations under the License. */ -import { paths } from '../../../lib/paths'; -import { getCodeownersFilePath, parseOwnerIds } from '../../../lib/codeowners'; +import fs from 'fs-extra'; +import camelCase from 'lodash/camelCase'; +import upperFirst from 'lodash/upperFirst'; +import chalk from 'chalk'; +import { paths } from '../../paths'; +import { + addCodeownersEntry, + getCodeownersFilePath, + parseOwnerIds, +} from '../../codeowners'; import { createFactory, CreateContext } from '../types'; +import { Lockfile } from '../../versioning'; +import { addPackageDependency, Task, templatingTask } from '../../tasks'; +import { createPackageVersionProvider } from '../../version'; type Options = { id: string; @@ -63,11 +74,130 @@ export const frontendPlugin = createFactory({ }, }, ], - async create(options: Options, context: CreateContext) { - console.log( - `Creating ${this.name} with options ${JSON.stringify( - options, - )} and context ${JSON.stringify(context)}`, + async create(options: Options, ctx: CreateContext) { + const { id } = options; + + const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + const extensionName = `${upperFirst(camelCase(id))}Page`; + + const pluginDir = ctx.isMonoRepo + ? paths.resolveTargetRoot('plugins', id) + : paths.resolveTargetRoot(`backstage-plugin-${id}`); + + let lockfile: Lockfile | undefined; + try { + lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + } catch (error) { + console.warn(`No yarn.lock available, ${error}`); + } + + Task.section('Validating prerequisites'); + const shortPluginDir = pluginDir.replace(`${paths.targetRoot}/`, ''); + await Task.forItem('availability', shortPluginDir, async () => { + if (await fs.pathExists(pluginDir)) { + throw new Error( + `A plugin with the same ID already exists at ${chalk.cyan( + shortPluginDir, + )}. Please try again with a different ID.`, + ); + } + }); + + const tempDir = await Task.forItem('creating', 'temp dir', async () => { + return await ctx.createTemporaryDirectory(`backstage-plugin-${id}`); + }); + + Task.section('Executing plugin template'); + await templatingTask( + paths.resolveOwn('templates/default-plugin'), + tempDir, + { + id, + pluginVar: `${camelCase(id)}Plugin`, + pluginVersion: ctx.defaultVersion, + extensionName, + name, + privatePackage: ctx.private, + npmRegistry: ctx.npmRegistry, + }, + createPackageVersionProvider(lockfile), ); + + Task.section('Installing plugin'); + await Task.forItem('moving', shortPluginDir, async () => { + await fs.move(tempDir, pluginDir).catch(error => { + throw new Error( + `Failed to move plugin from ${tempDir} to ${pluginDir}, ${error.message}`, + ); + }); + }); + + ctx.markAsModified(); + + if (await fs.pathExists(paths.resolveTargetRoot('packages/app'))) { + await Task.forItem('app', 'adding dependency', async () => { + await addPackageDependency( + paths.resolveTargetRoot('packages/app/package.json'), + { + dependencies: { + [name]: `^${ctx.defaultVersion}`, + }, + }, + ); + }); + + await Task.forItem('app', 'adding import', async () => { + const pluginsFilePath = paths.resolveTargetRoot( + 'packages/app/src/App.tsx', + ); + if (!(await fs.pathExists(pluginsFilePath))) { + return; + } + + const content = await fs.readFile(pluginsFilePath, 'utf8'); + const revLines = content.split('\n').reverse(); + + const lastImportIndex = revLines.findIndex(line => + line.match(/ from ("|').*("|')/), + ); + const lastRouteIndex = revLines.findIndex(line => + line.match(/<\/FlatRoutes/), + ); + + if (lastImportIndex !== -1 && lastRouteIndex !== -1) { + const importLine = `import { ${extensionName} } from '${name}';`; + if (!content.includes(importLine)) { + revLines.splice(lastImportIndex, 0, importLine); + } + + const componentLine = `}/>`; + if (!content.includes(componentLine)) { + const [indentation] = + revLines[lastRouteIndex + 1].match(/^\s*/) ?? []; + revLines.splice(lastRouteIndex + 1, 0, indentation + componentLine); + } + + const newContent = revLines.reverse().join('\n'); + await fs.writeFile(pluginsFilePath, newContent, 'utf8'); + } + }); + } + + if (options.codeOwnersPath && options.owner) { + const ownerIds = parseOwnerIds(options.owner); + if (ownerIds && ownerIds.length > 0) { + await addCodeownersEntry( + options.codeOwnersPath, + `/plugins/${id}`, + ownerIds, + ); + } + } + + await Task.forCommand('yarn install', { cwd: pluginDir, optional: true }); + await Task.forCommand('yarn lint --fix', { + cwd: pluginDir, + optional: true, + }); }, }); From 1c0ac6291cdaa278aa58cf00592e6ab7b562e92d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 16:00:54 +0100 Subject: [PATCH 13/42] cli: added create backend plugin factory Signed-off-by: Patrik Oldsberg --- .../src/lib/create/factories/backendPlugin.ts | 164 ++++++++++++++++++ .../cli/src/lib/create/factories/index.ts | 1 + 2 files changed, 165 insertions(+) create mode 100644 packages/cli/src/lib/create/factories/backendPlugin.ts diff --git a/packages/cli/src/lib/create/factories/backendPlugin.ts b/packages/cli/src/lib/create/factories/backendPlugin.ts new file mode 100644 index 0000000000..9cf78900a7 --- /dev/null +++ b/packages/cli/src/lib/create/factories/backendPlugin.ts @@ -0,0 +1,164 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 fs from 'fs-extra'; +import camelCase from 'lodash/camelCase'; +import upperFirst from 'lodash/upperFirst'; +import chalk from 'chalk'; +import { paths } from '../../paths'; +import { + addCodeownersEntry, + getCodeownersFilePath, + parseOwnerIds, +} from '../../codeowners'; +import { createFactory, CreateContext } from '../types'; +import { Lockfile } from '../../versioning'; +import { addPackageDependency, Task, templatingTask } from '../../tasks'; +import { createPackageVersionProvider } from '../../version'; + +type Options = { + id: string; + owner?: string; + codeOwnersPath?: string; +}; + +export const backendPlugin = createFactory({ + name: 'backend-plugin', + description: 'A new backend plugin', + optionsDiscovery: async () => ({ + codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), + }), + optionsPrompts: [ + { + type: 'input', + name: 'id', + message: 'Enter an ID for the plugin [required]', + validate: (value: string) => { + if (!value) { + return 'Please enter an ID for the plugin'; + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return 'Plugin IDs must be lowercase and contain only letters, digits, and dashes.'; + } + return true; + }, + }, + { + type: 'input', + name: 'owner', + message: 'Enter an owner of the plugin to add to CODEOWNERS [optional]', + when: opts => Boolean(opts.codeOwnersPath), + validate: (value: string) => { + if (!value) { + return true; + } + + const ownerIds = parseOwnerIds(value); + if (!ownerIds) { + return 'The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses (e.g. user@example.com).'; + } + + return true; + }, + }, + ], + async create(options: Options, ctx: CreateContext) { + const id = `${options.id}-backend`; + const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + + const pluginDir = ctx.isMonoRepo + ? paths.resolveTargetRoot('plugins', id) + : paths.resolveTargetRoot(`backstage-plugin-${id}`); + + let lockfile: Lockfile | undefined; + try { + lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + } catch (error) { + console.warn(`No yarn.lock available, ${error}`); + } + + Task.section('Validating prerequisites'); + const shortPluginDir = pluginDir.replace(`${paths.targetRoot}/`, ''); + await Task.forItem('availability', shortPluginDir, async () => { + if (await fs.pathExists(pluginDir)) { + throw new Error( + `A backend plugin with the same ID already exists at ${chalk.cyan( + shortPluginDir, + )}. Please try again with a different ID.`, + ); + } + }); + + const tempDir = await Task.forItem('creating', 'temp dir', async () => { + return await ctx.createTemporaryDirectory(`backstage-plugin-${id}`); + }); + + Task.section('Executing plugin template'); + await templatingTask( + paths.resolveOwn('templates/default-backend-plugin'), + tempDir, + { + id, + name, + pluginVar: `${camelCase(id)}Plugin`, + pluginVersion: ctx.defaultVersion, + privatePackage: ctx.private, + npmRegistry: ctx.npmRegistry, + }, + createPackageVersionProvider(lockfile), + ); + + Task.section('Installing plugin'); + await Task.forItem('moving', shortPluginDir, async () => { + await fs.move(tempDir, pluginDir).catch(error => { + throw new Error( + `Failed to move plugin from ${tempDir} to ${pluginDir}, ${error.message}`, + ); + }); + }); + + ctx.markAsModified(); + + if (await fs.pathExists(paths.resolveTargetRoot('packages/backend'))) { + await Task.forItem('backend', 'adding dependency', async () => { + await addPackageDependency( + paths.resolveTargetRoot('packages/backend/package.json'), + { + dependencies: { + [name]: `^${ctx.defaultVersion}`, + }, + }, + ); + }); + } + + if (options.codeOwnersPath && options.owner) { + const ownerIds = parseOwnerIds(options.owner); + if (ownerIds && ownerIds.length > 0) { + await addCodeownersEntry( + options.codeOwnersPath, + `/plugins/${id}`, + ownerIds, + ); + } + } + + await Task.forCommand('yarn install', { cwd: pluginDir, optional: true }); + await Task.forCommand('yarn lint --fix', { + cwd: pluginDir, + optional: true, + }); + }, +}); diff --git a/packages/cli/src/lib/create/factories/index.ts b/packages/cli/src/lib/create/factories/index.ts index 4375ba2a45..2e16979e50 100644 --- a/packages/cli/src/lib/create/factories/index.ts +++ b/packages/cli/src/lib/create/factories/index.ts @@ -15,3 +15,4 @@ */ export { frontendPlugin } from './frontendPlugin'; +export { backendPlugin } from './backendPlugin'; From 142cd41b7e351c20f23a6a9f611586b28bcf81ce Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 16:08:32 +0100 Subject: [PATCH 14/42] cli: refactor create factories to use common prompts Signed-off-by: Patrik Oldsberg --- .../src/lib/create/factories/backendPlugin.ts | 35 +---------- .../lib/create/factories/common/prompts.ts | 58 +++++++++++++++++++ .../lib/create/factories/frontendPlugin.ts | 35 +---------- packages/cli/src/lib/create/types.ts | 14 +++-- 4 files changed, 70 insertions(+), 72 deletions(-) create mode 100644 packages/cli/src/lib/create/factories/common/prompts.ts diff --git a/packages/cli/src/lib/create/factories/backendPlugin.ts b/packages/cli/src/lib/create/factories/backendPlugin.ts index 9cf78900a7..d7855d5d68 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.ts @@ -28,6 +28,7 @@ import { createFactory, CreateContext } from '../types'; import { Lockfile } from '../../versioning'; import { addPackageDependency, Task, templatingTask } from '../../tasks'; import { createPackageVersionProvider } from '../../version'; +import { ownerPrompt, pluginIdPrompt } from './common/prompts'; type Options = { id: string; @@ -41,39 +42,7 @@ export const backendPlugin = createFactory({ optionsDiscovery: async () => ({ codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), }), - optionsPrompts: [ - { - type: 'input', - name: 'id', - message: 'Enter an ID for the plugin [required]', - validate: (value: string) => { - if (!value) { - return 'Please enter an ID for the plugin'; - } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { - return 'Plugin IDs must be lowercase and contain only letters, digits, and dashes.'; - } - return true; - }, - }, - { - type: 'input', - name: 'owner', - message: 'Enter an owner of the plugin to add to CODEOWNERS [optional]', - when: opts => Boolean(opts.codeOwnersPath), - validate: (value: string) => { - if (!value) { - return true; - } - - const ownerIds = parseOwnerIds(value); - if (!ownerIds) { - return 'The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses (e.g. user@example.com).'; - } - - return true; - }, - }, - ], + optionsPrompts: [pluginIdPrompt(), ownerPrompt()], async create(options: Options, ctx: CreateContext) { const id = `${options.id}-backend`; const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; diff --git a/packages/cli/src/lib/create/factories/common/prompts.ts b/packages/cli/src/lib/create/factories/common/prompts.ts new file mode 100644 index 0000000000..9c7672ddfb --- /dev/null +++ b/packages/cli/src/lib/create/factories/common/prompts.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 { Prompt } from '../../types'; +import { parseOwnerIds } from '../../../codeowners'; + +export function pluginIdPrompt(): Prompt<{ id: string }> { + return { + type: 'input', + name: 'id', + message: 'Enter the ID of the plugin [required]', + validate: (value: string) => { + if (!value) { + return 'Please enter the ID of the plugin'; + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return 'Plugin IDs must be lowercase and contain only letters, digits, and dashes.'; + } + return true; + }, + }; +} + +export function ownerPrompt(): Prompt<{ + owner?: string; + codeOwnersPath?: string; +}> { + return { + type: 'input', + name: 'owner', + message: 'Enter an owner to add to CODEOWNERS [optional]', + when: opts => Boolean(opts.codeOwnersPath), + validate: (value: string) => { + if (!value) { + return true; + } + + const ownerIds = parseOwnerIds(value); + if (!ownerIds) { + return 'The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses (e.g. user@example.com).'; + } + + return true; + }, + }; +} diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index a85403e8ed..3fe01b7325 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -28,6 +28,7 @@ import { createFactory, CreateContext } from '../types'; import { Lockfile } from '../../versioning'; import { addPackageDependency, Task, templatingTask } from '../../tasks'; import { createPackageVersionProvider } from '../../version'; +import { ownerPrompt, pluginIdPrompt } from './common/prompts'; type Options = { id: string; @@ -41,39 +42,7 @@ export const frontendPlugin = createFactory({ optionsDiscovery: async () => ({ codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), }), - optionsPrompts: [ - { - type: 'input', - name: 'id', - message: 'Enter an ID for the plugin [required]', - validate: (value: string) => { - if (!value) { - return 'Please enter an ID for the plugin'; - } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { - return 'Plugin IDs must be lowercase and contain only letters, digits, and dashes.'; - } - return true; - }, - }, - { - type: 'input', - name: 'owner', - message: 'Enter an owner of the plugin to add to CODEOWNERS [optional]', - when: opts => Boolean(opts.codeOwnersPath), - validate: (value: string) => { - if (!value) { - return true; - } - - const ownerIds = parseOwnerIds(value); - if (!ownerIds) { - return 'The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses (e.g. user@example.com).'; - } - - return true; - }, - }, - ], + optionsPrompts: [pluginIdPrompt(), ownerPrompt()], async create(options: Options, ctx: CreateContext) { const { id } = options; diff --git a/packages/cli/src/lib/create/types.ts b/packages/cli/src/lib/create/types.ts index 6a45067e9b..fd2e684e11 100644 --- a/packages/cli/src/lib/create/types.ts +++ b/packages/cli/src/lib/create/types.ts @@ -37,18 +37,20 @@ export interface CreateContext { export type AnyOptions = Record; -export interface Factory { +export type Prompt = DistinctQuestion & { name: string }; + +export interface Factory { name: string; description: string; - optionsDiscovery?(): Promise>; - optionsPrompts?: ReadonlyArray & { name: string }>; - create(options: Options, context?: CreateContext): Promise; + optionsDiscovery?(): Promise>; + optionsPrompts?: ReadonlyArray>; + create(options: TOptions, context?: CreateContext): Promise; } export type AnyFactory = Factory; -export function createFactory( - config: Factory, +export function createFactory( + config: Factory, ): AnyFactory { return config as AnyFactory; } From 0ebcf168d25d04a7ba39b75929fbca273ae6446d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 16:36:57 +0100 Subject: [PATCH 15/42] cli: common helper for executing plugin package templates Signed-off-by: Patrik Oldsberg --- .../src/lib/create/factories/backendPlugin.ts | 57 +++----------- .../src/lib/create/factories/common/tasks.ts | 76 +++++++++++++++++++ .../lib/create/factories/frontendPlugin.ts | 60 +++------------ 3 files changed, 96 insertions(+), 97 deletions(-) create mode 100644 packages/cli/src/lib/create/factories/common/tasks.ts diff --git a/packages/cli/src/lib/create/factories/backendPlugin.ts b/packages/cli/src/lib/create/factories/backendPlugin.ts index d7855d5d68..68c684f6ab 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.ts @@ -16,8 +16,6 @@ import fs from 'fs-extra'; import camelCase from 'lodash/camelCase'; -import upperFirst from 'lodash/upperFirst'; -import chalk from 'chalk'; import { paths } from '../../paths'; import { addCodeownersEntry, @@ -25,10 +23,9 @@ import { parseOwnerIds, } from '../../codeowners'; import { createFactory, CreateContext } from '../types'; -import { Lockfile } from '../../versioning'; -import { addPackageDependency, Task, templatingTask } from '../../tasks'; -import { createPackageVersionProvider } from '../../version'; +import { addPackageDependency, Task } from '../../tasks'; import { ownerPrompt, pluginIdPrompt } from './common/prompts'; +import { executePluginPackageTemplate } from './common/tasks'; type Options = { id: string; @@ -47,38 +44,14 @@ export const backendPlugin = createFactory({ const id = `${options.id}-backend`; const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; - const pluginDir = ctx.isMonoRepo + const targetDir = ctx.isMonoRepo ? paths.resolveTargetRoot('plugins', id) : paths.resolveTargetRoot(`backstage-plugin-${id}`); - let lockfile: Lockfile | undefined; - try { - lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); - } catch (error) { - console.warn(`No yarn.lock available, ${error}`); - } - - Task.section('Validating prerequisites'); - const shortPluginDir = pluginDir.replace(`${paths.targetRoot}/`, ''); - await Task.forItem('availability', shortPluginDir, async () => { - if (await fs.pathExists(pluginDir)) { - throw new Error( - `A backend plugin with the same ID already exists at ${chalk.cyan( - shortPluginDir, - )}. Please try again with a different ID.`, - ); - } - }); - - const tempDir = await Task.forItem('creating', 'temp dir', async () => { - return await ctx.createTemporaryDirectory(`backstage-plugin-${id}`); - }); - - Task.section('Executing plugin template'); - await templatingTask( - paths.resolveOwn('templates/default-backend-plugin'), - tempDir, - { + await executePluginPackageTemplate(ctx, { + targetDir, + templateName: 'default-backend-plugin', + values: { id, name, pluginVar: `${camelCase(id)}Plugin`, @@ -86,20 +59,8 @@ export const backendPlugin = createFactory({ privatePackage: ctx.private, npmRegistry: ctx.npmRegistry, }, - createPackageVersionProvider(lockfile), - ); - - Task.section('Installing plugin'); - await Task.forItem('moving', shortPluginDir, async () => { - await fs.move(tempDir, pluginDir).catch(error => { - throw new Error( - `Failed to move plugin from ${tempDir} to ${pluginDir}, ${error.message}`, - ); - }); }); - ctx.markAsModified(); - if (await fs.pathExists(paths.resolveTargetRoot('packages/backend'))) { await Task.forItem('backend', 'adding dependency', async () => { await addPackageDependency( @@ -124,9 +85,9 @@ export const backendPlugin = createFactory({ } } - await Task.forCommand('yarn install', { cwd: pluginDir, optional: true }); + await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); await Task.forCommand('yarn lint --fix', { - cwd: pluginDir, + cwd: targetDir, optional: true, }); }, diff --git a/packages/cli/src/lib/create/factories/common/tasks.ts b/packages/cli/src/lib/create/factories/common/tasks.ts new file mode 100644 index 0000000000..b644110a9f --- /dev/null +++ b/packages/cli/src/lib/create/factories/common/tasks.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 fs from 'fs-extra'; +import chalk from 'chalk'; +import { paths } from '../../../paths'; +import { Task, templatingTask } from '../../../tasks'; +import { Lockfile } from '../../../versioning'; +import { createPackageVersionProvider } from '../../../version'; +import { CreateContext } from '../../types'; + +export async function executePluginPackageTemplate( + ctx: CreateContext, + options: { + templateName: string; + targetDir: string; + values: Record; + }, +) { + const { targetDir } = options; + + let lockfile: Lockfile | undefined; + try { + lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + } catch { + /* ignored */ + } + + Task.section('Checking Prerequisites'); + const shortPluginDir = targetDir.replace(`${paths.targetRoot}/`, ''); + await Task.forItem('availability', shortPluginDir, async () => { + if (await fs.pathExists(targetDir)) { + throw new Error( + `A package with the same plugin ID already exists at ${chalk.cyan( + shortPluginDir, + )}. Please try again with a different ID.`, + ); + } + }); + + const tempDir = await Task.forItem('creating', 'temp dir', async () => { + return await ctx.createTemporaryDirectory('backstage-create'); + }); + + Task.section('Executing Template'); + await templatingTask( + paths.resolveOwn('templates', options.templateName), + tempDir, + options.values, + createPackageVersionProvider(lockfile), + ); + + Task.section('Installing'); + await Task.forItem('moving', shortPluginDir, async () => { + await fs.move(tempDir, targetDir).catch(error => { + throw new Error( + `Failed to move package from ${tempDir} to ${targetDir}, ${error.message}`, + ); + }); + }); + + ctx.markAsModified(); +} diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index 3fe01b7325..bb93bd311c 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -17,7 +17,6 @@ import fs from 'fs-extra'; import camelCase from 'lodash/camelCase'; import upperFirst from 'lodash/upperFirst'; -import chalk from 'chalk'; import { paths } from '../../paths'; import { addCodeownersEntry, @@ -25,10 +24,9 @@ import { parseOwnerIds, } from '../../codeowners'; import { createFactory, CreateContext } from '../types'; -import { Lockfile } from '../../versioning'; -import { addPackageDependency, Task, templatingTask } from '../../tasks'; -import { createPackageVersionProvider } from '../../version'; +import { addPackageDependency, Task } from '../../tasks'; import { ownerPrompt, pluginIdPrompt } from './common/prompts'; +import { executePluginPackageTemplate } from './common/tasks'; type Options = { id: string; @@ -49,60 +47,24 @@ export const frontendPlugin = createFactory({ const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; const extensionName = `${upperFirst(camelCase(id))}Page`; - const pluginDir = ctx.isMonoRepo + const targetDir = ctx.isMonoRepo ? paths.resolveTargetRoot('plugins', id) : paths.resolveTargetRoot(`backstage-plugin-${id}`); - let lockfile: Lockfile | undefined; - try { - lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); - } catch (error) { - console.warn(`No yarn.lock available, ${error}`); - } - - Task.section('Validating prerequisites'); - const shortPluginDir = pluginDir.replace(`${paths.targetRoot}/`, ''); - await Task.forItem('availability', shortPluginDir, async () => { - if (await fs.pathExists(pluginDir)) { - throw new Error( - `A plugin with the same ID already exists at ${chalk.cyan( - shortPluginDir, - )}. Please try again with a different ID.`, - ); - } - }); - - const tempDir = await Task.forItem('creating', 'temp dir', async () => { - return await ctx.createTemporaryDirectory(`backstage-plugin-${id}`); - }); - - Task.section('Executing plugin template'); - await templatingTask( - paths.resolveOwn('templates/default-plugin'), - tempDir, - { + await executePluginPackageTemplate(ctx, { + targetDir, + templateName: 'default-plugin', + values: { id, + name, + extensionName, pluginVar: `${camelCase(id)}Plugin`, pluginVersion: ctx.defaultVersion, - extensionName, - name, privatePackage: ctx.private, npmRegistry: ctx.npmRegistry, }, - createPackageVersionProvider(lockfile), - ); - - Task.section('Installing plugin'); - await Task.forItem('moving', shortPluginDir, async () => { - await fs.move(tempDir, pluginDir).catch(error => { - throw new Error( - `Failed to move plugin from ${tempDir} to ${pluginDir}, ${error.message}`, - ); - }); }); - ctx.markAsModified(); - if (await fs.pathExists(paths.resolveTargetRoot('packages/app'))) { await Task.forItem('app', 'adding dependency', async () => { await addPackageDependency( @@ -163,9 +125,9 @@ export const frontendPlugin = createFactory({ } } - await Task.forCommand('yarn install', { cwd: pluginDir, optional: true }); + await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); await Task.forCommand('yarn lint --fix', { - cwd: pluginDir, + cwd: targetDir, optional: true, }); }, From 367e09e31fd828561f94024bf90355576774a295 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 16:45:56 +0100 Subject: [PATCH 16/42] cli: refactor to simplify codeowners logic Signed-off-by: Patrik Oldsberg --- .../commands/create-plugin/createPlugin.ts | 9 ++---- .../remove-plugin/removePlugin.test.ts | 4 +-- packages/cli/src/lib/codeowners/codeowners.ts | 30 ++++++++++++++----- .../src/lib/create/factories/backendPlugin.ts | 17 ++--------- .../lib/create/factories/frontendPlugin.ts | 17 ++--------- 5 files changed, 33 insertions(+), 44 deletions(-) diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index a5ea565cae..c80129662c 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -263,7 +263,6 @@ export default async (cmd: Command) => { const pluginDir = isMonoRepo ? paths.resolveTargetRoot('plugins', pluginId) : paths.resolveTargetRoot(pluginId); - const ownerIds = parseOwnerIds(answers.owner); const { version: pluginVersion } = isMonoRepo ? await fs.readJson(paths.resolveTargetRoot('lerna.json')) : { version: '0.1.0' }; @@ -318,12 +317,8 @@ export default async (cmd: Command) => { await addPluginExtensionToApp(pluginId, extensionName, name); } - if (ownerIds && ownerIds.length) { - await addCodeownersEntry( - codeownersPath!, - `/plugins/${pluginId}`, - ownerIds, - ); + if (answers.owner) { + await addCodeownersEntry(`/plugins/${pluginId}`, answers.owner); } Task.log(); diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts index 0390320128..8859b5d4cd 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts @@ -177,9 +177,9 @@ describe('removePlugin', () => { fse.readFileSync(mockedCodeownersPath, 'utf8'), ); await addCodeownersEntry( - testFilePath!, path.join('plugins', testPluginName), - ['@thisIsAtestTeam', 'test@gmail.com'], + '@thisIsAtestTeam test@gmail.com', + testFilePath, ); await removePluginFromCodeOwners(testFilePath, testPluginName); expect(testFileContent).toBe(codeOwnersFileContent); diff --git a/packages/cli/src/lib/codeowners/codeowners.ts b/packages/cli/src/lib/codeowners/codeowners.ts index b3fac12109..5734be95b8 100644 --- a/packages/cli/src/lib/codeowners/codeowners.ts +++ b/packages/cli/src/lib/codeowners/codeowners.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import path from 'path'; +import { paths } from '../paths'; const TEAM_ID_RE = /^@[-\w]+\/[-\w]+$/; const USER_ID_RE = /^@[-\w]+$/; @@ -30,14 +31,14 @@ type CodeownersEntry = { export async function getCodeownersFilePath( rootDir: string, ): Promise { - const paths = [ + const possiblePaths = [ path.join(rootDir, '.github', 'CODEOWNERS'), path.join(rootDir, '.gitlab', 'CODEOWNERS'), path.join(rootDir, 'docs', 'CODEOWNERS'), path.join(rootDir, 'CODEOWNERS'), ]; - for (const p of paths) { + for (const p of possiblePaths) { if (await fs.pathExists(p)) { return p; } @@ -70,11 +71,24 @@ export function parseOwnerIds( } export async function addCodeownersEntry( - codeownersFilePath: string, ownedPath: string, - ownerIds: string[], -): Promise { - const allLines = (await fs.readFile(codeownersFilePath, 'utf8')).split('\n'); + ownerStr: string, + codeownersFilePath?: string, +): Promise { + const ownerIds = parseOwnerIds(ownerStr); + if (!ownerIds || ownerIds.length === 0) { + return false; + } + + let filePath = codeownersFilePath; + if (!filePath) { + filePath = await getCodeownersFilePath(paths.targetRoot); + if (!filePath) { + return false; + } + } + + const allLines = (await fs.readFile(filePath, 'utf8')).split('\n'); // Only keep comments from the top of the file const commentLines = []; @@ -117,5 +131,7 @@ export async function addCodeownersEntry( const newLines = [...commentLines, '', ...newDeclarationLines, '']; - await fs.writeFile(codeownersFilePath, newLines.join('\n'), 'utf8'); + await fs.writeFile(filePath, newLines.join('\n'), 'utf8'); + + return true; } diff --git a/packages/cli/src/lib/create/factories/backendPlugin.ts b/packages/cli/src/lib/create/factories/backendPlugin.ts index 68c684f6ab..2fd461c2a9 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.ts @@ -17,11 +17,7 @@ import fs from 'fs-extra'; import camelCase from 'lodash/camelCase'; import { paths } from '../../paths'; -import { - addCodeownersEntry, - getCodeownersFilePath, - parseOwnerIds, -} from '../../codeowners'; +import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; import { createFactory, CreateContext } from '../types'; import { addPackageDependency, Task } from '../../tasks'; import { ownerPrompt, pluginIdPrompt } from './common/prompts'; @@ -74,15 +70,8 @@ export const backendPlugin = createFactory({ }); } - if (options.codeOwnersPath && options.owner) { - const ownerIds = parseOwnerIds(options.owner); - if (ownerIds && ownerIds.length > 0) { - await addCodeownersEntry( - options.codeOwnersPath, - `/plugins/${id}`, - ownerIds, - ); - } + if (options.owner) { + await addCodeownersEntry(`/plugins/${id}`, options.owner); } await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index bb93bd311c..1d30d0235d 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -18,11 +18,7 @@ import fs from 'fs-extra'; import camelCase from 'lodash/camelCase'; import upperFirst from 'lodash/upperFirst'; import { paths } from '../../paths'; -import { - addCodeownersEntry, - getCodeownersFilePath, - parseOwnerIds, -} from '../../codeowners'; +import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; import { createFactory, CreateContext } from '../types'; import { addPackageDependency, Task } from '../../tasks'; import { ownerPrompt, pluginIdPrompt } from './common/prompts'; @@ -114,15 +110,8 @@ export const frontendPlugin = createFactory({ }); } - if (options.codeOwnersPath && options.owner) { - const ownerIds = parseOwnerIds(options.owner); - if (ownerIds && ownerIds.length > 0) { - await addCodeownersEntry( - options.codeOwnersPath, - `/plugins/${id}`, - ownerIds, - ); - } + if (options.owner) { + await addCodeownersEntry(`/plugins/${id}`, options.owner); } await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); From 033f6abfcebc0b7ae1c276b412fcb306efe2443d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 18:15:03 +0100 Subject: [PATCH 17/42] cli: add tests for frontendPlugin create factory + minor fixes Signed-off-by: Patrik Oldsberg --- .../create/factories/frontendPlugin.test.ts | 232 ++++++++++++++++++ .../lib/create/factories/frontendPlugin.ts | 2 +- packages/cli/src/lib/tasks.ts | 8 +- 3 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 packages/cli/src/lib/create/factories/frontendPlugin.test.ts diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts new file mode 100644 index 0000000000..fe815ce80f --- /dev/null +++ b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts @@ -0,0 +1,232 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { resolve as resolvePath } from 'path'; +import { WriteStream } from 'tty'; +import { paths } from '../../paths'; +import { Task } from '../../tasks'; +import { FactoryRegistry } from '../FactoryRegistry'; +import { frontendPlugin } from './frontendPlugin'; + +function createMockOutputStream() { + const output = new Array(); + return [ + output, + { + cursorTo: () => {}, + clearLine: () => {}, + moveCursor: () => {}, + write: (msg: string) => + // Clean up colors and whitespace + // eslint-disable-next-line no-control-regex + output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), + } as unknown as WriteStream & { fd: any }, + ] as const; +} + +const appTsxContent = ` +import { createApp } from '@backstage/app-defaults'; + +const router = ( + + } /> + +) +`; + +describe('frontendPlugin factory', () => { + beforeEach(() => { + jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...ps) => resolvePath('/root', ...ps)); + }); + + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should create a frontend plugin', async () => { + mockFs({ + '/root': { + packages: { + app: { + 'package.json': JSON.stringify({}), + src: { + 'App.tsx': appTsxContent, + }, + }, + }, + plugins: mockFs.directory(), + }, + [paths.resolveOwn('templates')]: mockFs.load( + paths.resolveOwn('templates'), + ), + }); + + const options = await FactoryRegistry.populateOptions(frontendPlugin, { + id: 'test', + }); + + let modified = false; + + const [output, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + jest.spyOn(Task, 'forCommand').mockResolvedValue(); + + await frontendPlugin.create(options, { + private: true, + isMonoRepo: true, + defaultVersion: '1.0.0', + markAsModified: () => { + modified = true; + }, + createTemporaryDirectory: () => fs.mkdtemp('test'), + }); + + expect(modified).toBe(true); + + expect(output).toEqual([ + 'Checking Prerequisites:', + 'availability plugins/test ✔', + 'creating temp dir ✔', + 'Executing Template:', + 'copying .eslintrc.js ✔', + 'templating README.md.hbs ✔', + 'templating package.json.hbs ✔', + 'copying tsconfig.json ✔', + 'templating index.tsx.hbs ✔', + 'templating index.ts.hbs ✔', + 'templating plugin.test.ts.hbs ✔', + 'templating plugin.ts.hbs ✔', + 'templating routes.ts.hbs ✔', + 'copying setupTests.ts ✔', + 'templating ExampleComponent.test.tsx.hbs ✔', + 'templating ExampleComponent.tsx.hbs ✔', + 'copying index.ts ✔', + 'templating ExampleFetchComponent.test.tsx.hbs ✔', + 'templating ExampleFetchComponent.tsx.hbs ✔', + 'copying index.ts ✔', + 'Installing:', + 'moving plugins/test ✔', + 'app adding dependency ✔', + 'app adding import ✔', + ]); + + await expect( + fs.readJson('/root/packages/app/package.json'), + ).resolves.toEqual({ + dependencies: { + 'plugin-test': '^1.0.0', + }, + }); + + await expect(fs.readFile('/root/packages/app/src/App.tsx', 'utf8')).resolves + .toBe(` +import { createApp } from '@backstage/app-defaults'; +import { TestPage } from 'plugin-test'; + +const router = ( + + } /> + } /> + +) +`); + + expect(Task.forCommand).toHaveBeenCalledTimes(2); + expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { + cwd: '/root/plugins/test', + optional: true, + }); + expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { + cwd: '/root/plugins/test', + optional: true, + }); + }); + + it('should create a frontend plugin with more options and codeowners', async () => { + mockFs({ + '/root': { + CODEOWNERS: '', + packages: { + app: { + 'package.json': JSON.stringify({}), + src: { + 'App.tsx': appTsxContent, + }, + }, + }, + plugins: mockFs.directory(), + }, + [paths.resolveOwn('templates')]: mockFs.load( + paths.resolveOwn('templates'), + ), + }); + + const options = await FactoryRegistry.populateOptions(frontendPlugin, { + id: 'test', + owner: '@test-user', + }); + + const [, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + jest.spyOn(Task, 'forCommand').mockResolvedValue(); + + await frontendPlugin.create(options, { + scope: 'internal', + private: true, + isMonoRepo: true, + defaultVersion: '1.0.0', + markAsModified: () => {}, + createTemporaryDirectory: () => fs.mkdtemp('test'), + }); + + await expect( + fs.readJson('/root/packages/app/package.json'), + ).resolves.toEqual({ + dependencies: { + '@internal/plugin-test': '^1.0.0', + }, + }); + + await expect(fs.readFile('/root/packages/app/src/App.tsx', 'utf8')).resolves + .toBe(` +import { createApp } from '@backstage/app-defaults'; +import { TestPage } from '@internal/plugin-test'; + +const router = ( + + } /> + } /> + +) +`); + + expect(Task.forCommand).toHaveBeenCalledTimes(2); + expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { + cwd: '/root/plugins/test', + optional: true, + }); + expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { + cwd: '/root/plugins/test', + optional: true, + }); + }); +}); diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index 1d30d0235d..50e8dd42a3 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -97,7 +97,7 @@ export const frontendPlugin = createFactory({ revLines.splice(lastImportIndex, 0, importLine); } - const componentLine = `}/>`; + const componentLine = `} />`; if (!content.includes(componentLine)) { const [indentation] = revLines[lastRouteIndex + 1].match(/^\s*/) ?? []; diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index 9e67f7f30c..c84eed435e 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -31,16 +31,16 @@ const TASK_NAME_MAX_LENGTH = 14; export class Task { static log(name: string = '') { - process.stdout.write(`${chalk.green(name)}\n`); + process.stderr.write(`${chalk.green(name)}\n`); } static error(message: string = '') { - process.stdout.write(`\n${chalk.red(message)}\n\n`); + process.stderr.write(`\n${chalk.red(message)}\n\n`); } static section(name: string) { const title = chalk.green(`${name}:`); - process.stdout.write(`\n ${title}\n`); + process.stderr.write(`\n ${title}\n`); } static exit(code: number = 0) { @@ -81,7 +81,7 @@ export class Task { } catch (error) { assertError(error); if (error.stderr) { - process.stdout.write(error.stderr as Buffer); + process.stderr.write(error.stderr as Buffer); } if (error.stdout) { process.stdout.write(error.stdout as Buffer); From 1cf00cbe334488b62f4c36f1f59170bea8ab0382 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 18:19:33 +0100 Subject: [PATCH 18/42] cli: added test for backendPlugin create factory Signed-off-by: Patrik Oldsberg --- .../create/factories/backendPlugin.test.ts | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 packages/cli/src/lib/create/factories/backendPlugin.test.ts diff --git a/packages/cli/src/lib/create/factories/backendPlugin.test.ts b/packages/cli/src/lib/create/factories/backendPlugin.test.ts new file mode 100644 index 0000000000..1100b3001e --- /dev/null +++ b/packages/cli/src/lib/create/factories/backendPlugin.test.ts @@ -0,0 +1,130 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { resolve as resolvePath } from 'path'; +import { WriteStream } from 'tty'; +import { paths } from '../../paths'; +import { Task } from '../../tasks'; +import { FactoryRegistry } from '../FactoryRegistry'; +import { backendPlugin } from './backendPlugin'; + +function createMockOutputStream() { + const output = new Array(); + return [ + output, + { + cursorTo: () => {}, + clearLine: () => {}, + moveCursor: () => {}, + write: (msg: string) => + // Clean up colors and whitespace + // eslint-disable-next-line no-control-regex + output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), + } as unknown as WriteStream & { fd: any }, + ] as const; +} + +describe('backendPlugin factory', () => { + beforeEach(() => { + jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...ps) => resolvePath('/root', ...ps)); + }); + + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should create a backend plugin', async () => { + mockFs({ + '/root': { + packages: { + backend: { + 'package.json': JSON.stringify({}), + }, + }, + plugins: mockFs.directory(), + }, + [paths.resolveOwn('templates')]: mockFs.load( + paths.resolveOwn('templates'), + ), + }); + + const options = await FactoryRegistry.populateOptions(backendPlugin, { + id: 'test', + }); + + let modified = false; + + const [output, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + jest.spyOn(Task, 'forCommand').mockResolvedValue(); + + await backendPlugin.create(options, { + private: true, + isMonoRepo: true, + defaultVersion: '1.0.0', + markAsModified: () => { + modified = true; + }, + createTemporaryDirectory: () => fs.mkdtemp('test'), + }); + + expect(modified).toBe(true); + + expect(output).toEqual([ + 'Checking Prerequisites:', + 'availability plugins/test-backend ✔', + 'creating temp dir ✔', + 'Executing Template:', + 'copying .eslintrc.js ✔', + 'templating README.md.hbs ✔', + 'templating package.json.hbs ✔', + 'copying tsconfig.json ✔', + 'copying index.ts ✔', + 'templating run.ts.hbs ✔', + 'copying setupTests.ts ✔', + 'copying router.test.ts ✔', + 'copying router.ts ✔', + 'templating standaloneServer.ts.hbs ✔', + 'Installing:', + 'moving plugins/test-backend ✔', + 'backend adding dependency ✔', + ]); + + await expect( + fs.readJson('/root/packages/backend/package.json'), + ).resolves.toEqual({ + dependencies: { + 'plugin-test-backend': '^1.0.0', + }, + }); + + expect(Task.forCommand).toHaveBeenCalledTimes(2); + expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { + cwd: '/root/plugins/test-backend', + optional: true, + }); + expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { + cwd: '/root/plugins/test-backend', + optional: true, + }); + }); +}); From 8f2a7184386e1fdbb43b3d35d1bf90bbf681fdf4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 18:38:47 +0100 Subject: [PATCH 19/42] cli: added create factory and template for plugin common package Signed-off-by: Patrik Oldsberg --- .../cli/src/lib/create/factories/index.ts | 1 + .../lib/create/factories/pluginCommon.test.ts | 123 ++++++++++++++++++ .../src/lib/create/factories/pluginCommon.ts | 67 ++++++++++ .../.eslintrc.js | 3 + .../README.md.hbs | 5 + .../package.json.hbs | 32 +++++ .../src/index.ts | 16 +++ .../src/setupTests.ts | 1 + .../tsconfig.json | 9 ++ 9 files changed, 257 insertions(+) create mode 100644 packages/cli/src/lib/create/factories/pluginCommon.test.ts create mode 100644 packages/cli/src/lib/create/factories/pluginCommon.ts create mode 100644 packages/cli/templates/default-common-plugin-package/.eslintrc.js create mode 100644 packages/cli/templates/default-common-plugin-package/README.md.hbs create mode 100644 packages/cli/templates/default-common-plugin-package/package.json.hbs create mode 100644 packages/cli/templates/default-common-plugin-package/src/index.ts create mode 100644 packages/cli/templates/default-common-plugin-package/src/setupTests.ts create mode 100644 packages/cli/templates/default-common-plugin-package/tsconfig.json diff --git a/packages/cli/src/lib/create/factories/index.ts b/packages/cli/src/lib/create/factories/index.ts index 2e16979e50..e4df9062f7 100644 --- a/packages/cli/src/lib/create/factories/index.ts +++ b/packages/cli/src/lib/create/factories/index.ts @@ -16,3 +16,4 @@ export { frontendPlugin } from './frontendPlugin'; export { backendPlugin } from './backendPlugin'; +export { pluginCommon } from './pluginCommon'; diff --git a/packages/cli/src/lib/create/factories/pluginCommon.test.ts b/packages/cli/src/lib/create/factories/pluginCommon.test.ts new file mode 100644 index 0000000000..f7251dc443 --- /dev/null +++ b/packages/cli/src/lib/create/factories/pluginCommon.test.ts @@ -0,0 +1,123 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { resolve as resolvePath } from 'path'; +import { WriteStream } from 'tty'; +import { paths } from '../../paths'; +import { Task } from '../../tasks'; +import { FactoryRegistry } from '../FactoryRegistry'; +import { pluginCommon } from './pluginCommon'; + +function createMockOutputStream() { + const output = new Array(); + return [ + output, + { + cursorTo: () => {}, + clearLine: () => {}, + moveCursor: () => {}, + write: (msg: string) => + // Clean up colors and whitespace + // eslint-disable-next-line no-control-regex + output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), + } as unknown as WriteStream & { fd: any }, + ] as const; +} + +describe('pluginCommon factory', () => { + beforeEach(() => { + jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...ps) => resolvePath('/root', ...ps)); + }); + + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should create a common plugin package', async () => { + mockFs({ + '/root': { + plugins: mockFs.directory(), + }, + [paths.resolveOwn('templates')]: mockFs.load( + paths.resolveOwn('templates'), + ), + }); + + const options = await FactoryRegistry.populateOptions(pluginCommon, { + id: 'test', + }); + + let modified = false; + + const [output, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + jest.spyOn(Task, 'forCommand').mockResolvedValue(); + + await pluginCommon.create(options, { + private: true, + isMonoRepo: true, + defaultVersion: '1.0.0', + markAsModified: () => { + modified = true; + }, + createTemporaryDirectory: () => fs.mkdtemp('test'), + }); + + expect(modified).toBe(true); + + expect(output).toEqual([ + 'Checking Prerequisites:', + 'availability plugins/test-common ✔', + 'creating temp dir ✔', + 'Executing Template:', + 'copying .eslintrc.js ✔', + 'templating README.md.hbs ✔', + 'templating package.json.hbs ✔', + 'copying tsconfig.json ✔', + 'copying index.ts ✔', + 'copying setupTests.ts ✔', + 'Installing:', + 'moving plugins/test-common ✔', + ]); + + await expect( + fs.readJson('/root/plugins/test-common/package.json'), + ).resolves.toEqual( + expect.objectContaining({ + name: 'plugin-test-common', + description: 'Common functionalities for the test-common plugin', + private: true, + version: '1.0.0', + }), + ); + + expect(Task.forCommand).toHaveBeenCalledTimes(2); + expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { + cwd: '/root/plugins/test-common', + optional: true, + }); + expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { + cwd: '/root/plugins/test-common', + optional: true, + }); + }); +}); diff --git a/packages/cli/src/lib/create/factories/pluginCommon.ts b/packages/cli/src/lib/create/factories/pluginCommon.ts new file mode 100644 index 0000000000..5a6dd30046 --- /dev/null +++ b/packages/cli/src/lib/create/factories/pluginCommon.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 { paths } from '../../paths'; +import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; +import { createFactory, CreateContext } from '../types'; +import { Task } from '../../tasks'; +import { ownerPrompt, pluginIdPrompt } from './common/prompts'; +import { executePluginPackageTemplate } from './common/tasks'; + +type Options = { + id: string; + owner?: string; + codeOwnersPath?: string; +}; + +export const pluginCommon = createFactory({ + name: 'plugin-common', + description: 'A new isomorphic common plugin package', + optionsDiscovery: async () => ({ + codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), + }), + optionsPrompts: [pluginIdPrompt(), ownerPrompt()], + async create(options: Options, ctx: CreateContext) { + const id = `${options.id}-common`; + const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + + const targetDir = ctx.isMonoRepo + ? paths.resolveTargetRoot('plugins', id) + : paths.resolveTargetRoot(`backstage-plugin-${id}`); + + await executePluginPackageTemplate(ctx, { + targetDir, + templateName: 'default-common-plugin-package', + values: { + id, + name, + privatePackage: ctx.private, + npmRegistry: ctx.npmRegistry, + pluginVersion: ctx.defaultVersion, + }, + }); + + if (options.owner) { + await addCodeownersEntry(`/plugins/${id}`, options.owner); + } + + await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); + await Task.forCommand('yarn lint --fix', { + cwd: targetDir, + optional: true, + }); + }, +}); diff --git a/packages/cli/templates/default-common-plugin-package/.eslintrc.js b/packages/cli/templates/default-common-plugin-package/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/packages/cli/templates/default-common-plugin-package/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/packages/cli/templates/default-common-plugin-package/README.md.hbs b/packages/cli/templates/default-common-plugin-package/README.md.hbs new file mode 100644 index 0000000000..917e18d4b9 --- /dev/null +++ b/packages/cli/templates/default-common-plugin-package/README.md.hbs @@ -0,0 +1,5 @@ +# {{name}} + +Welcome to the common package for the {{id}} plugin! + +_This plugin was created through the Backstage CLI_ diff --git a/packages/cli/templates/default-common-plugin-package/package.json.hbs b/packages/cli/templates/default-common-plugin-package/package.json.hbs new file mode 100644 index 0000000000..c7ba25ac49 --- /dev/null +++ b/packages/cli/templates/default-common-plugin-package/package.json.hbs @@ -0,0 +1,32 @@ +{ + "name": "{{name}}", + "description": "Common functionalities for the {{id}} plugin", + "version": "{{pluginVersion}}", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", +{{#if privatePackage}} "private": {{privatePackage}}, +{{/if}} + "publishConfig": { +{{#if npmRegistry}} "registry": "{{npmRegistry}}", +{{/if}} + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "devDependencies": { + "@backstage/cli": "{{versionQuery '@backstage/cli'}}" + }, + "files": [ + "dist" + ] +} diff --git a/packages/cli/templates/default-common-plugin-package/src/index.ts b/packages/cli/templates/default-common-plugin-package/src/index.ts new file mode 100644 index 0000000000..6ee452b5bb --- /dev/null +++ b/packages/cli/templates/default-common-plugin-package/src/index.ts @@ -0,0 +1,16 @@ +/** + * Common functionalities for the {{id}} plugin. + */ + +/** + * In this package you might for example declare types that are common + * between the frontend and backend plugin packages. + */ +export type CommonType = { + field: string +} + +/** + * Or you might declare some common constants. + */ +export const COMMON_CONSTANT = 1 diff --git a/packages/cli/templates/default-common-plugin-package/src/setupTests.ts b/packages/cli/templates/default-common-plugin-package/src/setupTests.ts new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/packages/cli/templates/default-common-plugin-package/src/setupTests.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/cli/templates/default-common-plugin-package/tsconfig.json b/packages/cli/templates/default-common-plugin-package/tsconfig.json new file mode 100644 index 0000000000..5ae9aeb62d --- /dev/null +++ b/packages/cli/templates/default-common-plugin-package/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "dist-types", + "rootDir": "." + } +} From 827fb840b46c59b4322d11e5400a2e6364aadcb8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 18:45:51 +0100 Subject: [PATCH 20/42] cli: fix for create crashing if no scope is specified Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index 946918d77c..5fff07b194 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -86,7 +86,7 @@ export default async (cmd: Command) => { await factory.create(options, { isMonoRepo, defaultVersion, - scope: cmdOpts.scope.replace(/^@/, ''), + scope: cmdOpts.scope?.replace(/^@/, ''), npmRegistry: cmdOpts.npmRegistry, private: Boolean(cmdOpts.private), createTemporaryDirectory, From a9f8363b5d66bc55830ad9f90e1367c74efb5f15 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 18:46:25 +0100 Subject: [PATCH 21/42] cli: format package.json after creation to simplify template Signed-off-by: Patrik Oldsberg --- .../src/lib/create/factories/common/tasks.ts | 8 ++ .../default-backend-plugin/package.json.hbs | 76 ++++++++++--------- .../package.json.hbs | 6 +- .../templates/default-plugin/package.json.hbs | 6 +- 4 files changed, 55 insertions(+), 41 deletions(-) diff --git a/packages/cli/src/lib/create/factories/common/tasks.ts b/packages/cli/src/lib/create/factories/common/tasks.ts index b644110a9f..6fe2e77fc7 100644 --- a/packages/cli/src/lib/create/factories/common/tasks.ts +++ b/packages/cli/src/lib/create/factories/common/tasks.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import chalk from 'chalk'; +import { resolve as resolvePath } from 'path'; import { paths } from '../../../paths'; import { Task, templatingTask } from '../../../tasks'; import { Lockfile } from '../../../versioning'; @@ -63,6 +64,13 @@ export async function executePluginPackageTemplate( createPackageVersionProvider(lockfile), ); + // Format package.json if it exists + const targetPkgJsonPath = resolvePath(targetDir, 'package.json'); + if (await fs.pathExists(targetPkgJsonPath)) { + const pkgJson = await fs.readJson(targetPkgJsonPath); + await fs.writeJson(targetPkgJsonPath, pkgJson, { spaces: 2 }); + } + Task.section('Installing'); await Task.forItem('moving', shortPluginDir, async () => { await fs.move(tempDir, targetDir).catch(error => { diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 5cca7dcb07..37bcd94186 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -4,41 +4,43 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - {{#if privatePackage}} "private": {{privatePackage}}, - {{/if}} +{{#if privatePackage}} + "private": {{privatePackage}}, +{{/if}} "publishConfig": { - {{#if npmRegistry}} "registry": "{{npmRegistry}}", - {{/if}} - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" - }, - "scripts": { - "start": "backstage-cli backend:dev", - "build": "backstage-cli backend:build", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" - }, - "dependencies": { - "@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}", - "@backstage/config": "{{versionQuery '@backstage/config'}}", - "@types/express": "{{versionQuery '@types/express' '4.17.6'}}", - "express": "{{versionQuery 'express' '4.17.1'}}", - "express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}", - "winston": "{{versionQuery 'winston' '3.2.1'}}", - "cross-fetch": "{{versionQuery 'cross-fetch' '3.0.6'}}", - "yn": "{{versionQuery 'yn' '4.0.0'}}" - }, - "devDependencies": { - "@backstage/cli": "{{versionQuery '@backstage/cli'}}", - "@types/supertest": "{{versionQuery '@types/supertest' '2.0.8'}}", - "supertest": "{{versionQuery 'supertest' '4.0.2'}}", - "msw": "{{versionQuery 'msw' '0.35.0'}}" - }, - "files": [ - "dist" - ] - } +{{#if npmRegistry}} + "registry": "{{npmRegistry}}", +{{/if}} + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}", + "@backstage/config": "{{versionQuery '@backstage/config'}}", + "@types/express": "{{versionQuery '@types/express' '4.17.6'}}", + "express": "{{versionQuery 'express' '4.17.1'}}", + "express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}", + "winston": "{{versionQuery 'winston' '3.2.1'}}", + "cross-fetch": "{{versionQuery 'cross-fetch' '3.0.6'}}", + "yn": "{{versionQuery 'yn' '4.0.0'}}" + }, + "devDependencies": { + "@backstage/cli": "{{versionQuery '@backstage/cli'}}", + "@types/supertest": "{{versionQuery '@types/supertest' '2.0.8'}}", + "supertest": "{{versionQuery 'supertest' '4.0.2'}}", + "msw": "{{versionQuery 'msw' '0.35.0'}}" + }, + "files": [ + "dist" + ] +} diff --git a/packages/cli/templates/default-common-plugin-package/package.json.hbs b/packages/cli/templates/default-common-plugin-package/package.json.hbs index c7ba25ac49..efaee496e7 100644 --- a/packages/cli/templates/default-common-plugin-package/package.json.hbs +++ b/packages/cli/templates/default-common-plugin-package/package.json.hbs @@ -5,10 +5,12 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", -{{#if privatePackage}} "private": {{privatePackage}}, +{{#if privatePackage}} + "private": {{privatePackage}}, {{/if}} "publishConfig": { -{{#if npmRegistry}} "registry": "{{npmRegistry}}", +{{#if npmRegistry}} + "registry": "{{npmRegistry}}", {{/if}} "access": "public", "main": "dist/index.cjs.js", diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 41376821ba..624302da93 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -4,10 +4,12 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", -{{#if privatePackage}} "private": {{privatePackage}}, +{{#if privatePackage}} + "private": {{privatePackage}}, {{/if}} "publishConfig": { -{{#if npmRegistry}} "registry": "{{npmRegistry}}", +{{#if npmRegistry}} + "registry": "{{npmRegistry}}", {{/if}} "access": "public", "main": "dist/index.esm.js", From 1c291cb66f2d61467b7a0eeb1c967dc5675456ac Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 10:32:25 +0100 Subject: [PATCH 22/42] package.json: add backstage-create script Signed-off-by: Patrik Oldsberg --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e6d376c46..a7c5e2fbf7 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "lint:all": "lerna run lint --", "lint:type-deps": "node scripts/check-type-dependencies.js", "docker-build": "yarn tsc && yarn workspace example-backend build --build-dependencies && yarn workspace example-backend build-image", - "create-plugin": "backstage-cli create-plugin --scope backstage --no-private", + "backstage-create": "backstage-cli create --scope backstage --no-private", + "create-plugin": "yarn backstage-create --select plugin", "remove-plugin": "backstage-cli remove-plugin", "release": "changeset version && yarn diff --yes && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' && yarn install", "prettier:check": "prettier --check .", From 8c83ce8084df178556d60752bf3f0f0c50db4473 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 10:38:49 +0100 Subject: [PATCH 23/42] cli: leave initial create logging to the factories Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 3 --- packages/cli/src/lib/create/factories/backendPlugin.test.ts | 2 ++ packages/cli/src/lib/create/factories/backendPlugin.ts | 4 ++++ packages/cli/src/lib/create/factories/frontendPlugin.test.ts | 2 ++ packages/cli/src/lib/create/factories/frontendPlugin.ts | 4 ++++ packages/cli/src/lib/create/factories/pluginCommon.test.ts | 2 ++ packages/cli/src/lib/create/factories/pluginCommon.ts | 4 ++++ 7 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index 5fff07b194..d80daa6ddf 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -80,9 +80,6 @@ export default async (cmd: Command) => { let modified = false; try { - Task.log(); - Task.log(`Creating new ${factory.name}`); - await factory.create(options, { isMonoRepo, defaultVersion, diff --git a/packages/cli/src/lib/create/factories/backendPlugin.test.ts b/packages/cli/src/lib/create/factories/backendPlugin.test.ts index 1100b3001e..cf597572ad 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.test.ts @@ -90,6 +90,8 @@ describe('backendPlugin factory', () => { expect(modified).toBe(true); expect(output).toEqual([ + '', + 'Creating backend plugin plugin-test-backend', 'Checking Prerequisites:', 'availability plugins/test-backend ✔', 'creating temp dir ✔', diff --git a/packages/cli/src/lib/create/factories/backendPlugin.ts b/packages/cli/src/lib/create/factories/backendPlugin.ts index 2fd461c2a9..2bdc62741d 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.ts @@ -15,6 +15,7 @@ */ import fs from 'fs-extra'; +import chalk from 'chalk'; import camelCase from 'lodash/camelCase'; import { paths } from '../../paths'; import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; @@ -40,6 +41,9 @@ export const backendPlugin = createFactory({ const id = `${options.id}-backend`; const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + Task.log(); + Task.log(`Creating backend plugin ${chalk.cyan(name)}`); + const targetDir = ctx.isMonoRepo ? paths.resolveTargetRoot('plugins', id) : paths.resolveTargetRoot(`backstage-plugin-${id}`); diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts index fe815ce80f..3b24f95e41 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts @@ -103,6 +103,8 @@ describe('frontendPlugin factory', () => { expect(modified).toBe(true); expect(output).toEqual([ + '', + 'Creating backend plugin plugin-test', 'Checking Prerequisites:', 'availability plugins/test ✔', 'creating temp dir ✔', diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index 50e8dd42a3..a6c0d0470d 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -15,6 +15,7 @@ */ import fs from 'fs-extra'; +import chalk from 'chalk'; import camelCase from 'lodash/camelCase'; import upperFirst from 'lodash/upperFirst'; import { paths } from '../../paths'; @@ -43,6 +44,9 @@ export const frontendPlugin = createFactory({ const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; const extensionName = `${upperFirst(camelCase(id))}Page`; + Task.log(); + Task.log(`Creating backend plugin ${chalk.cyan(name)}`); + const targetDir = ctx.isMonoRepo ? paths.resolveTargetRoot('plugins', id) : paths.resolveTargetRoot(`backstage-plugin-${id}`); diff --git a/packages/cli/src/lib/create/factories/pluginCommon.test.ts b/packages/cli/src/lib/create/factories/pluginCommon.test.ts index f7251dc443..6e797f4cca 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.test.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.test.ts @@ -85,6 +85,8 @@ describe('pluginCommon factory', () => { expect(modified).toBe(true); expect(output).toEqual([ + '', + 'Creating backend plugin plugin-test-common', 'Checking Prerequisites:', 'availability plugins/test-common ✔', 'creating temp dir ✔', diff --git a/packages/cli/src/lib/create/factories/pluginCommon.ts b/packages/cli/src/lib/create/factories/pluginCommon.ts index 5a6dd30046..97b1eccec7 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import chalk from 'chalk'; import { paths } from '../../paths'; import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; import { createFactory, CreateContext } from '../types'; @@ -38,6 +39,9 @@ export const pluginCommon = createFactory({ const id = `${options.id}-common`; const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + Task.log(); + Task.log(`Creating backend plugin ${chalk.cyan(name)}`); + const targetDir = ctx.isMonoRepo ? paths.resolveTargetRoot('plugins', id) : paths.resolveTargetRoot(`backstage-plugin-${id}`); From 7b8f19492edfea021cc4681c392e3e43f79d5b74 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 12:30:04 +0100 Subject: [PATCH 24/42] cli: add test for plugin package template execution + fix Signed-off-by: Patrik Oldsberg --- .../lib/create/factories/common/tasks.test.ts | 117 ++++++++++++++++++ .../src/lib/create/factories/common/tasks.ts | 8 +- .../lib/create/factories/common/testUtils.ts | 68 ++++++++++ 3 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 packages/cli/src/lib/create/factories/common/tasks.test.ts create mode 100644 packages/cli/src/lib/create/factories/common/testUtils.ts diff --git a/packages/cli/src/lib/create/factories/common/tasks.test.ts b/packages/cli/src/lib/create/factories/common/tasks.test.ts new file mode 100644 index 0000000000..b3a751ba5b --- /dev/null +++ b/packages/cli/src/lib/create/factories/common/tasks.test.ts @@ -0,0 +1,117 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { createMockOutputStream, mockPaths } from './testUtils'; +import { CreateContext } from '../../types'; +import { executePluginPackageTemplate } from './tasks'; + +mockPaths({ + ownDir: '/own', + targetRoot: '/root', +}); + +describe('executePluginPackageTemplate', () => { + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should execute template', async () => { + mockFs({ + '/root': { + 'yarn.lock': ` +some-package@^1.1.0: + version "1.5.0" +`, + }, + '/own': { + templates: { + 'test-template': { + 'package.json.hbs': ` +{ + "name": "my-{{id}}-plugin", + {{#if makePrivate}} + "private": true, + {{/if}} + "description": "testing", + "dependencies": { + "some-package": "{{ versionQuery 'some-package' '1.3.0' }}", + "other-package": "{{ versionQuery 'other-package' '2.3.0' }}" + } +} +`, + subdir: { + 'templated.txt.hbs': 'Hello {{id}}!', + 'not-templated.txt': 'Hello {{id}}!', + }, + }, + }, + }, + }); + + const [output, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + + let modified = false; + await executePluginPackageTemplate( + { + createTemporaryDirectory: (name: string) => fs.mkdtemp(name), + markAsModified: () => { + modified = true; + }, + } as CreateContext, + { + templateName: 'test-template', + targetDir: '/target', + values: { + id: 'testing', + makePrivate: true, + }, + }, + ); + + expect(modified).toBe(true); + expect(output).toEqual([ + 'Checking Prerequisites:', + 'availability /target ✔', + 'creating temp dir ✔', + 'Executing Template:', + 'templating package.json.hbs ✔', + 'copying not-templated.txt ✔', + 'templating templated.txt.hbs ✔', + 'Installing:', + 'moving /target ✔', + ]); + await expect(fs.readFile('/target/package.json', 'utf8')).resolves.toBe(`{ + "name": "my-testing-plugin", + "private": true, + "description": "testing", + "dependencies": { + "some-package": "^1.1.0", + "other-package": "^2.3.0" + } +} +`); + await expect( + fs.readFile('/target/subdir/templated.txt', 'utf8'), + ).resolves.toBe('Hello testing!'); + await expect( + fs.readFile('/target/subdir/not-templated.txt', 'utf8'), + ).resolves.toBe('Hello {{id}}!'); + }); +}); diff --git a/packages/cli/src/lib/create/factories/common/tasks.ts b/packages/cli/src/lib/create/factories/common/tasks.ts index 6fe2e77fc7..fd3bd4d930 100644 --- a/packages/cli/src/lib/create/factories/common/tasks.ts +++ b/packages/cli/src/lib/create/factories/common/tasks.ts @@ -65,10 +65,10 @@ export async function executePluginPackageTemplate( ); // Format package.json if it exists - const targetPkgJsonPath = resolvePath(targetDir, 'package.json'); - if (await fs.pathExists(targetPkgJsonPath)) { - const pkgJson = await fs.readJson(targetPkgJsonPath); - await fs.writeJson(targetPkgJsonPath, pkgJson, { spaces: 2 }); + const pkgJsonPath = resolvePath(tempDir, 'package.json'); + if (await fs.pathExists(pkgJsonPath)) { + const pkgJson = await fs.readJson(pkgJsonPath); + await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 }); } Task.section('Installing'); diff --git a/packages/cli/src/lib/create/factories/common/testUtils.ts b/packages/cli/src/lib/create/factories/common/testUtils.ts new file mode 100644 index 0000000000..651409451e --- /dev/null +++ b/packages/cli/src/lib/create/factories/common/testUtils.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 { WriteStream } from 'tty'; +import { resolve as resolvePath } from 'path'; +import { paths } from '../../../paths'; + +export function mockPaths(options: { + ownDir?: string; + ownRoot?: string; + targetDir?: string; + targetRoot?: string; +}): void { + const { ownDir, ownRoot, targetDir, targetRoot } = options; + if (ownDir) { + paths.ownDir = ownDir; + jest + .spyOn(paths, 'resolveOwn') + .mockImplementation((...ps) => resolvePath(ownDir, ...ps)); + } + if (ownRoot) { + jest.spyOn(paths, 'ownRoot', 'get').mockReturnValue(ownRoot); + jest + .spyOn(paths, 'resolveOwnRoot') + .mockImplementation((...ps) => resolvePath(ownRoot, ...ps)); + } + if (targetDir) { + paths.targetDir = targetDir; + jest + .spyOn(paths, 'resolveTarget') + .mockImplementation((...ps) => resolvePath(targetDir, ...ps)); + } + if (targetRoot) { + jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue(targetRoot); + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...ps) => resolvePath(targetRoot, ...ps)); + } +} + +export function createMockOutputStream() { + const output = new Array(); + return [ + output, + { + cursorTo: () => {}, + clearLine: () => {}, + moveCursor: () => {}, + write: (msg: string) => + // Clean up colors and whitespace + // eslint-disable-next-line no-control-regex + output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), + } as unknown as WriteStream & { fd: any }, + ] as const; +} From 823a097b3b5a87205eefc8c086cf13e0ae0aa8c6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 12:36:47 +0100 Subject: [PATCH 25/42] cli: use common testing utils for create factory tests Signed-off-by: Patrik Oldsberg --- .../create/factories/backendPlugin.test.ts | 26 +++------------- .../create/factories/frontendPlugin.test.ts | 26 +++------------- .../lib/create/factories/pluginCommon.test.ts | 31 +++---------------- 3 files changed, 12 insertions(+), 71 deletions(-) diff --git a/packages/cli/src/lib/create/factories/backendPlugin.test.ts b/packages/cli/src/lib/create/factories/backendPlugin.test.ts index cf597572ad..409b7f476a 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.test.ts @@ -16,35 +16,17 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; -import { resolve as resolvePath } from 'path'; -import { WriteStream } from 'tty'; import { paths } from '../../paths'; import { Task } from '../../tasks'; import { FactoryRegistry } from '../FactoryRegistry'; +import { createMockOutputStream, mockPaths } from './common/testUtils'; import { backendPlugin } from './backendPlugin'; -function createMockOutputStream() { - const output = new Array(); - return [ - output, - { - cursorTo: () => {}, - clearLine: () => {}, - moveCursor: () => {}, - write: (msg: string) => - // Clean up colors and whitespace - // eslint-disable-next-line no-control-regex - output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), - } as unknown as WriteStream & { fd: any }, - ] as const; -} - describe('backendPlugin factory', () => { beforeEach(() => { - jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); - jest - .spyOn(paths, 'resolveTargetRoot') - .mockImplementation((...ps) => resolvePath('/root', ...ps)); + mockPaths({ + targetRoot: '/root', + }); }); afterEach(() => { diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts index 3b24f95e41..3e0f94f6ad 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts @@ -16,29 +16,12 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; -import { resolve as resolvePath } from 'path'; -import { WriteStream } from 'tty'; import { paths } from '../../paths'; import { Task } from '../../tasks'; import { FactoryRegistry } from '../FactoryRegistry'; +import { createMockOutputStream, mockPaths } from './common/testUtils'; import { frontendPlugin } from './frontendPlugin'; -function createMockOutputStream() { - const output = new Array(); - return [ - output, - { - cursorTo: () => {}, - clearLine: () => {}, - moveCursor: () => {}, - write: (msg: string) => - // Clean up colors and whitespace - // eslint-disable-next-line no-control-regex - output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), - } as unknown as WriteStream & { fd: any }, - ] as const; -} - const appTsxContent = ` import { createApp } from '@backstage/app-defaults'; @@ -51,10 +34,9 @@ const router = ( describe('frontendPlugin factory', () => { beforeEach(() => { - jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); - jest - .spyOn(paths, 'resolveTargetRoot') - .mockImplementation((...ps) => resolvePath('/root', ...ps)); + mockPaths({ + targetRoot: '/root', + }); }); afterEach(() => { diff --git a/packages/cli/src/lib/create/factories/pluginCommon.test.ts b/packages/cli/src/lib/create/factories/pluginCommon.test.ts index 6e797f4cca..73edc5159b 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.test.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.test.ts @@ -16,40 +16,17 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; -import { resolve as resolvePath } from 'path'; -import { WriteStream } from 'tty'; import { paths } from '../../paths'; import { Task } from '../../tasks'; import { FactoryRegistry } from '../FactoryRegistry'; +import { createMockOutputStream, mockPaths } from './common/testUtils'; import { pluginCommon } from './pluginCommon'; -function createMockOutputStream() { - const output = new Array(); - return [ - output, - { - cursorTo: () => {}, - clearLine: () => {}, - moveCursor: () => {}, - write: (msg: string) => - // Clean up colors and whitespace - // eslint-disable-next-line no-control-regex - output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), - } as unknown as WriteStream & { fd: any }, - ] as const; -} - describe('pluginCommon factory', () => { beforeEach(() => { - jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); - jest - .spyOn(paths, 'resolveTargetRoot') - .mockImplementation((...ps) => resolvePath('/root', ...ps)); - }); - - afterEach(() => { - mockFs.restore(); - jest.resetAllMocks(); + mockPaths({ + targetRoot: '/root', + }); }); it('should create a common plugin package', async () => { From 9bbc2da04ee00711983284cf4955c05769623ca4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 13:18:07 +0100 Subject: [PATCH 26/42] docs/local-dev: add cli create command docs Signed-off-by: Patrik Oldsberg --- docs/local-dev/cli-commands.md | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/local-dev/cli-commands.md b/docs/local-dev/cli-commands.md index 0b14a2e136..18fdef892a 100644 --- a/docs/local-dev/cli-commands.md +++ b/docs/local-dev/cli-commands.md @@ -41,6 +41,7 @@ lint Lint a package test Run tests, forwarding args to Jest, defaulting to watch mode clean Delete cache directories +create Open up an interactive guide to creating new things in your app create-plugin Creates a new plugin in the current repository remove-plugin Removes plugin in the current repository @@ -277,6 +278,44 @@ Options: -h, --help display help for command ``` +## create + +Scope: `root` + +The `create` command opens up an interactive guide for you to create new things +in your app. If you do not pass in any options it is completely interactive, but +it is possible to pre-select what you want to create using the `--select` flag, +and provide options using `--options`, for example: + +```bash +backstage-cli create --select plugin --option id=foo +``` + +This command is typically added as script in the root `package.json` to be +executed with `yarn backstage-create`, using options that are appropriate for +the organization that owns the app repo. For example you may have it set up like +this: + +```json +{ + "scripts": { + "backstage-create": "backstage-cli create --scope internal --no-private --npm-registry https://acme.org/npm" + } +} +``` + +```text +Usage: backstage-cli create [options] + +Options: + --select Select the thing you want to be creating upfront + --option = Pre-fill options for the creation process (default: []) + --scope The scope to use for new packages + --npm-registry The package registry to use for new packages + --no-private Do not mark new packages as private + -h, --help display help for command +``` + ## create-plugin Scope: `root` From 16d06f6ac3ec8fd1553c4b847b96e692d9dc8125 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 13:20:48 +0100 Subject: [PATCH 27/42] changesets: add changeset for CLI create command Signed-off-by: Patrik Oldsberg --- .changeset/tidy-beans-reflect.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tidy-beans-reflect.md diff --git a/.changeset/tidy-beans-reflect.md b/.changeset/tidy-beans-reflect.md new file mode 100644 index 0000000000..20796075f0 --- /dev/null +++ b/.changeset/tidy-beans-reflect.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Introduces new `backstage-cli create` command to replace `create-plugin` and make space for creating a wider array of things. The create command also adds a new template for creating isomorphic common plugin packages. From 6a46eb2693223fce4f08bb3b707397a0bd7c2a82 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 15:01:50 +0100 Subject: [PATCH 28/42] cli: refactor inquirer prompt message transforms for clarity Signed-off-by: Patrik Oldsberg --- .../cli/src/lib/create/FactoryRegistry.ts | 50 +++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/lib/create/FactoryRegistry.ts b/packages/cli/src/lib/create/FactoryRegistry.ts index 1b25f5950a..9072d46c4c 100644 --- a/packages/cli/src/lib/create/FactoryRegistry.ts +++ b/packages/cli/src/lib/create/FactoryRegistry.ts @@ -16,10 +16,39 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; -import { AnyFactory } from './types'; +import { AnyFactory, Prompt } from './types'; import * as factories from './factories'; import partition from 'lodash/partition'; +function applyPromptMessageTransforms( + prompt: Prompt, + transforms: { + message: (msg: string) => string; + error: (msg: string) => string; + }, +): Prompt { + return { + ...prompt, + message: + prompt.message && + (async answers => { + if (typeof prompt.message === 'function') { + return transforms.message(await prompt.message(answers)); + } + return transforms.message(await prompt.message!); + }), + validate: + prompt.validate && + (async (...args) => { + const result = await prompt.validate!(...args); + if (typeof result === 'string') { + return transforms.error(result); + } + return result; + }), + }; +} + export class FactoryRegistry { private static factoryMap = new Map( Object.values(factories).map(factory => [factory.name, factory]), @@ -82,19 +111,12 @@ export class FactoryRegistry { } currentOptions = await inquirer.prompt( - needsAnswers.map(option => ({ - ...option, - message: option.message && chalk.blue(option.message), - validate: - option.validate && - (async (...args) => { - const result = await option.validate!(...args); - if (typeof result === 'string') { - return chalk.red(result); - } - return result; - }), - })), + needsAnswers.map(option => + applyPromptMessageTransforms(option, { + message: chalk.blue, + error: chalk.red, + }), + ), currentOptions, ); } From 069934c62760d3bc00c1713ba183ee084c5386fc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 15:02:08 +0100 Subject: [PATCH 29/42] cli: some more docs for create types Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/create/types.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/cli/src/lib/create/types.ts b/packages/cli/src/lib/create/types.ts index fd2e684e11..5a2460d0df 100644 --- a/packages/cli/src/lib/create/types.ts +++ b/packages/cli/src/lib/create/types.ts @@ -40,10 +40,31 @@ export type AnyOptions = Record; export type Prompt = DistinctQuestion & { name: string }; export interface Factory { + /** + * The name used for this factory. + */ name: string; + + /** + * A description that describes what this factory creates to the user. + */ description: string; + + /** + * An optional options discovery step that is run + * before the prompts to potentially fill in some of the options. + */ optionsDiscovery?(): Promise>; + + /** + * Inquirer prompts that will be filled in either interactively or + * through command line arguments. + */ optionsPrompts?: ReadonlyArray>; + + /** + * The main method of the factory that handles creation. + */ create(options: TOptions, context?: CreateContext): Promise; } From bd3ae04de9e990ed9bad2c8051cb491721f18d94 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 15:26:23 +0100 Subject: [PATCH 30/42] cli: fixes for common package factory Signed-off-by: Patrik Oldsberg --- .../src/lib/create/factories/pluginCommon.test.ts | 9 +++++++-- .../cli/src/lib/create/factories/pluginCommon.ts | 13 ++++++++----- .../src/{index.ts => index.ts.hbs} | 3 +++ 3 files changed, 18 insertions(+), 7 deletions(-) rename packages/cli/templates/default-common-plugin-package/src/{index.ts => index.ts.hbs} (90%) diff --git a/packages/cli/src/lib/create/factories/pluginCommon.test.ts b/packages/cli/src/lib/create/factories/pluginCommon.test.ts index 73edc5159b..e329e2588f 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.test.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.test.ts @@ -29,6 +29,11 @@ describe('pluginCommon factory', () => { }); }); + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + it('should create a common plugin package', async () => { mockFs({ '/root': { @@ -72,7 +77,7 @@ describe('pluginCommon factory', () => { 'templating README.md.hbs ✔', 'templating package.json.hbs ✔', 'copying tsconfig.json ✔', - 'copying index.ts ✔', + 'templating index.ts.hbs ✔', 'copying setupTests.ts ✔', 'Installing:', 'moving plugins/test-common ✔', @@ -83,7 +88,7 @@ describe('pluginCommon factory', () => { ).resolves.toEqual( expect.objectContaining({ name: 'plugin-test-common', - description: 'Common functionalities for the test-common plugin', + description: 'Common functionalities for the test plugin', private: true, version: '1.0.0', }), diff --git a/packages/cli/src/lib/create/factories/pluginCommon.ts b/packages/cli/src/lib/create/factories/pluginCommon.ts index 97b1eccec7..af42d74b0b 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.ts @@ -36,15 +36,18 @@ export const pluginCommon = createFactory({ }), optionsPrompts: [pluginIdPrompt(), ownerPrompt()], async create(options: Options, ctx: CreateContext) { - const id = `${options.id}-common`; - const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + const { id } = options; + const suffix = `${id}-common`; + const name = ctx.scope + ? `@${ctx.scope}/plugin-${suffix}` + : `plugin-${suffix}`; Task.log(); Task.log(`Creating backend plugin ${chalk.cyan(name)}`); const targetDir = ctx.isMonoRepo - ? paths.resolveTargetRoot('plugins', id) - : paths.resolveTargetRoot(`backstage-plugin-${id}`); + ? paths.resolveTargetRoot('plugins', suffix) + : paths.resolveTargetRoot(`backstage-plugin-${suffix}`); await executePluginPackageTemplate(ctx, { targetDir, @@ -59,7 +62,7 @@ export const pluginCommon = createFactory({ }); if (options.owner) { - await addCodeownersEntry(`/plugins/${id}`, options.owner); + await addCodeownersEntry(`/plugins/${suffix}`, options.owner); } await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); diff --git a/packages/cli/templates/default-common-plugin-package/src/index.ts b/packages/cli/templates/default-common-plugin-package/src/index.ts.hbs similarity index 90% rename from packages/cli/templates/default-common-plugin-package/src/index.ts rename to packages/cli/templates/default-common-plugin-package/src/index.ts.hbs index 6ee452b5bb..2e1150d74e 100644 --- a/packages/cli/templates/default-common-plugin-package/src/index.ts +++ b/packages/cli/templates/default-common-plugin-package/src/index.ts.hbs @@ -1,5 +1,8 @@ +/***/ /** * Common functionalities for the {{id}} plugin. + * + * @packageDocumentation */ /** From 103d9e2f53e97f93ef742715a88be15f9941bb3d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 15:29:33 +0100 Subject: [PATCH 31/42] cli: fix create monorepo check Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index d80daa6ddf..64067e1b7e 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -51,10 +51,20 @@ export default async (cmd: Command) => { providedOptions, ); - const rootPackageJson = await fs.readJson( - paths.resolveTargetRoot('package.json'), - ); - const isMonoRepo = Boolean(rootPackageJson.workspaces); + let isMonoRepo = false; + try { + const rootPackageJson = await fs.readJson( + paths.resolveTargetRoot('package.json'), + ); + if (rootPackageJson.workspaces) { + isMonoRepo = true; + } + } catch (error) { + assertError(error); + if (error.code !== 'ENOENT') { + throw error; + } + } let defaultVersion = '0.1.0'; try { From edaaf2dccb8a20f26a26c20b912bc90234350f74 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 15:32:20 +0100 Subject: [PATCH 32/42] cli: fix non-monorepo package naming for create Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/create/factories/backendPlugin.test.ts | 4 ++-- packages/cli/src/lib/create/factories/backendPlugin.ts | 4 +++- .../cli/src/lib/create/factories/frontendPlugin.test.ts | 6 +++--- packages/cli/src/lib/create/factories/frontendPlugin.ts | 4 +++- packages/cli/src/lib/create/factories/pluginCommon.test.ts | 4 ++-- packages/cli/src/lib/create/factories/pluginCommon.ts | 2 +- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/lib/create/factories/backendPlugin.test.ts b/packages/cli/src/lib/create/factories/backendPlugin.test.ts index 409b7f476a..5fe52c3732 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.test.ts @@ -73,7 +73,7 @@ describe('backendPlugin factory', () => { expect(output).toEqual([ '', - 'Creating backend plugin plugin-test-backend', + 'Creating backend plugin backstage-plugin-test-backend', 'Checking Prerequisites:', 'availability plugins/test-backend ✔', 'creating temp dir ✔', @@ -97,7 +97,7 @@ describe('backendPlugin factory', () => { fs.readJson('/root/packages/backend/package.json'), ).resolves.toEqual({ dependencies: { - 'plugin-test-backend': '^1.0.0', + 'backstage-plugin-test-backend': '^1.0.0', }, }); diff --git a/packages/cli/src/lib/create/factories/backendPlugin.ts b/packages/cli/src/lib/create/factories/backendPlugin.ts index 2bdc62741d..4d03a4da7b 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.ts @@ -39,7 +39,9 @@ export const backendPlugin = createFactory({ optionsPrompts: [pluginIdPrompt(), ownerPrompt()], async create(options: Options, ctx: CreateContext) { const id = `${options.id}-backend`; - const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + const name = ctx.scope + ? `@${ctx.scope}/plugin-${id}` + : `backstage-plugin-${id}`; Task.log(); Task.log(`Creating backend plugin ${chalk.cyan(name)}`); diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts index 3e0f94f6ad..a882a130ea 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts @@ -86,7 +86,7 @@ describe('frontendPlugin factory', () => { expect(output).toEqual([ '', - 'Creating backend plugin plugin-test', + 'Creating backend plugin backstage-plugin-test', 'Checking Prerequisites:', 'availability plugins/test ✔', 'creating temp dir ✔', @@ -117,14 +117,14 @@ describe('frontendPlugin factory', () => { fs.readJson('/root/packages/app/package.json'), ).resolves.toEqual({ dependencies: { - 'plugin-test': '^1.0.0', + 'backstage-plugin-test': '^1.0.0', }, }); await expect(fs.readFile('/root/packages/app/src/App.tsx', 'utf8')).resolves .toBe(` import { createApp } from '@backstage/app-defaults'; -import { TestPage } from 'plugin-test'; +import { TestPage } from 'backstage-plugin-test'; const router = ( diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index a6c0d0470d..3862ae4883 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -41,7 +41,9 @@ export const frontendPlugin = createFactory({ async create(options: Options, ctx: CreateContext) { const { id } = options; - const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + const name = ctx.scope + ? `@${ctx.scope}/plugin-${id}` + : `backstage-plugin-${id}`; const extensionName = `${upperFirst(camelCase(id))}Page`; Task.log(); diff --git a/packages/cli/src/lib/create/factories/pluginCommon.test.ts b/packages/cli/src/lib/create/factories/pluginCommon.test.ts index e329e2588f..a39bb3877d 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.test.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.test.ts @@ -68,7 +68,7 @@ describe('pluginCommon factory', () => { expect(output).toEqual([ '', - 'Creating backend plugin plugin-test-common', + 'Creating backend plugin backstage-plugin-test-common', 'Checking Prerequisites:', 'availability plugins/test-common ✔', 'creating temp dir ✔', @@ -87,7 +87,7 @@ describe('pluginCommon factory', () => { fs.readJson('/root/plugins/test-common/package.json'), ).resolves.toEqual( expect.objectContaining({ - name: 'plugin-test-common', + name: 'backstage-plugin-test-common', description: 'Common functionalities for the test plugin', private: true, version: '1.0.0', diff --git a/packages/cli/src/lib/create/factories/pluginCommon.ts b/packages/cli/src/lib/create/factories/pluginCommon.ts index af42d74b0b..1bcca2fb6a 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.ts @@ -40,7 +40,7 @@ export const pluginCommon = createFactory({ const suffix = `${id}-common`; const name = ctx.scope ? `@${ctx.scope}/plugin-${suffix}` - : `plugin-${suffix}`; + : `backstage-plugin-${suffix}`; Task.log(); Task.log(`Creating backend plugin ${chalk.cyan(name)}`); From 6dcfe227a29006a43763bf2c1d5e345a4bfc0cb3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 16:32:20 +0100 Subject: [PATCH 33/42] cli: add create factory for scaffolder modules Signed-off-by: Patrik Oldsberg --- .changeset/shiny-starfishes-float.md | 5 + .../cli/src/lib/create/factories/index.ts | 1 + .../create/factories/scaffolderModule.test.ts | 110 ++++++++++++++++++ .../lib/create/factories/scaffolderModule.ts | 96 +++++++++++++++ packages/cli/src/lib/version.ts | 2 + .../templates/scaffolder-module/.eslintrc.js | 3 + .../templates/scaffolder-module/README.md.hbs | 5 + .../scaffolder-module/package.json.hbs | 37 ++++++ .../src/actions/example/example.test.ts | 50 ++++++++ .../src/actions/example/example.ts | 57 +++++++++ .../src/actions/example/index.ts | 1 + .../scaffolder-module/src/actions/index.ts | 1 + .../scaffolder-module/src/index.ts.hbs | 8 ++ .../templates/scaffolder-module/tsconfig.json | 9 ++ 14 files changed, 385 insertions(+) create mode 100644 .changeset/shiny-starfishes-float.md create mode 100644 packages/cli/src/lib/create/factories/scaffolderModule.test.ts create mode 100644 packages/cli/src/lib/create/factories/scaffolderModule.ts create mode 100644 packages/cli/templates/scaffolder-module/.eslintrc.js create mode 100644 packages/cli/templates/scaffolder-module/README.md.hbs create mode 100644 packages/cli/templates/scaffolder-module/package.json.hbs create mode 100644 packages/cli/templates/scaffolder-module/src/actions/example/example.test.ts create mode 100644 packages/cli/templates/scaffolder-module/src/actions/example/example.ts create mode 100644 packages/cli/templates/scaffolder-module/src/actions/example/index.ts create mode 100644 packages/cli/templates/scaffolder-module/src/actions/index.ts create mode 100644 packages/cli/templates/scaffolder-module/src/index.ts.hbs create mode 100644 packages/cli/templates/scaffolder-module/tsconfig.json diff --git a/.changeset/shiny-starfishes-float.md b/.changeset/shiny-starfishes-float.md new file mode 100644 index 0000000000..0673791375 --- /dev/null +++ b/.changeset/shiny-starfishes-float.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added a scaffolder backend module template for the `create` command. diff --git a/packages/cli/src/lib/create/factories/index.ts b/packages/cli/src/lib/create/factories/index.ts index e4df9062f7..0764d33e2c 100644 --- a/packages/cli/src/lib/create/factories/index.ts +++ b/packages/cli/src/lib/create/factories/index.ts @@ -17,3 +17,4 @@ export { frontendPlugin } from './frontendPlugin'; export { backendPlugin } from './backendPlugin'; export { pluginCommon } from './pluginCommon'; +export { scaffolderModule } from './scaffolderModule'; diff --git a/packages/cli/src/lib/create/factories/scaffolderModule.test.ts b/packages/cli/src/lib/create/factories/scaffolderModule.test.ts new file mode 100644 index 0000000000..dde711b750 --- /dev/null +++ b/packages/cli/src/lib/create/factories/scaffolderModule.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { paths } from '../../paths'; +import { Task } from '../../tasks'; +import { FactoryRegistry } from '../FactoryRegistry'; +import { createMockOutputStream, mockPaths } from './common/testUtils'; +import { scaffolderModule } from './scaffolderModule'; + +describe('scaffolderModule factory', () => { + beforeEach(() => { + mockPaths({ + targetRoot: '/root', + }); + }); + + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should create a scaffolder backend module package', async () => { + mockFs({ + '/root': { + plugins: mockFs.directory(), + }, + [paths.resolveOwn('templates')]: mockFs.load( + paths.resolveOwn('templates'), + ), + }); + + const options = await FactoryRegistry.populateOptions(scaffolderModule, { + id: 'test', + }); + + let modified = false; + + const [output, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + jest.spyOn(Task, 'forCommand').mockResolvedValue(); + + await scaffolderModule.create(options, { + private: true, + isMonoRepo: true, + defaultVersion: '1.0.0', + markAsModified: () => { + modified = true; + }, + createTemporaryDirectory: (name: string) => fs.mkdtemp(name), + }); + + expect(modified).toBe(true); + + expect(output).toEqual([ + '', + 'Creating module backstage-plugin-scaffolder-backend-module-test', + 'Checking Prerequisites:', + 'availability plugins/scaffolder-backend-module-test ✔', + 'creating temp dir ✔', + 'Executing Template:', + 'copying .eslintrc.js ✔', + 'templating README.md.hbs ✔', + 'templating package.json.hbs ✔', + 'copying tsconfig.json ✔', + 'templating index.ts.hbs ✔', + 'copying index.ts ✔', + 'copying example.test.ts ✔', + 'copying example.ts ✔', + 'copying index.ts ✔', + 'Installing:', + 'moving plugins/scaffolder-backend-module-test ✔', + ]); + + await expect( + fs.readJson('/root/plugins/scaffolder-backend-module-test/package.json'), + ).resolves.toEqual( + expect.objectContaining({ + name: 'backstage-plugin-scaffolder-backend-module-test', + description: 'The test module for @backstage/plugin-scaffolder-backend', + private: true, + version: '1.0.0', + }), + ); + + expect(Task.forCommand).toHaveBeenCalledTimes(2); + expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { + cwd: '/root/plugins/scaffolder-backend-module-test', + optional: true, + }); + expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { + cwd: '/root/plugins/scaffolder-backend-module-test', + optional: true, + }); + }); +}); diff --git a/packages/cli/src/lib/create/factories/scaffolderModule.ts b/packages/cli/src/lib/create/factories/scaffolderModule.ts new file mode 100644 index 0000000000..b89f0fc691 --- /dev/null +++ b/packages/cli/src/lib/create/factories/scaffolderModule.ts @@ -0,0 +1,96 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 chalk from 'chalk'; +import { paths } from '../../paths'; +import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; +import { createFactory, CreateContext } from '../types'; +import { Task } from '../../tasks'; +import { ownerPrompt } from './common/prompts'; +import { executePluginPackageTemplate } from './common/tasks'; + +type Options = { + id: string; + owner?: string; + codeOwnersPath?: string; +}; + +export const scaffolderModule = createFactory({ + name: 'scaffolder-module', + description: + 'An module exporting custom actions for @backstage/plugin-scaffolder-backend', + optionsDiscovery: async () => ({ + codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), + }), + optionsPrompts: [ + { + type: 'input', + name: 'id', + message: 'Enter the name of the module [required]', + validate: (value: string) => { + if (!value) { + return 'Please enter the name of the module'; + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return 'Module names must be lowercase and contain only letters, digits, and dashes.'; + } + return true; + }, + }, + ownerPrompt(), + ], + async create(options: Options, ctx: CreateContext) { + const { id } = options; + const slug = `scaffolder-backend-module-${id}`; + + let name = `backstage-plugin-${slug}`; + if (ctx.scope) { + if (ctx.scope === 'backstage') { + name = `@backstage/plugin-${slug}`; + } else { + name = `@${ctx.scope}/backstage-plugin-${slug}`; + } + } + + Task.log(); + Task.log(`Creating module ${chalk.cyan(name)}`); + + const targetDir = ctx.isMonoRepo + ? paths.resolveTargetRoot('plugins', slug) + : paths.resolveTargetRoot(`backstage-plugin-${slug}`); + + await executePluginPackageTemplate(ctx, { + targetDir, + templateName: 'scaffolder-module', + values: { + id, + name, + privatePackage: ctx.private, + npmRegistry: ctx.npmRegistry, + pluginVersion: ctx.defaultVersion, + }, + }); + + if (options.owner) { + await addCodeownersEntry(`/plugins/${slug}`, options.owner); + } + + await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); + await Task.forCommand('yarn lint --fix', { + cwd: targetDir, + optional: true, + }); + }, +}); diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index c69072c64d..3b512b22c3 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -42,6 +42,7 @@ import { version as corePluginApi } from '@backstage/core-plugin-api/package.jso import { version as devUtils } from '@backstage/dev-utils/package.json'; import { version as testUtils } from '@backstage/test-utils/package.json'; import { version as theme } from '@backstage/theme/package.json'; +import { version as scaffolderBackend } from '@backstage/plugin-scaffolder-backend/package.json'; export const packageVersions: Record = { '@backstage/backend-common': backendCommon, @@ -53,6 +54,7 @@ export const packageVersions: Record = { '@backstage/dev-utils': devUtils, '@backstage/test-utils': testUtils, '@backstage/theme': theme, + '@backstage/plugin-scaffolder-backend': scaffolderBackend, }; export function findVersion() { diff --git a/packages/cli/templates/scaffolder-module/.eslintrc.js b/packages/cli/templates/scaffolder-module/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/packages/cli/templates/scaffolder-module/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/packages/cli/templates/scaffolder-module/README.md.hbs b/packages/cli/templates/scaffolder-module/README.md.hbs new file mode 100644 index 0000000000..8ee653a4ac --- /dev/null +++ b/packages/cli/templates/scaffolder-module/README.md.hbs @@ -0,0 +1,5 @@ +# {{name}} + +The {{id}} module for [@backstage/plugin-scaffolder-backend](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend). + +_This plugin was created through the Backstage CLI_ diff --git a/packages/cli/templates/scaffolder-module/package.json.hbs b/packages/cli/templates/scaffolder-module/package.json.hbs new file mode 100644 index 0000000000..de6ffd9e3a --- /dev/null +++ b/packages/cli/templates/scaffolder-module/package.json.hbs @@ -0,0 +1,37 @@ +{ + "name": "{{name}}", + "description": "The {{id}} module for @backstage/plugin-scaffolder-backend", + "version": "{{pluginVersion}}", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", +{{#if privatePackage}} + "private": {{privatePackage}}, +{{/if}} + "publishConfig": { +{{#if npmRegistry}} + "registry": "{{npmRegistry}}", +{{/if}} + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli build --output cjs,types", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/plugin-scaffolder-backend": "{{versionQuery '@backstage/plugin-scaffolder-backend'}}" + }, + "devDependencies": { + "@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}", + "@backstage/cli": "{{versionQuery '@backstage/cli'}}" + }, + "files": [ + "dist" + ] +} diff --git a/packages/cli/templates/scaffolder-module/src/actions/example/example.test.ts b/packages/cli/templates/scaffolder-module/src/actions/example/example.test.ts new file mode 100644 index 0000000000..e427b2c603 --- /dev/null +++ b/packages/cli/templates/scaffolder-module/src/actions/example/example.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 { PassThrough } from 'stream'; +import { createAcmeExampleAction } from './example'; +import { getVoidLogger } from '@backstage/backend-common'; + +describe('acme:example', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should call action', async () => { + const action = createAcmeExampleAction(); + + const logger = getVoidLogger(); + jest.spyOn(logger, 'info'); + + await action.handler({ + input: { + myParameter: 'test', + }, + workspacePath: '/tmp', + logger, + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory() { + // Usage of mock-fs is recommended for testing of filesystem operations + throw new Error('Not implemented'); + }, + }); + + expect(logger.info).toHaveBeenCalledWith( + 'Running example template with parameters: test', + ); + }); +}); diff --git a/packages/cli/templates/scaffolder-module/src/actions/example/example.ts b/packages/cli/templates/scaffolder-module/src/actions/example/example.ts new file mode 100644 index 0000000000..c20a4bdf25 --- /dev/null +++ b/packages/cli/templates/scaffolder-module/src/actions/example/example.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 { createTemplateAction } from '@backstage/plugin-scaffolder-backend'; + +/** + * Creates an `acme:example` Scaffolder action. + * + * @remarks + * + * See {@link https://example.com} for more information. + * + * @public + */ +export function createAcmeExampleAction() { + // For more information on how to define custom actions, see + // https://backstage.io/docs/features/software-templates/writing-custom-actions + return createTemplateAction<{ + myParameter: string; + }>({ + id: 'acme:example', + description: 'Runs Yeoman on an installed Yeoman generator', + schema: { + input: { + type: 'object', + required: ['myParameter'], + properties: { + myParameter: { + title: 'An example parameter', + description: 'This is the schema for our example parameter', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + ctx.logger.info( + `Running example template with parameters: ${ctx.input.myParameter}`, + ); + + await new Promise(resolve => setTimeout(resolve, 1000)); + }, + }); +} diff --git a/packages/cli/templates/scaffolder-module/src/actions/example/index.ts b/packages/cli/templates/scaffolder-module/src/actions/example/index.ts new file mode 100644 index 0000000000..e81099f333 --- /dev/null +++ b/packages/cli/templates/scaffolder-module/src/actions/example/index.ts @@ -0,0 +1 @@ +export { createAcmeExampleAction } from './example'; diff --git a/packages/cli/templates/scaffolder-module/src/actions/index.ts b/packages/cli/templates/scaffolder-module/src/actions/index.ts new file mode 100644 index 0000000000..ab6642ebb0 --- /dev/null +++ b/packages/cli/templates/scaffolder-module/src/actions/index.ts @@ -0,0 +1 @@ +export * from './example'; diff --git a/packages/cli/templates/scaffolder-module/src/index.ts.hbs b/packages/cli/templates/scaffolder-module/src/index.ts.hbs new file mode 100644 index 0000000000..3690e43b8e --- /dev/null +++ b/packages/cli/templates/scaffolder-module/src/index.ts.hbs @@ -0,0 +1,8 @@ +/***/ +/** + * The {{id}} module for @backstage/plugin-scaffolder-backend. + * + * @packageDocumentation + */ + +export * from './actions'; diff --git a/packages/cli/templates/scaffolder-module/tsconfig.json b/packages/cli/templates/scaffolder-module/tsconfig.json new file mode 100644 index 0000000000..5ae9aeb62d --- /dev/null +++ b/packages/cli/templates/scaffolder-module/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "dist-types", + "rootDir": "." + } +} From b6a4bacdc4f35e1d5071f61224811213cb26a2af Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Nov 2021 16:14:48 +0100 Subject: [PATCH 34/42] core-plugin-api: added ErrorApi* prefix to Error and ErrorContext types Signed-off-by: Patrik Oldsberg --- .changeset/yellow-deers-act.md | 5 +++ packages/core-app-api/api-report.md | 7 ++-- packages/core-plugin-api/api-report.md | 32 +++++++++++-------- .../src/apis/definitions/ErrorApi.ts | 23 ++++++++++--- 4 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 .changeset/yellow-deers-act.md diff --git a/.changeset/yellow-deers-act.md b/.changeset/yellow-deers-act.md new file mode 100644 index 0000000000..a9008ea41e --- /dev/null +++ b/.changeset/yellow-deers-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': patch +--- + +Deprecated the `Error` and `ErrorContext` types, replacing them with identical `ErrorApiError` and `ErrorApiErrorContext` types. diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index d98b5ef5f8..26438ba798 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -28,8 +28,9 @@ import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { ConfigReader } from '@backstage/config'; import { DiscoveryApi } from '@backstage/core-plugin-api'; -import { Error as Error_2 } from '@backstage/core-plugin-api'; import { ErrorApi } from '@backstage/core-plugin-api'; +import { ErrorApiError } from '@backstage/core-plugin-api'; +import { ErrorApiErrorContext } from '@backstage/core-plugin-api'; import { ErrorContext } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FeatureFlag } from '@backstage/core-plugin-api'; @@ -327,8 +328,8 @@ export class ErrorAlerter implements ErrorApi { constructor(alertApi: AlertApi, errorApi: ErrorApi); // (undocumented) error$(): Observable<{ - error: Error_2; - context?: ErrorContext | undefined; + error: ErrorApiError; + context?: ErrorApiErrorContext | undefined; }>; // (undocumented) post(error: Error, context?: ErrorContext): void; diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 2765b02bca..87cbf1dd5f 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -404,23 +404,31 @@ export interface ElementCollection { }): ElementCollection; } -// @public -type Error_2 = { - name: string; - message: string; - stack?: string; -}; +// @public @deprecated (undocumented) +type Error_2 = ErrorApiError; export { Error_2 as Error }; // @public export type ErrorApi = { - post(error: Error_2, context?: ErrorContext): void; + post(error: ErrorApiError, context?: ErrorApiErrorContext): void; error$(): Observable_2<{ - error: Error_2; - context?: ErrorContext; + error: ErrorApiError; + context?: ErrorApiErrorContext; }>; }; +// @public +export type ErrorApiError = { + name: string; + message: string; + stack?: string; +}; + +// @public +export type ErrorApiErrorContext = { + hidden?: boolean; +}; + // @public export const errorApiRef: ApiRef; @@ -431,10 +439,8 @@ export type ErrorBoundaryFallbackProps = { resetError: () => void; }; -// @public -export type ErrorContext = { - hidden?: boolean; -}; +// @public @deprecated (undocumented) +export type ErrorContext = ErrorApiErrorContext; // @public export type Extension = { diff --git a/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts b/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts index 820660ac52..45ee0901c8 100644 --- a/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts @@ -23,22 +23,34 @@ import { Observable } from '@backstage/types'; * * @public */ -export type Error = { +export type ErrorApiError = { name: string; message: string; stack?: string; }; +/** + * @public + * @deprecated Use ErrorApiError instead + */ +export type Error = ErrorApiError; + /** * Provides additional information about an error that was posted to the application. * * @public */ -export type ErrorContext = { +export type ErrorApiErrorContext = { // If set to true, this error should not be displayed to the user. Defaults to false. hidden?: boolean; }; +/** + * @public + * @deprecated Use ErrorApiErrorContext instead + */ +export type ErrorContext = ErrorApiErrorContext; + /** * The error API is used to report errors to the app, and display them to the user. * @@ -62,12 +74,15 @@ export type ErrorApi = { /** * Post an error for handling by the application. */ - post(error: Error, context?: ErrorContext): void; + post(error: ErrorApiError, context?: ErrorApiErrorContext): void; /** * Observe errors posted by other parts of the application. */ - error$(): Observable<{ error: Error; context?: ErrorContext }>; + error$(): Observable<{ + error: ErrorApiError; + context?: ErrorApiErrorContext; + }>; }; /** From 0b1de527326325aedbf0488c454a922cc03e8b01 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Nov 2021 16:21:02 +0100 Subject: [PATCH 35/42] core-app-api,test-utils: migrated to using new ErrorApi* names Signed-off-by: Patrik Oldsberg --- .changeset/nine-bananas-mate.md | 6 ++++++ docs/api/utility-apis.md | 2 +- packages/core-app-api/api-report.md | 9 ++++----- .../implementations/ErrorApi/ErrorAlerter.ts | 9 +++++++-- .../ErrorApi/ErrorApiForwarder.ts | 12 ++++++++---- .../ErrorApi/UnhandledErrorForwarder.ts | 10 +++++++--- packages/test-utils/api-report.md | 13 +++++++------ .../testUtils/apis/ErrorApi/MockErrorApi.ts | 19 +++++++++++++------ 8 files changed, 53 insertions(+), 27 deletions(-) create mode 100644 .changeset/nine-bananas-mate.md diff --git a/.changeset/nine-bananas-mate.md b/.changeset/nine-bananas-mate.md new file mode 100644 index 0000000000..e119eb722f --- /dev/null +++ b/.changeset/nine-bananas-mate.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-app-api': patch +'@backstage/test-utils': patch +--- + +Migrated to using new `ErrorApiError` and `ErrorApiErrorContext` names. diff --git a/docs/api/utility-apis.md b/docs/api/utility-apis.md index 1b364bcdae..133653df44 100644 --- a/docs/api/utility-apis.md +++ b/docs/api/utility-apis.md @@ -199,7 +199,7 @@ export a class that `implements` the target API, for example: ```ts export class IgnoringErrorApi implements ErrorApi { - post(error: Error, context?: ErrorContext) { + post(error: ErrorApiError, context?: ErrorApiErrorContext) { // ignore error } } diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 26438ba798..2c390bf132 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -31,7 +31,6 @@ import { DiscoveryApi } from '@backstage/core-plugin-api'; import { ErrorApi } from '@backstage/core-plugin-api'; import { ErrorApiError } from '@backstage/core-plugin-api'; import { ErrorApiErrorContext } from '@backstage/core-plugin-api'; -import { ErrorContext } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FeatureFlag } from '@backstage/core-plugin-api'; import { FeatureFlagsApi } from '@backstage/core-plugin-api'; @@ -332,7 +331,7 @@ export class ErrorAlerter implements ErrorApi { context?: ErrorApiErrorContext | undefined; }>; // (undocumented) - post(error: Error, context?: ErrorContext): void; + post(error: ErrorApiError, context?: ErrorApiErrorContext): void; } // @public @@ -340,10 +339,10 @@ export class ErrorApiForwarder implements ErrorApi { // (undocumented) error$(): Observable<{ error: Error; - context?: ErrorContext; + context?: ErrorApiErrorContext; }>; // (undocumented) - post(error: Error, context?: ErrorContext): void; + post(error: ErrorApiError, context?: ErrorApiErrorContext): void; } // @public @@ -604,7 +603,7 @@ export type SignInResult = { // @public export class UnhandledErrorForwarder { - static forward(errorApi: ErrorApi, errorContext: ErrorContext): void; + static forward(errorApi: ErrorApi, errorContext: ErrorApiErrorContext): void; } // @public diff --git a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts index 2111798d6a..350213d938 100644 --- a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts +++ b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts @@ -13,7 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ErrorApi, ErrorContext, AlertApi } from '@backstage/core-plugin-api'; +import { + ErrorApi, + ErrorApiError, + ErrorApiErrorContext, + AlertApi, +} from '@backstage/core-plugin-api'; /** * Decorates an ErrorApi by also forwarding error messages @@ -27,7 +32,7 @@ export class ErrorAlerter implements ErrorApi { private readonly errorApi: ErrorApi, ) {} - post(error: Error, context?: ErrorContext) { + post(error: ErrorApiError, context?: ErrorApiErrorContext) { if (!context?.hidden) { this.alertApi.post({ message: error.message, severity: 'error' }); } diff --git a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts index cd4564a050..f67c00d991 100644 --- a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts +++ b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api'; +import { + ErrorApi, + ErrorApiError, + ErrorApiErrorContext, +} from '@backstage/core-plugin-api'; import { Observable } from '@backstage/types'; import { PublishSubject } from '../../../lib/subjects'; @@ -26,14 +30,14 @@ import { PublishSubject } from '../../../lib/subjects'; export class ErrorApiForwarder implements ErrorApi { private readonly subject = new PublishSubject<{ error: Error; - context?: ErrorContext; + context?: ErrorApiErrorContext; }>(); - post(error: Error, context?: ErrorContext) { + post(error: ErrorApiError, context?: ErrorApiErrorContext) { this.subject.next({ error, context }); } - error$(): Observable<{ error: Error; context?: ErrorContext }> { + error$(): Observable<{ error: Error; context?: ErrorApiErrorContext }> { return this.subject; } } diff --git a/packages/core-app-api/src/apis/implementations/ErrorApi/UnhandledErrorForwarder.ts b/packages/core-app-api/src/apis/implementations/ErrorApi/UnhandledErrorForwarder.ts index 16e473fb3a..8e697f12b2 100644 --- a/packages/core-app-api/src/apis/implementations/ErrorApi/UnhandledErrorForwarder.ts +++ b/packages/core-app-api/src/apis/implementations/ErrorApi/UnhandledErrorForwarder.ts @@ -1,4 +1,8 @@ -import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api'; +import { + ErrorApi, + ErrorApiError, + ErrorApiErrorContext, +} from '@backstage/core-plugin-api'; /* * Copyright 2020 Spotify AB @@ -25,11 +29,11 @@ export class UnhandledErrorForwarder { /** * Add event listener, such that unhandled errors can be forwarded using an given `ErrorApi` instance */ - static forward(errorApi: ErrorApi, errorContext: ErrorContext) { + static forward(errorApi: ErrorApi, errorContext: ErrorApiErrorContext) { window.addEventListener( 'unhandledrejection', (e: PromiseRejectionEvent) => { - errorApi.post(e.reason as Error, errorContext); + errorApi.post(e.reason as ErrorApiError, errorContext); }, ); } diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index c4936f6d96..62a8d9f601 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -7,7 +7,8 @@ import { AnalyticsApi } from '@backstage/core-plugin-api'; import { AnalyticsEvent } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { ErrorApi } from '@backstage/core-plugin-api'; -import { ErrorContext } from '@backstage/core-plugin-api'; +import { ErrorApiError } from '@backstage/core-plugin-api'; +import { ErrorApiErrorContext } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { Observable } from '@backstage/types'; import { ReactElement } from 'react'; @@ -27,8 +28,8 @@ export type CollectedLogs = { // @public export type ErrorWithContext = { - error: Error; - context?: ErrorContext; + error: ErrorApiError; + context?: ErrorApiErrorContext; }; // @public @deprecated (undocumented) @@ -109,13 +110,13 @@ export class MockErrorApi implements ErrorApi { constructor(options?: MockErrorApiOptions); // (undocumented) error$(): Observable<{ - error: Error; - context?: ErrorContext; + error: ErrorApiError; + context?: ErrorApiErrorContext; }>; // (undocumented) getErrors(): ErrorWithContext[]; // (undocumented) - post(error: Error, context?: ErrorContext): void; + post(error: ErrorApiError, context?: ErrorApiErrorContext): void; // (undocumented) waitForError(pattern: RegExp, timeoutMs?: number): Promise; } diff --git a/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts b/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts index 87601e29e2..96918b4e47 100644 --- a/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts +++ b/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api'; +import { + ErrorApi, + ErrorApiError, + ErrorApiErrorContext, +} from '@backstage/core-plugin-api'; import { Observable } from '@backstage/types'; /** @@ -27,12 +31,12 @@ export type MockErrorApiOptions = { }; /** - * ErrorWithContext contains error and ErrorContext + * ErrorWithContext contains error and ErrorApiErrorContext * @public */ export type ErrorWithContext = { - error: Error; - context?: ErrorContext; + error: ErrorApiError; + context?: ErrorApiErrorContext; }; type Waiter = { @@ -59,7 +63,7 @@ export class MockErrorApi implements ErrorApi { constructor(private readonly options: MockErrorApiOptions = {}) {} - post(error: Error, context?: ErrorContext) { + post(error: ErrorApiError, context?: ErrorApiErrorContext) { if (this.options.collect) { this.errors.push({ error, context }); @@ -76,7 +80,10 @@ export class MockErrorApi implements ErrorApi { throw new Error(`MockErrorApi received unexpected error, ${error}`); } - error$(): Observable<{ error: Error; context?: ErrorContext }> { + error$(): Observable<{ + error: ErrorApiError; + context?: ErrorApiErrorContext; + }> { return nullObservable; } From 7df99cdb77d795b7fd30482b5be4bdc45177f1a6 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 10 Nov 2021 15:10:17 +0100 Subject: [PATCH 36/42] core-plugin-api: Remove exports of unused types Signed-off-by: Johan Haals --- .changeset/silent-taxis-tan.md | 5 +++++ packages/core-plugin-api/src/plugin/index.ts | 2 -- packages/core-plugin-api/src/plugin/types.ts | 17 ----------------- 3 files changed, 5 insertions(+), 19 deletions(-) create mode 100644 .changeset/silent-taxis-tan.md diff --git a/.changeset/silent-taxis-tan.md b/.changeset/silent-taxis-tan.md new file mode 100644 index 0000000000..286ef40a4c --- /dev/null +++ b/.changeset/silent-taxis-tan.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': minor +--- + +Remove exports of unused types(`RouteOptions` and `RoutePath`). diff --git a/packages/core-plugin-api/src/plugin/index.ts b/packages/core-plugin-api/src/plugin/index.ts index 0cb9a2aede..d3272607ef 100644 --- a/packages/core-plugin-api/src/plugin/index.ts +++ b/packages/core-plugin-api/src/plugin/index.ts @@ -25,6 +25,4 @@ export type { PluginConfig, PluginHooks, PluginOutput, - RouteOptions, - RoutePath, } from './types'; diff --git a/packages/core-plugin-api/src/plugin/types.ts b/packages/core-plugin-api/src/plugin/types.ts index 192e771092..aeb7037c51 100644 --- a/packages/core-plugin-api/src/plugin/types.ts +++ b/packages/core-plugin-api/src/plugin/types.ts @@ -17,23 +17,6 @@ import { RouteRef, SubRouteRef, ExternalRouteRef } from '../routing'; import { AnyApiFactory } from '../apis/system'; -/** - * Route configuration. - * - * @public - */ -export type RouteOptions = { - // Whether the route path must match exactly, defaults to true. - exact?: boolean; -}; - -/** - * Type alias for paths. - * - * @public - */ -export type RoutePath = string; - /** * Replace with using {@link RouteRef}s. * From 51d0fb3a02d73d4888b002501e895368b14af57a Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 15 Nov 2021 14:56:06 +0100 Subject: [PATCH 37/42] Fix API report Signed-off-by: Johan Haals --- packages/core-plugin-api/api-report.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 2765b02bca..824b12d6fc 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -703,14 +703,6 @@ export type RouteFunc = ( ...[params]: Params extends undefined ? readonly [] : readonly [Params] ) => string; -// @public -export type RouteOptions = { - exact?: boolean; -}; - -// @public -export type RoutePath = string; - // @public export type RouteRef = { $$routeRefType: 'absolute'; From dee1db5f8ac526a58fc66c56cdd4da2654389839 Mon Sep 17 00:00:00 2001 From: Soren Mathiasen Date: Mon, 15 Nov 2021 18:30:55 +0100 Subject: [PATCH 38/42] Adding Tradeshift as adopters Signed-off-by: Soren Mathiasen --- ADOPTERS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ADOPTERS.md b/ADOPTERS.md index 6736538fa0..f5f5e260c9 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -64,4 +64,5 @@ | [SoundCloud](https://www.soundcloud.com) | [Julio Zynger](https://github.com/julioz) | Developer portal as a [humane registry](https://martinfowler.com/bliki/HumaneRegistry.html) for the organization: catalog of people, services, documentation, feature toggles, escalation policies, etc. | | [Volvofinans Bank](https://www.volvofinans.se) | [Johan Hammar](https://github.com/johanhammar) | Developer portal enabling engineers to manage and explore software and documentation. | | [Palo Alto Networks](https://www.paloaltonetworks.com) | [Jeremy Guarini](https://github.com/jeremyguarini), [Brian Lomeland](https://github.com/bbbmmmlll), [Palo Alto Networks](https://github.com/PaloAltoNetworks) | Developer portal, service catalog, documentation and tooling | -| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem +| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem | +| [Tradeshift](https://www.tradeshift.com/) | [Soren Mathiasen](https://github.com/sorenmat) | Developer Portal: documentation, monitoring, service templates, service catalog for our micro services | From 2345b318fb49fb79c19e4ba087bffca279b3fd55 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Nov 2021 22:36:41 +0100 Subject: [PATCH 39/42] create-app: fix windows file copy and mocking Signed-off-by: Patrik Oldsberg --- packages/create-app/src/createApp.test.ts | 2 +- packages/create-app/src/lib/tasks.ts | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index 7b3b8f124c..09099e2d15 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -25,7 +25,7 @@ jest.mock('./lib/tasks'); beforeAll(() => { mockFs({ - 'package.json': '', // required by `findPaths(__dirname)` + [`${__dirname}/package.json`]: '', // required by `findPaths(__dirname)` 'templates/': mockFs.load(path.resolve(__dirname, '../templates/')), }); }); diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 0b3ca63728..3961e84753 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -19,7 +19,12 @@ import fs from 'fs-extra'; import handlebars from 'handlebars'; import ora from 'ora'; import recursive from 'recursive-readdir'; -import { basename, dirname, resolve as resolvePath } from 'path'; +import { + basename, + dirname, + resolve as resolvePath, + relative as relativePath, +} from 'path'; import { exec as execCb } from 'child_process'; import { packageVersions } from './versions'; import { promisify } from 'util'; @@ -85,7 +90,10 @@ export async function templatingTask( }); for (const file of files) { - const destinationFile = file.replace(templateDir, destinationDir); + const destinationFile = resolvePath( + destinationDir, + relativePath(templateDir, file), + ); await fs.ensureDir(dirname(destinationFile)); if (file.endsWith('.hbs')) { From ad3afc5ad63ced76ab81d2c07607023ad5a9d705 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Nov 2021 00:22:37 +0100 Subject: [PATCH 40/42] cli: trim away non-ascii in stream mock testing util Signed-off-by: Patrik Oldsberg --- .../create/factories/backendPlugin.test.ts | 28 ++++++------- .../lib/create/factories/common/tasks.test.ts | 12 +++--- .../lib/create/factories/common/testUtils.ts | 15 +++++-- .../create/factories/frontendPlugin.test.ts | 42 +++++++++---------- .../lib/create/factories/pluginCommon.test.ts | 18 ++++---- .../create/factories/scaffolderModule.test.ts | 24 +++++------ 6 files changed, 73 insertions(+), 66 deletions(-) diff --git a/packages/cli/src/lib/create/factories/backendPlugin.test.ts b/packages/cli/src/lib/create/factories/backendPlugin.test.ts index 5fe52c3732..916ba7187c 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.test.ts @@ -75,22 +75,22 @@ describe('backendPlugin factory', () => { '', 'Creating backend plugin backstage-plugin-test-backend', 'Checking Prerequisites:', - 'availability plugins/test-backend ✔', - 'creating temp dir ✔', + 'availability plugins/test-backend', + 'creating temp dir', 'Executing Template:', - 'copying .eslintrc.js ✔', - 'templating README.md.hbs ✔', - 'templating package.json.hbs ✔', - 'copying tsconfig.json ✔', - 'copying index.ts ✔', - 'templating run.ts.hbs ✔', - 'copying setupTests.ts ✔', - 'copying router.test.ts ✔', - 'copying router.ts ✔', - 'templating standaloneServer.ts.hbs ✔', + 'copying .eslintrc.js', + 'templating README.md.hbs', + 'templating package.json.hbs', + 'copying tsconfig.json', + 'copying index.ts', + 'templating run.ts.hbs', + 'copying setupTests.ts', + 'copying router.test.ts', + 'copying router.ts', + 'templating standaloneServer.ts.hbs', 'Installing:', - 'moving plugins/test-backend ✔', - 'backend adding dependency ✔', + 'moving plugins/test-backend', + 'backend adding dependency', ]); await expect( diff --git a/packages/cli/src/lib/create/factories/common/tasks.test.ts b/packages/cli/src/lib/create/factories/common/tasks.test.ts index b3a751ba5b..0892322e61 100644 --- a/packages/cli/src/lib/create/factories/common/tasks.test.ts +++ b/packages/cli/src/lib/create/factories/common/tasks.test.ts @@ -88,14 +88,14 @@ some-package@^1.1.0: expect(modified).toBe(true); expect(output).toEqual([ 'Checking Prerequisites:', - 'availability /target ✔', - 'creating temp dir ✔', + 'availability /target', + 'creating temp dir', 'Executing Template:', - 'templating package.json.hbs ✔', - 'copying not-templated.txt ✔', - 'templating templated.txt.hbs ✔', + 'templating package.json.hbs', + 'copying not-templated.txt', + 'templating templated.txt.hbs', 'Installing:', - 'moving /target ✔', + 'moving /target', ]); await expect(fs.readFile('/target/package.json', 'utf8')).resolves.toBe(`{ "name": "my-testing-plugin", diff --git a/packages/cli/src/lib/create/factories/common/testUtils.ts b/packages/cli/src/lib/create/factories/common/testUtils.ts index 651409451e..01081a7486 100644 --- a/packages/cli/src/lib/create/factories/common/testUtils.ts +++ b/packages/cli/src/lib/create/factories/common/testUtils.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +/* eslint-disable no-control-regex */ + import { WriteStream } from 'tty'; import { resolve as resolvePath } from 'path'; import { paths } from '../../../paths'; @@ -59,10 +61,15 @@ export function createMockOutputStream() { cursorTo: () => {}, clearLine: () => {}, moveCursor: () => {}, - write: (msg: string) => - // Clean up colors and whitespace - // eslint-disable-next-line no-control-regex - output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), + write: (msg: string) => { + let clean = msg; + // Remove terminal color escape sequences + clean = clean.replace(/\x1B\[\d\dm/g, ''); + // Remove any non-ascii + clean = clean.replace(/[^\x00-\x7F]+/g, ''); + clean = clean.trim(); + output.push(clean); + }, } as unknown as WriteStream & { fd: any }, ] as const; } diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts index a882a130ea..439b134478 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts @@ -88,29 +88,29 @@ describe('frontendPlugin factory', () => { '', 'Creating backend plugin backstage-plugin-test', 'Checking Prerequisites:', - 'availability plugins/test ✔', - 'creating temp dir ✔', + 'availability plugins/test', + 'creating temp dir', 'Executing Template:', - 'copying .eslintrc.js ✔', - 'templating README.md.hbs ✔', - 'templating package.json.hbs ✔', - 'copying tsconfig.json ✔', - 'templating index.tsx.hbs ✔', - 'templating index.ts.hbs ✔', - 'templating plugin.test.ts.hbs ✔', - 'templating plugin.ts.hbs ✔', - 'templating routes.ts.hbs ✔', - 'copying setupTests.ts ✔', - 'templating ExampleComponent.test.tsx.hbs ✔', - 'templating ExampleComponent.tsx.hbs ✔', - 'copying index.ts ✔', - 'templating ExampleFetchComponent.test.tsx.hbs ✔', - 'templating ExampleFetchComponent.tsx.hbs ✔', - 'copying index.ts ✔', + 'copying .eslintrc.js', + 'templating README.md.hbs', + 'templating package.json.hbs', + 'copying tsconfig.json', + 'templating index.tsx.hbs', + 'templating index.ts.hbs', + 'templating plugin.test.ts.hbs', + 'templating plugin.ts.hbs', + 'templating routes.ts.hbs', + 'copying setupTests.ts', + 'templating ExampleComponent.test.tsx.hbs', + 'templating ExampleComponent.tsx.hbs', + 'copying index.ts', + 'templating ExampleFetchComponent.test.tsx.hbs', + 'templating ExampleFetchComponent.tsx.hbs', + 'copying index.ts', 'Installing:', - 'moving plugins/test ✔', - 'app adding dependency ✔', - 'app adding import ✔', + 'moving plugins/test', + 'app adding dependency', + 'app adding import', ]); await expect( diff --git a/packages/cli/src/lib/create/factories/pluginCommon.test.ts b/packages/cli/src/lib/create/factories/pluginCommon.test.ts index a39bb3877d..3124df6b1d 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.test.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.test.ts @@ -70,17 +70,17 @@ describe('pluginCommon factory', () => { '', 'Creating backend plugin backstage-plugin-test-common', 'Checking Prerequisites:', - 'availability plugins/test-common ✔', - 'creating temp dir ✔', + 'availability plugins/test-common', + 'creating temp dir', 'Executing Template:', - 'copying .eslintrc.js ✔', - 'templating README.md.hbs ✔', - 'templating package.json.hbs ✔', - 'copying tsconfig.json ✔', - 'templating index.ts.hbs ✔', - 'copying setupTests.ts ✔', + 'copying .eslintrc.js', + 'templating README.md.hbs', + 'templating package.json.hbs', + 'copying tsconfig.json', + 'templating index.ts.hbs', + 'copying setupTests.ts', 'Installing:', - 'moving plugins/test-common ✔', + 'moving plugins/test-common', ]); await expect( diff --git a/packages/cli/src/lib/create/factories/scaffolderModule.test.ts b/packages/cli/src/lib/create/factories/scaffolderModule.test.ts index dde711b750..a5f1f8e63a 100644 --- a/packages/cli/src/lib/create/factories/scaffolderModule.test.ts +++ b/packages/cli/src/lib/create/factories/scaffolderModule.test.ts @@ -70,20 +70,20 @@ describe('scaffolderModule factory', () => { '', 'Creating module backstage-plugin-scaffolder-backend-module-test', 'Checking Prerequisites:', - 'availability plugins/scaffolder-backend-module-test ✔', - 'creating temp dir ✔', + 'availability plugins/scaffolder-backend-module-test', + 'creating temp dir', 'Executing Template:', - 'copying .eslintrc.js ✔', - 'templating README.md.hbs ✔', - 'templating package.json.hbs ✔', - 'copying tsconfig.json ✔', - 'templating index.ts.hbs ✔', - 'copying index.ts ✔', - 'copying example.test.ts ✔', - 'copying example.ts ✔', - 'copying index.ts ✔', + 'copying .eslintrc.js', + 'templating README.md.hbs', + 'templating package.json.hbs', + 'copying tsconfig.json', + 'templating index.ts.hbs', + 'copying index.ts', + 'copying example.test.ts', + 'copying example.ts', + 'copying index.ts', 'Installing:', - 'moving plugins/scaffolder-backend-module-test ✔', + 'moving plugins/scaffolder-backend-module-test', ]); await expect( From 6f279f9b8eebf4cea506baea2c6bca733de290c6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Nov 2021 01:52:56 +0100 Subject: [PATCH 41/42] cli: fix failing create tests on windows Signed-off-by: Patrik Oldsberg --- .../src/lib/create/factories/backendPlugin.test.ts | 9 +++++---- .../src/lib/create/factories/common/tasks.test.ts | 5 +++-- .../cli/src/lib/create/factories/common/tasks.ts | 4 ++-- .../src/lib/create/factories/frontendPlugin.test.ts | 13 +++++++------ .../src/lib/create/factories/pluginCommon.test.ts | 9 +++++---- .../lib/create/factories/scaffolderModule.test.ts | 9 +++++---- 6 files changed, 27 insertions(+), 22 deletions(-) diff --git a/packages/cli/src/lib/create/factories/backendPlugin.test.ts b/packages/cli/src/lib/create/factories/backendPlugin.test.ts index 916ba7187c..6f4a73c02d 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.test.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; +import { sep, resolve as resolvePath } from 'path'; import { paths } from '../../paths'; import { Task } from '../../tasks'; import { FactoryRegistry } from '../FactoryRegistry'; @@ -75,7 +76,7 @@ describe('backendPlugin factory', () => { '', 'Creating backend plugin backstage-plugin-test-backend', 'Checking Prerequisites:', - 'availability plugins/test-backend', + `availability plugins${sep}test-backend`, 'creating temp dir', 'Executing Template:', 'copying .eslintrc.js', @@ -89,7 +90,7 @@ describe('backendPlugin factory', () => { 'copying router.ts', 'templating standaloneServer.ts.hbs', 'Installing:', - 'moving plugins/test-backend', + `moving plugins${sep}test-backend`, 'backend adding dependency', ]); @@ -103,11 +104,11 @@ describe('backendPlugin factory', () => { expect(Task.forCommand).toHaveBeenCalledTimes(2); expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: '/root/plugins/test-backend', + cwd: resolvePath('/root/plugins/test-backend'), optional: true, }); expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: '/root/plugins/test-backend', + cwd: resolvePath('/root/plugins/test-backend'), optional: true, }); }); diff --git a/packages/cli/src/lib/create/factories/common/tasks.test.ts b/packages/cli/src/lib/create/factories/common/tasks.test.ts index 0892322e61..49381676e7 100644 --- a/packages/cli/src/lib/create/factories/common/tasks.test.ts +++ b/packages/cli/src/lib/create/factories/common/tasks.test.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; +import { sep } from 'path'; import { createMockOutputStream, mockPaths } from './testUtils'; import { CreateContext } from '../../types'; import { executePluginPackageTemplate } from './tasks'; @@ -88,14 +89,14 @@ some-package@^1.1.0: expect(modified).toBe(true); expect(output).toEqual([ 'Checking Prerequisites:', - 'availability /target', + `availability ..${sep}target`, 'creating temp dir', 'Executing Template:', 'templating package.json.hbs', 'copying not-templated.txt', 'templating templated.txt.hbs', 'Installing:', - 'moving /target', + `moving ..${sep}target`, ]); await expect(fs.readFile('/target/package.json', 'utf8')).resolves.toBe(`{ "name": "my-testing-plugin", diff --git a/packages/cli/src/lib/create/factories/common/tasks.ts b/packages/cli/src/lib/create/factories/common/tasks.ts index fd3bd4d930..93af1e2b00 100644 --- a/packages/cli/src/lib/create/factories/common/tasks.ts +++ b/packages/cli/src/lib/create/factories/common/tasks.ts @@ -16,7 +16,7 @@ import fs from 'fs-extra'; import chalk from 'chalk'; -import { resolve as resolvePath } from 'path'; +import { resolve as resolvePath, relative as relativePath } from 'path'; import { paths } from '../../../paths'; import { Task, templatingTask } from '../../../tasks'; import { Lockfile } from '../../../versioning'; @@ -41,7 +41,7 @@ export async function executePluginPackageTemplate( } Task.section('Checking Prerequisites'); - const shortPluginDir = targetDir.replace(`${paths.targetRoot}/`, ''); + const shortPluginDir = relativePath(paths.targetRoot, targetDir); await Task.forItem('availability', shortPluginDir, async () => { if (await fs.pathExists(targetDir)) { throw new Error( diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts index 439b134478..27ee14ea2c 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; +import { sep, resolve as resolvePath } from 'path'; import { paths } from '../../paths'; import { Task } from '../../tasks'; import { FactoryRegistry } from '../FactoryRegistry'; @@ -88,7 +89,7 @@ describe('frontendPlugin factory', () => { '', 'Creating backend plugin backstage-plugin-test', 'Checking Prerequisites:', - 'availability plugins/test', + `availability plugins${sep}test`, 'creating temp dir', 'Executing Template:', 'copying .eslintrc.js', @@ -108,7 +109,7 @@ describe('frontendPlugin factory', () => { 'templating ExampleFetchComponent.tsx.hbs', 'copying index.ts', 'Installing:', - 'moving plugins/test', + `moving plugins${sep}test`, 'app adding dependency', 'app adding import', ]); @@ -136,11 +137,11 @@ const router = ( expect(Task.forCommand).toHaveBeenCalledTimes(2); expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: '/root/plugins/test', + cwd: resolvePath('/root/plugins/test'), optional: true, }); expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: '/root/plugins/test', + cwd: resolvePath('/root/plugins/test'), optional: true, }); }); @@ -205,11 +206,11 @@ const router = ( expect(Task.forCommand).toHaveBeenCalledTimes(2); expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: '/root/plugins/test', + cwd: resolvePath('/root/plugins/test'), optional: true, }); expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: '/root/plugins/test', + cwd: resolvePath('/root/plugins/test'), optional: true, }); }); diff --git a/packages/cli/src/lib/create/factories/pluginCommon.test.ts b/packages/cli/src/lib/create/factories/pluginCommon.test.ts index 3124df6b1d..602fce7bbd 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.test.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.test.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; +import { sep, resolve as resolvePath } from 'path'; import { paths } from '../../paths'; import { Task } from '../../tasks'; import { FactoryRegistry } from '../FactoryRegistry'; @@ -70,7 +71,7 @@ describe('pluginCommon factory', () => { '', 'Creating backend plugin backstage-plugin-test-common', 'Checking Prerequisites:', - 'availability plugins/test-common', + `availability plugins${sep}test-common`, 'creating temp dir', 'Executing Template:', 'copying .eslintrc.js', @@ -80,7 +81,7 @@ describe('pluginCommon factory', () => { 'templating index.ts.hbs', 'copying setupTests.ts', 'Installing:', - 'moving plugins/test-common', + `moving plugins${sep}test-common`, ]); await expect( @@ -96,11 +97,11 @@ describe('pluginCommon factory', () => { expect(Task.forCommand).toHaveBeenCalledTimes(2); expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: '/root/plugins/test-common', + cwd: resolvePath('/root/plugins/test-common'), optional: true, }); expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: '/root/plugins/test-common', + cwd: resolvePath('/root/plugins/test-common'), optional: true, }); }); diff --git a/packages/cli/src/lib/create/factories/scaffolderModule.test.ts b/packages/cli/src/lib/create/factories/scaffolderModule.test.ts index a5f1f8e63a..c60c58e619 100644 --- a/packages/cli/src/lib/create/factories/scaffolderModule.test.ts +++ b/packages/cli/src/lib/create/factories/scaffolderModule.test.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; +import { sep, resolve as resolvePath } from 'path'; import { paths } from '../../paths'; import { Task } from '../../tasks'; import { FactoryRegistry } from '../FactoryRegistry'; @@ -70,7 +71,7 @@ describe('scaffolderModule factory', () => { '', 'Creating module backstage-plugin-scaffolder-backend-module-test', 'Checking Prerequisites:', - 'availability plugins/scaffolder-backend-module-test', + `availability plugins${sep}scaffolder-backend-module-test`, 'creating temp dir', 'Executing Template:', 'copying .eslintrc.js', @@ -83,7 +84,7 @@ describe('scaffolderModule factory', () => { 'copying example.ts', 'copying index.ts', 'Installing:', - 'moving plugins/scaffolder-backend-module-test', + `moving plugins${sep}scaffolder-backend-module-test`, ]); await expect( @@ -99,11 +100,11 @@ describe('scaffolderModule factory', () => { expect(Task.forCommand).toHaveBeenCalledTimes(2); expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: '/root/plugins/scaffolder-backend-module-test', + cwd: resolvePath('/root/plugins/scaffolder-backend-module-test'), optional: true, }); expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: '/root/plugins/scaffolder-backend-module-test', + cwd: resolvePath('/root/plugins/scaffolder-backend-module-test'), optional: true, }); }); From 6c1348591e513137442f6d539fae3d905f72e442 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Nov 2021 04:22:02 +0000 Subject: [PATCH 42/42] build(deps-dev): bump prettier from 2.4.0 to 2.4.1 Bumps [prettier](https://github.com/prettier/prettier) from 2.4.0 to 2.4.1. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/2.4.0...2.4.1) --- updated-dependencies: - dependency-name: prettier dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 37c1bd3329..9bff375969 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23142,9 +23142,9 @@ prettier@^1.19.1: integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== prettier@^2.2.1: - version "2.4.0" - resolved "https://registry.npmjs.org/prettier/-/prettier-2.4.0.tgz#85bdfe0f70c3e777cf13a4ffff39713ca6f64cba" - integrity sha512-DsEPLY1dE5HF3BxCRBmD4uYZ+5DCbvatnolqTqcxEgKVZnL2kUfyu7b8pPQ5+hTBkdhU9SLUmK0/pHb07RE4WQ== + version "2.4.1" + resolved "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c" + integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA== prettier@~2.2.1: version "2.2.1"