diff --git a/.changeset/polite-pumas-joke.md b/.changeset/polite-pumas-joke.md new file mode 100644 index 0000000000..9b026abc0e --- /dev/null +++ b/.changeset/polite-pumas-joke.md @@ -0,0 +1,18 @@ +--- +'@backstage/cli': minor +--- + +The `new` command is now powered by a new template system that allows you to define your own templates in a declarative way, as well as import existing templates from external sources. See the [CLI templates documentation](https://backstage.io/docs/tooling/cli/templates) for more information. + +The following flags for the `new` command have been deprecated and will be removed in a future release: + +- `--license=`: Configure the global `license` instead. +- `--no-private`: Configure the global `private` instead. +- `--baseVersion=`: Configure the global `version` instead. +- `--npmRegistry=`: Configure the global `publishRegistry` instead. +- `--scope=`: Configure the global `namePrefix` and/or `namePluginInfix` instead. + +As part of this change the template IDs and their options have changed. The following backwards compatibility mappings for the `--select` and `--option` flags are enabled when using the default set of templates, but they will also be removed in the future: + +- `--select=plugin` is mapped to `--select=frontend-plugin` instead. +- `--option=id=` is mapped to `--option=pluginId=` instead. diff --git a/docs/features/software-catalog/extending-the-model--old.md b/docs/features/software-catalog/extending-the-model--old.md index 7368a4a339..668d1d8311 100644 --- a/docs/features/software-catalog/extending-the-model--old.md +++ b/docs/features/software-catalog/extending-the-model--old.md @@ -495,8 +495,8 @@ want to have an isomorphic package that houses these types. Within the Backstage main repo the package naming pattern of `-common` is used for isomorphic packages, and you may choose to adopt this pattern as well. -You can generate an isomorphic plugin package by running:`yarn new --select plugin-common` -or you can run `yarn new` and then select "plugin-common" from the list of options +You can generate an isomorphic plugin package by running: `yarn new` and then +select "plugin-common" from the list of options There's at this point no existing templates for generating isomorphic plugins using the `@backstage/cli`. Perhaps the simplest way to get started right now is diff --git a/docs/features/software-catalog/extending-the-model.md b/docs/features/software-catalog/extending-the-model.md index f9d20d03c2..d289886a09 100644 --- a/docs/features/software-catalog/extending-the-model.md +++ b/docs/features/software-catalog/extending-the-model.md @@ -495,8 +495,8 @@ want to have an isomorphic package that houses these types. Within the Backstage main repo the package naming pattern of `-common` is used for isomorphic packages, and you may choose to adopt this pattern as well. -You can generate an isomorphic plugin package by running:`yarn new --select plugin-common` -or you can run `yarn new` and then select "plugin-common" from the list of options +You can generate an isomorphic plugin package by running: `yarn new` and then +selecting "plugin-common" from the list of options There's at this point no existing templates for generating isomorphic plugins using the `@backstage/cli`. Perhaps the simplest way to get started right now is @@ -520,7 +520,7 @@ validate entities of our new kind. Just like with the definition package, you can find inspiration in for example the existing [ScaffolderEntitiesProcessor](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-scaffolder-entity-model/src/processor/ScaffolderEntitiesProcessor.ts). -The custom processor should be created as a separate module for the catalog plugin. For information on how to set that up, see the [plugin docs](../../plugins/backend-plugin.md#creating-a-backend-plugin). Use `yarn new --select backend-module` instead to create a module. For our case, the module ID will be `foobar` and the plugin ID will be `catalog`. +The custom processor should be created as a separate module for the catalog plugin. For information on how to set that up, see the [plugin docs](../../plugins/backend-plugin.md#creating-a-backend-plugin). Use `yarn new` and select `backend-module` instead to create a module. For our case, the module ID will be `foobar` and the plugin ID will be `catalog`. We also provide a high-level example of what a catalog process for a custom entity might look like: diff --git a/docs/features/software-catalog/external-integrations.md b/docs/features/software-catalog/external-integrations.md index f721c50ec2..6b57922166 100644 --- a/docs/features/software-catalog/external-integrations.md +++ b/docs/features/software-catalog/external-integrations.md @@ -72,7 +72,7 @@ putting all extensions like this in a backend module package of their own in the `plugins` folder of your Backstage repo: ```sh -yarn new --select backend-module --option id=catalog +yarn new --select backend-module --option pluginId=catalog ``` The class will have this basic structure: @@ -650,7 +650,7 @@ putting all extensions like this in a backend module package of their own in the `plugins` folder of your Backstage repo: ```sh -yarn new --select backend-module --option id=catalog +yarn new --select backend-module --option pluginId=catalog ``` The class will have this basic structure: diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index 1366a59099..bfd210aeb9 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -10,18 +10,12 @@ Backstage repository. ## Creating a Backend Plugin A new, bare-bones backend plugin package can be created by issuing the following -command in your Backstage repository root: +command in your Backstage repository root and selecting `backend-plugin`: ```sh -yarn new --select backend-plugin +yarn new ``` -Please also see the `--help` flag for the `new` command for some -further options that are available, notably the `--scope` and `--no-private` -flags that control naming and publishing of the newly created package. Your repo -root `package.json` will probably also have some default values already set up -for these. - You will be asked to supply a name for the plugin. This is an identifier that will be part of the NPM package name, so make it short and containing only lowercase characters separated by dashes, for example `carmen`, if it's a diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md index d80e9cd57e..3810495707 100644 --- a/docs/plugins/create-a-plugin.md +++ b/docs/plugins/create-a-plugin.md @@ -15,9 +15,11 @@ invoking the from the root of your project. ```bash -yarn new --select plugin +yarn new ``` +And then select `frontend-plugin`. + ![Example of output when creating a new plugin](../assets/getting-started/create-plugin_output.png) This will create a new Backstage Plugin based on the ID that was provided. It diff --git a/docs/plugins/integrating-plugin-into-software-catalog.md b/docs/plugins/integrating-plugin-into-software-catalog.md index d47be7f4c0..ed2e25c601 100644 --- a/docs/plugins/integrating-plugin-into-software-catalog.md +++ b/docs/plugins/integrating-plugin-into-software-catalog.md @@ -21,7 +21,8 @@ should have a separate package in a folder, which represents your plugin. Example: ``` -$ yarn new --select plugin +$ yarn new +# Select `frontend-plugin` > ? Enter an ID for the plugin [required] my-plugin > ? Enter the owner(s) of the plugin. If specified, this will be added to CODEOWNERS for the plugin path. [optional] diff --git a/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index 2da7e11ef4..9d8dd1543e 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -258,30 +258,28 @@ it is possible to pre-select what you want to create using the `--select` flag, and provide options using `--option`, for example: ```bash -backstage-cli new --select plugin --option id=foo +backstage-cli new --select plugin --option pluginId=foo ``` This command is typically added as script in the root `package.json` to be -executed with `yarn new`, using options that are appropriate for the organization -that owns the app repo. For example you may have it set up like this: +executed with `yarn new`. For example you may have it set up like this: ```json { "scripts": { - "new": "backstage-cli new --scope internal --no-private --npm-registry https://acme.org/npm" + "new": "backstage-cli new" } } ``` +The `new` command comes with a default collection of plugins/packages, however, +you can customize this list and even create your own CLI templates. For more +information see [CLI Templates](./04-templates.md). + ```text -Usage: backstage-cli create [options] +Usage: backstage-cli new 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 ``` diff --git a/docs/tooling/cli/04-templates.md b/docs/tooling/cli/04-templates.md new file mode 100644 index 0000000000..ad1c9207ba --- /dev/null +++ b/docs/tooling/cli/04-templates.md @@ -0,0 +1,149 @@ +--- +id: templates +title: CLI Templates +description: Overview of the new CLI Declarative Templates +--- + +The behavior of the `backstage-cli new` command is configurable through your root `package.json`, and you can also create and add custom CLI templates to suit your needs. + +## Basic Configuration + +```json +{ + "name": "root", + "backstage": { + "cli": { + "new": { + "globals": { + "license": "MIT", + "namePrefix": "@my-org/" + } + } + } + } +} +``` + +- `globals` - Configures input for all generated packages and plugins. + - `version` - Sets the value of the `version` field in `package.json` of all generated packages. Defaults to `Apache-2.0`. + - `license` - Sets the value of the `license` field in `package.json` of all generated packages. Defaults to `0.1.0`. + - `private` - Sets the value of the `private` field in `package.json` of all generated packages. Defaults to `true`. + - `publishRegistry` - Sets the value of the `publishConfig.registry` field in `package.json` of all generated packages. + - `namePrefix` - The prefix used to generate the full package name. Defaults to `@internal/`. + - `namePluginInfix` - The infix used to generate the full package name for plugin packages. Defaults to `plugin-`. +- `templates` - Specifies custom templates. + - See [Installing custom templates](#installing-custom-templates) and [Creating your own CLI templates](#creating-your-own-cli-templates) for more information. + +The generated package name is based on the `namePrefix` and `namePluginInfix` globals, as well as the "base name" which is derived from the package role and user input. For plugin packages the final package name will be ``, and for other packages it will be ``. + +For example, if you want your plugin frontend packages to end up with the name `@acme/backstage-plugin-`, you should use the following configuration: + +```json +{ + "name": "root", + "backstage": { + "cli": { + "new": { + "globals": { + "namePrefix": "@acme/", + "namePluginInfix": "backstage-plugin-" + } + } + } + } +} +``` + +## Installing custom templates + +Custom templates can be installed from local directories. To install a template you add it to the `backstage.cli.new.templates` configuration array in your root `package.json`: + +```json +{ + "name": "root", + "backstage": { + "cli": { + "new": { + "templates": ["./templates/custom-plugin"] + } + } + } +} +``` + +Each entry in the `templates` array should be a relative path that points to a directory containing a `portable-template.yaml` file. If the path starts with `./` it will be used as is, otherwise it will be resolved as a module within `node_modules`. + +When defining the `templates` array it will override the default set of templates. If you want to keep using one of the build-in templates in the Backstage CLI you can reference them directly within the CLI package. This following is the full list of built-in templates: + +```json +{ + "name": "root", + "backstage": { + "cli": { + "new": { + "templates": [ + "@backstage/cli/templates/frontend-plugin", + "@backstage/cli/templates/backend-plugin", + "@backstage/cli/templates/backend-plugin-module", + "@backstage/cli/templates/plugin-web-library", + "@backstage/cli/templates/plugin-node-library", + "@backstage/cli/templates/plugin-common-library", + "@backstage/cli/templates/web-library", + "@backstage/cli/templates/node-library", + "@backstage/cli/templates/scaffolder-backend-module" + ] + } + } + } +} +``` + +## Creating your own CLI templates + +Each template lives in its own directory and must have a `portable-template.yaml` file that describes the template. The template directory can also contain any files that should be templated or copied to the generated package. + +Start by creating `portable-template.yaml` in a new directory somewhere in your project, in this example we're using `./templates/custom-plugin/portable-template.yaml`: + +```yaml title="in templates/custom-plugin/portable-template.yaml" +name: custom-plugin +role: frontend-plugin +description: Description of my CLI template # optional +values: # optional + pluginVar: '{{ camelCase pluginId }}Plugin' +``` + +The following properties are supported: + +- `name` **(required)** - The name of your template, used by the user to select it. +- `role` **(required)** - The role of the template, similar to package role. See [Template Roles](#template-roles) for more details. +- `description` - A description of the type of package that this template produces. +- `values` - A map of additional values that will be present during templating. The values are themselves templated and can reference other values. If the key matches any of the user prompts, such as `pluginId`, the value will be used directly instead of prompting the user. + +Next, add any other files you want to be part of the template to the same directory. All files will be copied as is, except any files with a `.hbs` extension. They will be treated as [Handlebars](https://handlebarsjs.com/) templates and will be rendered with the values from the `portable-template.yaml` file as well as additional prompts such as `pluginId`. For example, you could create a `src/index.ts` file with the following content: + +```typescript title="in templates/custom-plugin/src/index.ts.hbs" +export function getPluginId() { + return '{{ pluginId }}'; +} +``` + +If you'd like to see more examples, you can find all the default templates and their yaml files [here](https://github.com/backstage/backstage/tree/master/packages/cli/templates). + +Once your template is ready, [add it to your config](#installing-custom-templates), and you should now be able to select it when running `yarn new`. + +### Template Roles + +The `role` property in the template yaml file is used to determine what input will be gathered for the template, as well as what actions will be taken after the new package has been created. The following roles are supported: + +| Role | Prompts | Output Directory | Additional Actions | +| :----------------------- | :--------------------- | :--------------- | :-------------------------------------------------------------------------------- | +| `frontend-plugin` | `pluginId` | `plugins` | Add dependency to `packages/app` and entry to `packages/backend/src/App.tsx` | +| `frontend-plugin-module` | `pluginId`, `moduleId` | `plugins` | Add dependency to `packages/app` | +| `backend-plugin` | `pluginId` | `plugins` | Add dependency to `packages/backend` and entry to `packages/backend/src/index.ts` | +| `backend-plugin-module` | `pluginId`, `moduleId` | `plugins` | Add dependency to `packages/backend` and entry to `packages/backend/src/index.ts` | +| `web-library` | `name` | `packages` | none | +| `node-library` | `name` | `packages` | none | +| `common-library` | `name` | `packages` | none | +| `plugin-web-library` | `pluginId` | `plugins` | none | +| `plugin-node-library` | `pluginId` | `plugins` | none | +| `plugin-common-library` | `pluginId` | `plugins` | none | diff --git a/docs/tutorials/quickstart-app-plugin.md b/docs/tutorials/quickstart-app-plugin.md index a1b30829b3..e02d2f546f 100644 --- a/docs/tutorials/quickstart-app-plugin.md +++ b/docs/tutorials/quickstart-app-plugin.md @@ -30,7 +30,7 @@ title: Adding Custom Plugin to Existing Monorepo App # The Skeleton Plugin 1. Start by using the built-in creator. From the terminal and root of your - project run: `yarn new --select plugin` + project run: `yarn new` and select `frontend-plugin`. 1. Enter a plugin ID. I used `github-playground` 1. When the process finishes, let's start the backend: `yarn --cwd packages/backend start` diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index e07ff68c18..533c7176a2 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -242,6 +242,10 @@ const config: Config = { from: '/docs/local-dev/cli-overview/', to: '/docs/tooling/cli/overview/', }, + { + from: '/docs/local-dev/cli-templates/', + to: '/docs/tooling/cli/templates/', + }, { from: '/docs/local-dev/linking-local-packages/', to: '/docs/tooling/local-dev/linking-local-packages', diff --git a/microsite/sidebars.js b/microsite/sidebars.js index 8fbd91a5b1..b9f7a5baa5 100644 --- a/microsite/sidebars.js +++ b/microsite/sidebars.js @@ -479,6 +479,7 @@ module.exports = { 'tooling/cli/overview', 'tooling/cli/build-system', 'tooling/cli/commands', + 'tooling/cli/templates', { type: 'category', label: 'Local Development', diff --git a/package.json b/package.json index af4f1c98c1..65ff47c5a9 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,16 @@ "type": "git", "url": "https://github.com/backstage/backstage" }, + "backstage": { + "cli": { + "new": { + "globals": { + "private": false, + "namePrefix": "@backstage/" + } + } + } + }, "workspaces": { "packages": [ "packages/*", @@ -33,7 +43,7 @@ "lint:docs": "node ./scripts/check-docs-quality", "lint:peer-deps": "backstage-repo-tools peer-deps", "lint:type-deps": "backstage-repo-tools type-deps", - "new": "backstage-cli new --scope backstage --baseVersion 0.0.0 --no-private", + "new": "backstage-cli new", "prepare": "husky", "prettier:check": "prettier --check .", "prettier:fix": "prettier --write .", diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index d9fec4db22..41b1ddab73 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -179,6 +179,7 @@ Usage: backstage-cli new [options] Options: --select --option = + --skip-install --scope --npm-registry --baseVersion diff --git a/packages/cli/package.json b/packages/cli/package.json index fc211f237a..121d48d497 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -159,7 +159,8 @@ "yargs": "^16.2.0", "yml-loader": "^2.1.0", "yn": "^4.0.0", - "zod": "^3.22.4" + "zod": "^3.22.4", + "zod-validation-error": "^3.4.0" }, "devDependencies": { "@backstage/backend-plugin-api": "workspace:^", diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index f1119053ec..47c362988c 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -257,6 +257,10 @@ export function registerCommands(program: Command) { (opt, arr: string[]) => [...arr, opt], [], ) + .option( + '--skip-install', + `Skips running 'yarn install' and 'yarn lint --fix'`, + ) .option('--scope ', 'The scope to use for new packages') .option( '--npm-registry ', diff --git a/packages/cli/src/commands/new/new.ts b/packages/cli/src/commands/new/new.ts index 2733b0be9b..5c06555905 100644 --- a/packages/cli/src/commands/new/new.ts +++ b/packages/cli/src/commands/new/new.ts @@ -14,17 +14,66 @@ * limitations under the License. */ -import os from 'os'; -import fs from 'fs-extra'; -import { join as joinPath } from 'path'; -import { OptionValues } from 'commander'; -import { FactoryRegistry } from '../../lib/new/FactoryRegistry'; -import { isMonoRepo } from '@backstage/cli-node'; -import { paths } from '../../lib/paths'; -import { assertError } from '@backstage/errors'; -import { Task } from '../../lib/tasks'; +import { createNewPackage } from '../../lib/new/createNewPackage'; -function parseOptions(optionStrings: string[]): Record { +type ArgOptions = { + option: string[]; + select?: string; + skipInstall: boolean; + private?: boolean; + npmRegistry?: string; + scope?: string; + license?: string; + baseVersion?: string; +}; + +export default async (opts: ArgOptions) => { + const { + option: rawArgOptions, + select: preselectedTemplateId, + skipInstall, + scope, + private: isPrivate, + ...otherGlobals + } = opts; + + const prefilledParams = parseParams(rawArgOptions); + + let pluginInfix: string | undefined = undefined; + let packagePrefix: string | undefined = undefined; + if (scope) { + const backstagePrefix = scope.startsWith('backstage') ? '' : 'backstage-'; + packagePrefix = scope.includes('/') + ? `@${scope}${backstagePrefix}` + : `@${scope}/${backstagePrefix}`; + pluginInfix = scope.includes('backstage') ? 'plugin-' : 'backstage-plugin-'; + } + + if ( + isPrivate === false || // set to false with --no-private flag + Object.values(otherGlobals).filter(Boolean).length !== 0 + ) { + console.warn( + `Global template configuration via CLI flags is deprecated, see https://backstage.io/docs/cli/new for information on how to configure package templating`, + ); + } + + await createNewPackage({ + prefilledParams, + preselectedTemplateId, + configOverrides: { + license: otherGlobals.license, + version: otherGlobals.baseVersion, + private: isPrivate, + publishRegistry: otherGlobals.npmRegistry, + packageNamePrefix: packagePrefix, + packageNamePluginInfix: pluginInfix, + }, + skipInstall, + }); +}; + +function parseParams(optionStrings: string[]): Record { const options: Record = {}; for (const str of optionStrings) { @@ -40,81 +89,3 @@ function parseOptions(optionStrings: string[]): Record { return options; } - -export default async (opts: OptionValues) => { - const factory = await FactoryRegistry.interactiveSelect(opts.select); - - const providedOptions = parseOptions(opts.option); - const options = await FactoryRegistry.populateOptions( - factory, - providedOptions, - ); - - let defaultVersion = '0.1.0'; - if (opts.baseVersion) { - defaultVersion = opts.baseVersion; - } else { - const lernaVersion = await fs - .readJson(paths.resolveTargetRoot('lerna.json')) - .then(pkg => pkg.version) - .catch(() => undefined); - if (lernaVersion) { - defaultVersion = lernaVersion; - } - } - - const tempDirs = new Array(); - async function createTemporaryDirectory(name: string): Promise { - const dir = await fs.mkdtemp(joinPath(os.tmpdir(), name)); - tempDirs.push(dir); - return dir; - } - - const license = opts.license ?? 'Apache-2.0'; - - let modified = false; - try { - await factory.create(options, { - isMonoRepo: await isMonoRepo(), - defaultVersion, - license, - scope: opts.scope?.replace(/^@/, ''), - npmRegistry: opts.npmRegistry, - private: Boolean(opts.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 { - await fs.remove(dir); - } catch (error) { - console.error( - `Failed to remove temporary directory '${dir}', ${error}`, - ); - } - } - } -}; diff --git a/packages/cli/src/commands/versions/migrate.test.ts b/packages/cli/src/commands/versions/migrate.test.ts index 043e1893a4..919eb18278 100644 --- a/packages/cli/src/commands/versions/migrate.test.ts +++ b/packages/cli/src/commands/versions/migrate.test.ts @@ -21,7 +21,6 @@ import * as run from '../../lib/run'; import migrate from './migrate'; import { withLogCollector } from '@backstage/test-utils'; import fs from 'fs-extra'; -import { expectLogsToMatch } from '../../lib/new/factories/common/testUtils'; // Remove log coloring to simplify log matching jest.mock('chalk', () => ({ @@ -52,6 +51,10 @@ jest.mock('../../lib/run', () => { }; }); +function expectLogsToMatch(recievedLogs: String[], expected: String[]): void { + expect(recievedLogs.filter(Boolean).sort()).toEqual(expected.sort()); +} + describe('versions:migrate', () => { mockDir = createMockDirectory(); diff --git a/packages/cli/src/lib/new/FactoryRegistry.ts b/packages/cli/src/lib/new/FactoryRegistry.ts deleted file mode 100644 index d16aa0615a..0000000000 --- a/packages/cli/src/lib/new/FactoryRegistry.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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 inquirer, { Answers } from 'inquirer'; -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]), - ); - - 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> { - let currentOptions = provided; - - if (factory.optionsDiscovery) { - const discoveredOptions = await factory.optionsDiscovery(); - currentOptions = { - ...currentOptions, - ...(discoveredOptions as Record), - }; - } - - if (factory.optionsPrompts) { - const [hasAnswers, needsAnswers] = partition( - factory.optionsPrompts, - option => option.name in currentOptions, - ); - - 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.map(option => - applyPromptMessageTransforms(option, { - message: chalk.blue, - error: chalk.red, - }), - ), - currentOptions, - ); - } - - return currentOptions; - } -} diff --git a/packages/cli/src/lib/new/createNewPackage.ts b/packages/cli/src/lib/new/createNewPackage.ts new file mode 100644 index 0000000000..0e3d7e554e --- /dev/null +++ b/packages/cli/src/lib/new/createNewPackage.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2025 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 { + collectPortableTemplateInput, + loadPortableTemplate, + loadPortableTemplateConfig, + selectTemplateInteractively, +} from './preparation'; +import { executePortableTemplate } from './execution'; +import { PortableTemplateConfig, PortableTemplateParams } from './types'; + +export type CreateNewPackageOptions = { + preselectedTemplateId?: string; + configOverrides: Partial; + prefilledParams: PortableTemplateParams; + skipInstall?: boolean; +}; + +export async function createNewPackage(options: CreateNewPackageOptions) { + const config = await loadPortableTemplateConfig({ + overrides: options.configOverrides, + }); + + const selectedTemplate = await selectTemplateInteractively( + config, + options.preselectedTemplateId, + ); + const template = await loadPortableTemplate(selectedTemplate); + + const input = await collectPortableTemplateInput({ + config, + template, + prefilledParams: options.prefilledParams, + }); + + await executePortableTemplate({ + config, + template, + input, + skipInstall: options.skipInstall, + }); +} diff --git a/packages/cli/src/lib/new/defaultTemplates.ts b/packages/cli/src/lib/new/defaultTemplates.ts new file mode 100644 index 0000000000..1a2f7a89d4 --- /dev/null +++ b/packages/cli/src/lib/new/defaultTemplates.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2025 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 const defaultTemplates = [ + '@backstage/cli/templates/frontend-plugin', + '@backstage/cli/templates/backend-plugin', + '@backstage/cli/templates/backend-plugin-module', + '@backstage/cli/templates/plugin-web-library', + '@backstage/cli/templates/plugin-node-library', + '@backstage/cli/templates/plugin-common-library', + '@backstage/cli/templates/web-library', + '@backstage/cli/templates/node-library', + '@backstage/cli/templates/scaffolder-backend-module', +]; diff --git a/packages/cli/src/lib/new/execution/PortableTemplater.ts b/packages/cli/src/lib/new/execution/PortableTemplater.ts new file mode 100644 index 0000000000..8e5e9251d4 --- /dev/null +++ b/packages/cli/src/lib/new/execution/PortableTemplater.ts @@ -0,0 +1,107 @@ +/* + * Copyright 2025 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 handlebars from 'handlebars'; +import { PortableTemplateParams } from '../types'; +import camelCase from 'lodash/camelCase'; +import kebabCase from 'lodash/kebabCase'; +import lowerCase from 'lodash/lowerCase'; +import snakeCase from 'lodash/snakeCase'; +import startCase from 'lodash/startCase'; +import upperCase from 'lodash/upperCase'; +import upperFirst from 'lodash/upperFirst'; +import lowerFirst from 'lodash/lowerFirst'; +import { Lockfile } from '../../versioning'; +import { paths } from '../../paths'; +import { createPackageVersionProvider } from '../../version'; + +const builtInHelpers = { + camelCase, + kebabCase, + lowerCase, + snakeCase, + startCase, + upperCase, + upperFirst, + lowerFirst, +}; + +type CreatePortableTemplaterOptions = { + values?: PortableTemplateParams; + templatedValues?: Record; +}; + +export class PortableTemplater { + static async create(options: CreatePortableTemplaterOptions = {}) { + let lockfile: Lockfile | undefined; + try { + lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + } catch { + /* ignored */ + } + + const versionProvider = createPackageVersionProvider(lockfile); + + const templater = new PortableTemplater( + { + versionQuery(name: string, versionHint: string | unknown) { + return versionProvider( + name, + typeof versionHint === 'string' ? versionHint : undefined, + ); + }, + }, + options.values ?? {}, + ); + + if (options.templatedValues) { + templater.appendTemplatedValues(options.templatedValues); + } + + return templater; + } + + readonly #templater: typeof handlebars; + #values: PortableTemplateParams; + + private constructor( + helpers: handlebars.HelperDeclareSpec, + values: PortableTemplateParams, + ) { + this.#templater = handlebars.create(); + + this.#templater.registerHelper(builtInHelpers); + + if (helpers) { + this.#templater.registerHelper(helpers); + } + + this.#values = values; + } + + template(content: string): string { + return this.#templater.compile(content, { + strict: true, + })(this.#values); + } + + appendTemplatedValues(record: Record): void { + const newValues = Object.fromEntries( + Object.entries(record).map(([key, value]) => [key, this.template(value)]), + ); + this.#values = { ...this.#values, ...newValues }; + } +} diff --git a/packages/cli/src/lib/new/execution/executePortableTemplate.ts b/packages/cli/src/lib/new/execution/executePortableTemplate.ts new file mode 100644 index 0000000000..0041b931ba --- /dev/null +++ b/packages/cli/src/lib/new/execution/executePortableTemplate.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2025 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 { assertError } from '@backstage/errors'; +import { addCodeownersEntry } from '../../codeowners'; +import { Task } from '../../tasks'; +import { + PortableTemplate, + PortableTemplateConfig, + PortableTemplateInput, +} from '../types'; +import { installNewPackage } from './installNewPackage'; +import { writeTemplateContents } from './writeTemplateContents'; + +type ExecuteNewTemplateOptions = { + config: PortableTemplateConfig; + template: PortableTemplate; + input: PortableTemplateInput; + skipInstall?: boolean; +}; + +export async function executePortableTemplate( + options: ExecuteNewTemplateOptions, +) { + const { template, input } = options; + + let modified = false; + try { + const { targetDir } = await Task.forItem( + 'templating', + input.packagePath, + () => writeTemplateContents(template, input), + ); + + modified = true; + + await installNewPackage(input); + + if (input.owner) { + await addCodeownersEntry(targetDir, input.owner); + } + + if (!options.skipInstall) { + await Task.forCommand('yarn install', { + cwd: targetDir, + optional: true, + }); + await Task.forCommand('yarn lint --fix', { + cwd: targetDir, + optional: true, + }); + } + + Task.log(); + Task.log(`🎉 Successfully created ${template.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 ${template.name}!`); + } + } +} diff --git a/packages/cli/src/lib/new/factories/index.ts b/packages/cli/src/lib/new/execution/index.ts similarity index 53% rename from packages/cli/src/lib/new/factories/index.ts rename to packages/cli/src/lib/new/execution/index.ts index 44d687eda2..3b8ed3c090 100644 --- a/packages/cli/src/lib/new/factories/index.ts +++ b/packages/cli/src/lib/new/execution/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * Copyright 2025 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. @@ -14,12 +14,4 @@ * limitations under the License. */ -export { frontendPlugin } from './frontendPlugin'; -export { backendPlugin } from './backendPlugin'; -export { backendModule } from './backendModule'; -export { nodeLibraryPackage } from './nodeLibraryPackage'; -export { webLibraryPackage } from './webLibraryPackage'; -export { pluginCommon } from './pluginCommon'; -export { pluginNode } from './pluginNode'; -export { pluginWeb } from './pluginWeb'; -export { scaffolderModule } from './scaffolderModule'; +export { executePortableTemplate } from './executePortableTemplate'; diff --git a/packages/cli/src/lib/new/execution/installNewPackage.ts b/packages/cli/src/lib/new/execution/installNewPackage.ts new file mode 100644 index 0000000000..6ed60541c1 --- /dev/null +++ b/packages/cli/src/lib/new/execution/installNewPackage.ts @@ -0,0 +1,147 @@ +/* + * Copyright 2024 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 upperFirst from 'lodash/upperFirst'; +import camelCase from 'lodash/camelCase'; +import { paths } from '../../paths'; +import { Task } from '../../tasks'; +import { PortableTemplateInput } from '../types'; + +export async function installNewPackage(input: PortableTemplateInput) { + switch (input.roleParams.role) { + case 'web-library': + case 'node-library': + case 'common-library': + case 'plugin-web-library': + case 'plugin-node-library': + case 'plugin-common-library': + return; // No installation action needed for library packages + case 'frontend-plugin': + await addDependency(input, 'package/app/package.json'); + await tryAddFrontendLegacy(input); + return; + case 'frontend-plugin-module': + await addDependency(input, 'package/app/package.json'); + return; + case 'backend-plugin': + await addDependency(input, 'package/backend/package.json'); + await tryAddBackend(input); + return; + case 'backend-plugin-module': + await addDependency(input, 'package/backend/package.json'); + await tryAddBackend(input); + return; + default: + throw new Error( + `Unsupported role ${(input.roleParams as { role: string }).role}`, + ); + } +} + +async function addDependency(input: PortableTemplateInput, path: string) { + const pkgJsonPath = paths.resolveTargetRoot(path); + + const pkgJson = await fs.readJson(pkgJsonPath).catch(error => { + if (error.code === 'ENOENT') { + return undefined; + } + throw error; + }); + if (!pkgJson) { + return; + } + + try { + pkgJson.dependencies = { + ...pkgJson.dependencies, + [input.packageName]: `workspace:^`, + }; + + await fs.writeJson(path, pkgJson, { spaces: 2 }); + } catch (error) { + throw new Error(`Failed to add package dependencies, ${error}`); + } +} + +async function tryAddFrontendLegacy(input: PortableTemplateInput) { + const { roleParams } = input; + if (roleParams.role !== 'frontend-plugin') { + throw new Error( + 'add-frontend-legacy can only be used for frontend plugins', + ); + } + + const appDefinitionPath = paths.resolveTargetRoot('packages/app/src/App.tsx'); + if (!(await fs.pathExists(appDefinitionPath))) { + return; + } + + await Task.forItem('app', 'adding import', async () => { + const content = await fs.readFile(appDefinitionPath, '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 extensionName = upperFirst(`${camelCase(roleParams.pluginId)}Page`); + const importLine = `import { ${extensionName} } from '${input.packageName}';`; + 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(appDefinitionPath, newContent, 'utf8'); + } + }); +} + +async function tryAddBackend(input: PortableTemplateInput) { + const backendIndexPath = paths.resolveTargetRoot( + 'packages/backend/src/index.ts', + ); + if (!(await fs.pathExists(backendIndexPath))) { + return; + } + + await Task.forItem('backend', `adding ${input.packageName}`, async () => { + const content = await fs.readFile(backendIndexPath, 'utf8'); + const lines = content.split('\n'); + const backendAddLine = `backend.add(import('${input.packageName}'));`; + + const backendStartIndex = lines.findIndex(line => + line.match(/backend.start/), + ); + + if (backendStartIndex !== -1) { + const [indentation] = lines[backendStartIndex].match(/^\s*/)!; + lines.splice(backendStartIndex, 0, `${indentation}${backendAddLine}`); + + const newContent = lines.join('\n'); + await fs.writeFile(backendIndexPath, newContent, 'utf8'); + } + }); +} diff --git a/packages/cli/src/lib/new/execution/writeTemplateContents.test.ts b/packages/cli/src/lib/new/execution/writeTemplateContents.test.ts new file mode 100644 index 0000000000..7e0e81f137 --- /dev/null +++ b/packages/cli/src/lib/new/execution/writeTemplateContents.test.ts @@ -0,0 +1,97 @@ +/* + * 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 { relative as relativePath } from 'node:path'; +import { writeTemplateContents } from './writeTemplateContents'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { paths } from '../../paths'; + +const baseConfig = { + version: '0.1.0', + license: 'Apache-2.0', + private: true, +}; + +describe('writeTemplateContents', () => { + const mockDir = createMockDirectory(); + + beforeEach(() => { + mockDir.clear(); + jest.resetAllMocks(); + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...args) => mockDir.resolve(...args)); + }); + + it('should write an empty template', async () => { + const { targetDir } = await writeTemplateContents( + { + name: 'test', + files: [], + role: 'frontend-plugin', + values: {}, + }, + { + ...baseConfig, + roleParams: { role: 'frontend-plugin', pluginId: 'test' }, + packageName: '@internal/plugin-test', + packagePath: 'plugins/plugin-test', + }, + ); + + expect(relativePath(mockDir.path, targetDir)).toBe('plugins/plugin-test'); + expect(mockDir.content()).toEqual({}); + }); + + it('should write template with various files', async () => { + await writeTemplateContents( + { + name: 'test', + files: [ + { + content: 'test', + path: 'test.txt', + }, + { + content: 'id={{ pluginId}}', + path: 'plugin.txt', + syntax: 'handlebars', + }, + { + content: '{"x":1}', + path: 'test.json', + }, + ], + role: 'frontend-plugin', + values: {}, + }, + { + ...baseConfig, + roleParams: { role: 'frontend-plugin', pluginId: 'test' }, + packageName: '@internal/plugin-test', + packagePath: 'out', + }, + ); + + expect(mockDir.content()).toEqual({ + out: { + 'test.txt': 'test', + 'plugin.txt': 'id=test', + 'test.json': '{"x":1}', + }, + }); + }); +}); diff --git a/packages/cli/src/lib/new/execution/writeTemplateContents.ts b/packages/cli/src/lib/new/execution/writeTemplateContents.ts new file mode 100644 index 0000000000..f54b2d73d6 --- /dev/null +++ b/packages/cli/src/lib/new/execution/writeTemplateContents.ts @@ -0,0 +1,147 @@ +/* + * 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 { dirname, resolve as resolvePath } from 'path'; + +import { paths } from '../../paths'; +import { PortableTemplate, PortableTemplateInput } from '../types'; +import { ForwardedError, InputError } from '@backstage/errors'; +import { isMonoRepo as getIsMonoRepo } from '@backstage/cli-node'; +import { PortableTemplater } from './PortableTemplater'; + +export async function writeTemplateContents( + template: PortableTemplate, + input: PortableTemplateInput, +): Promise<{ targetDir: string }> { + const targetDir = paths.resolveTargetRoot(input.packagePath); + + if (await fs.pathExists(targetDir)) { + throw new InputError(`Package '${input.packagePath}' already exists`); + } + + try { + const isMonoRepo = await getIsMonoRepo(); + + const { role, ...roleValues } = input.roleParams; + + const templater = await PortableTemplater.create({ + values: { + ...roleValues, + packageName: input.packageName, + }, + templatedValues: template.values, + }); + + if (!isMonoRepo) { + await fs.writeJson( + resolvePath(targetDir, 'tsconfig.json'), + { + extends: '@backstage/cli/config/tsconfig.json', + include: ['src', 'dev', 'migrations'], + exclude: ['node_modules'], + compilerOptions: { + outDir: 'dist-types', + rootDir: '.', + }, + }, + { spaces: 2 }, + ); + } + + for (const file of template.files) { + const destPath = resolvePath(targetDir, file.path); + await fs.ensureDir(dirname(destPath)); + + let content = + file.syntax === 'handlebars' + ? templater.template(file.content) + : file.content; + + // Automatically inject input values into package.json + if (file.path === 'package.json') { + try { + content = injectPackageJsonInput(input, content); + } catch (error) { + throw new ForwardedError( + 'Failed to transform templated package.json', + error, + ); + } + } + + await fs.writeFile(destPath, content).catch(error => { + throw new ForwardedError(`Failed to copy file to ${destPath}`, error); + }); + } + + return { targetDir }; + } catch (error) { + await fs.rm(targetDir, { recursive: true, force: true, maxRetries: 10 }); + throw error; + } +} + +export function injectPackageJsonInput( + input: PortableTemplateInput, + content: string, +) { + const pkgJson = JSON.parse(content); + + const toAdd = new Array<[name: string, value: unknown]>(); + + if (pkgJson.version) { + pkgJson.version = input.version; + } else { + toAdd.push(['version', input.version]); + } + if (pkgJson.license) { + pkgJson.license = input.license; + } else { + toAdd.push(['license', input.license]); + } + if (input.private) { + if (pkgJson.private === false) { + pkgJson.private = true; + } else if (!pkgJson.private) { + toAdd.push(['private', true]); + } + } else { + delete pkgJson.private; + } + + if (input.publishRegistry) { + if (pkgJson.publishConfig) { + pkgJson.publishConfig = { + ...pkgJson.publishConfig, + registry: input.publishRegistry, + }; + } else { + toAdd.push(['publishConfig', { registry: input.publishRegistry }]); + } + } + + const entries = Object.entries(pkgJson); + + const nameIndex = entries.findIndex(([name]) => name === 'name'); + if (nameIndex === -1) { + throw new Error('templated package.json does not contain a name field'); + } + + entries.splice(nameIndex + 1, 0, ...toAdd); + + return JSON.stringify(Object.fromEntries(entries), null, 2); +} diff --git a/packages/cli/src/lib/new/factories/backendModule.test.ts b/packages/cli/src/lib/new/factories/backendModule.test.ts deleted file mode 100644 index 36ed9655a1..0000000000 --- a/packages/cli/src/lib/new/factories/backendModule.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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 { sep } from 'path'; -import { Task } from '../../tasks'; -import { FactoryRegistry } from '../FactoryRegistry'; -import { - createMockOutputStream, - expectLogsToMatch, - mockPaths, -} from './common/testUtils'; -import { backendModule } from './backendModule'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -const backendIndexTsContent = ` -import { createBackend } from '@backstage/backend-defaults'; - -const backend = createBackend(); - -backend.start(); -`; - -describe('backendModule factory', () => { - const mockDir = createMockDirectory(); - - beforeEach(() => { - mockPaths({ - targetRoot: mockDir.path, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should create a backend plugin', async () => { - mockDir.setContent({ - packages: { - backend: { - 'package.json': JSON.stringify({}), - src: { - 'index.ts': backendIndexTsContent, - }, - }, - }, - plugins: {}, - }); - - const options = await FactoryRegistry.populateOptions(backendModule, { - id: 'test', - moduleId: 'tester-two', - }); - - let modified = false; - - const [output, mockStream] = createMockOutputStream(); - jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); - jest.spyOn(Task, 'forCommand').mockResolvedValue(); - - await backendModule.create(options, { - private: true, - isMonoRepo: true, - defaultVersion: '1.0.0', - markAsModified: () => { - modified = true; - }, - createTemporaryDirectory: () => fs.mkdtemp('test'), - license: 'Apache-2.0', - }); - - expect(modified).toBe(true); - - expectLogsToMatch(output, [ - 'Creating backend module backstage-plugin-test-backend-module-tester-two', - 'Checking Prerequisites:', - `availability plugins${sep}test-backend-module-tester-two`, - 'creating temp dir', - 'Executing Template:', - 'templating .eslintrc.js.hbs', - 'templating README.md.hbs', - 'templating package.json.hbs', - 'templating index.ts.hbs', - 'templating module.ts.hbs', - 'Installing:', - `moving plugins${sep}test-backend-module-tester-two`, - 'backend adding dependency', - 'backend adding module', - ]); - - await expect( - fs.readFile(mockDir.resolve('packages/backend/src/index.ts'), 'utf8'), - ).resolves.toBe(` -import { createBackend } from '@backstage/backend-defaults'; - -const backend = createBackend(); - -backend.add(import('backstage-plugin-test-backend-module-tester-two')); -backend.start(); -`); - - await expect( - fs.readJson(mockDir.resolve('packages/backend/package.json')), - ).resolves.toEqual({ - dependencies: { - 'backstage-plugin-test-backend-module-tester-two': '^1.0.0', - }, - }); - const moduleFile = await fs.readFile( - mockDir.resolve('plugins/test-backend-module-tester-two/src/module.ts'), - 'utf-8', - ); - - expect(moduleFile).toContain( - `const testModuleTesterTwo = createBackendModule({`, - ); - expect(moduleFile).toContain(`pluginId: 'test',`); - expect(moduleFile).toContain(`moduleId: 'tester-two',`); - - expect(Task.forCommand).toHaveBeenCalledTimes(2); - expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: mockDir.resolve('plugins/test-backend-module-tester-two'), - optional: true, - }); - expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: mockDir.resolve('plugins/test-backend-module-tester-two'), - optional: true, - }); - }); -}); diff --git a/packages/cli/src/lib/new/factories/backendModule.ts b/packages/cli/src/lib/new/factories/backendModule.ts deleted file mode 100644 index de759538b1..0000000000 --- a/packages/cli/src/lib/new/factories/backendModule.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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 camelCase from 'lodash/camelCase'; -import { paths } from '../../paths'; -import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; -import { CreateContext, createFactory } from '../types'; -import { addPackageDependency, addToBackend, Task } from '../../tasks'; -import { - moduleIdIdPrompt, - ownerPrompt, - pluginIdPrompt, -} from './common/prompts'; -import { executePluginPackageTemplate } from './common/tasks'; -import { resolvePackageName } from './common/util'; - -type Options = { - id: string; - moduleId: string; - owner?: string; - codeOwnersPath?: string; -}; - -export const backendModule = createFactory({ - name: 'backend-module', - description: - 'A new backend module that extends an existing backend plugin with additional features', - optionsDiscovery: async () => ({ - codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), - }), - optionsPrompts: [pluginIdPrompt(), moduleIdIdPrompt(), ownerPrompt()], - async create(options: Options, ctx: CreateContext) { - const { id: pluginId, moduleId } = options; - const dirName = `${pluginId}-backend-module-${moduleId}`; - const name = resolvePackageName({ - baseName: dirName, - scope: ctx.scope, - plugin: true, - }); - - Task.log(); - Task.log(`Creating backend module ${chalk.cyan(name)}`); - - const targetDir = ctx.isMonoRepo - ? paths.resolveTargetRoot('plugins', dirName) - : paths.resolveTargetRoot(`backstage-plugin-${dirName}`); - - const moduleCamelCase = camelCase(moduleId); - const modulePascalCase = - moduleCamelCase[0].toUpperCase() + moduleCamelCase.slice(1); - const moduleVar = `${camelCase(pluginId)}Module${modulePascalCase}`; - await executePluginPackageTemplate(ctx, { - targetDir, - templateName: 'default-backend-module', - values: { - pluginId, - moduleId, - name, - moduleVar, - packageVersion: ctx.defaultVersion, - privatePackage: ctx.private, - npmRegistry: ctx.npmRegistry, - license: ctx.license, - }, - }); - - 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}`, - }, - }, - ); - }); - } - - await addToBackend(name, { - type: 'module', - }); - - if (options.owner) { - await addCodeownersEntry(`/plugins/${dirName}`, 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/new/factories/backendPlugin.test.ts b/packages/cli/src/lib/new/factories/backendPlugin.test.ts deleted file mode 100644 index 5415a3cd71..0000000000 --- a/packages/cli/src/lib/new/factories/backendPlugin.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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 { sep } from 'path'; -import { Task } from '../../tasks'; -import { FactoryRegistry } from '../FactoryRegistry'; -import { - createMockOutputStream, - expectLogsToMatch, - mockPaths, -} from './common/testUtils'; -import { backendPlugin } from './backendPlugin'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -const backendIndexTsContent = ` -import { createBackend } from '@backstage/backend-defaults'; - -const backend = createBackend(); - -backend.start(); -`; - -describe('backendPlugin factory', () => { - const mockDir = createMockDirectory(); - - beforeEach(() => { - mockPaths({ - targetRoot: mockDir.path, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should create a backend plugin', async () => { - mockDir.setContent({ - packages: { - backend: { - 'package.json': JSON.stringify({}), - src: { - 'index.ts': backendIndexTsContent, - }, - }, - }, - plugins: {}, - }); - - 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'), - license: 'Apache-2.0', - }); - - expect(modified).toBe(true); - - expectLogsToMatch(output, [ - 'Creating backend plugin backstage-plugin-test-backend', - 'Checking Prerequisites:', - `availability plugins${sep}test-backend`, - 'creating temp dir', - 'Executing Template:', - 'templating .eslintrc.js.hbs', - 'templating README.md.hbs', - 'templating index.ts.hbs', - 'templating index.ts.hbs', - 'templating package.json.hbs', - 'templating plugin.ts.hbs', - 'templating plugin.test.ts.hbs', - 'copying index.ts', - 'copying setupTests.ts', - 'copying router.ts', - 'copying router.test.ts', - 'copying createTodoListService.ts', - 'copying types.ts', - 'Installing:', - `moving plugins${sep}test-backend`, - 'backend adding dependency', - 'backend adding plugin', - ]); - - await expect( - fs.readJson(mockDir.resolve('packages/backend/package.json')), - ).resolves.toEqual({ - dependencies: { - 'backstage-plugin-test-backend': '^1.0.0', - }, - }); - - await expect( - fs.readFile(mockDir.resolve('packages/backend/src/index.ts'), 'utf8'), - ).resolves.toBe(` -import { createBackend } from '@backstage/backend-defaults'; - -const backend = createBackend(); - -backend.add(import('backstage-plugin-test-backend')); -backend.start(); -`); - - expect(Task.forCommand).toHaveBeenCalledTimes(2); - expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: mockDir.resolve('plugins/test-backend'), - optional: true, - }); - expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: mockDir.resolve('plugins/test-backend'), - optional: true, - }); - }); -}); diff --git a/packages/cli/src/lib/new/factories/backendPlugin.ts b/packages/cli/src/lib/new/factories/backendPlugin.ts deleted file mode 100644 index 050d05f0df..0000000000 --- a/packages/cli/src/lib/new/factories/backendPlugin.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 camelCase from 'lodash/camelCase'; -import { paths } from '../../paths'; -import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; -import { CreateContext, createFactory } from '../types'; -import { addPackageDependency, addToBackend, Task } from '../../tasks'; -import { ownerPrompt, pluginIdPrompt } from './common/prompts'; -import { executePluginPackageTemplate } from './common/tasks'; -import { resolvePackageName } from './common/util'; - -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: [pluginIdPrompt(), ownerPrompt()], - async create(options: Options, ctx: CreateContext) { - const { id } = options; - const pluginId = `${id}-backend`; - const name = resolvePackageName({ - baseName: pluginId, - scope: ctx.scope, - plugin: true, - }); - - Task.log(); - Task.log(`Creating backend plugin ${chalk.cyan(name)}`); - - const targetDir = ctx.isMonoRepo - ? paths.resolveTargetRoot('plugins', pluginId) - : paths.resolveTargetRoot(`backstage-plugin-${pluginId}`); - - await executePluginPackageTemplate(ctx, { - targetDir, - templateName: 'default-backend-plugin', - values: { - id, - name, - pluginVar: `${camelCase(id)}Plugin`, - pluginVersion: ctx.defaultVersion, - privatePackage: ctx.private, - npmRegistry: ctx.npmRegistry, - license: ctx.license, - }, - }); - - 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}`, - }, - }, - ); - }); - } - - await addToBackend(name, { - type: 'plugin', - }); - - 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/src/lib/new/factories/common/prompts.ts b/packages/cli/src/lib/new/factories/common/prompts.ts deleted file mode 100644 index 428ba8f9d3..0000000000 --- a/packages/cli/src/lib/new/factories/common/prompts.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 moduleIdIdPrompt(): Prompt<{ moduleId: string }> { - return { - type: 'input', - name: 'moduleId', - message: 'Enter the ID of the module [required]', - validate: (value: string) => { - if (!value) { - return 'Please enter the ID of the module'; - } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { - return 'Module 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/new/factories/common/tasks.test.ts b/packages/cli/src/lib/new/factories/common/tasks.test.ts deleted file mode 100644 index 60783344fc..0000000000 --- a/packages/cli/src/lib/new/factories/common/tasks.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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 { sep } from 'path'; -import { - createMockOutputStream, - expectLogsToMatch, - mockPaths, -} from './testUtils'; -import { CreateContext } from '../../types'; -import { executePluginPackageTemplate } from './tasks'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -const mockDir = createMockDirectory(); - -mockPaths({ - ownDir: mockDir.resolve('own'), - targetRoot: mockDir.resolve('root'), -}); - -describe('executePluginPackageTemplate', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should execute template', async () => { - mockDir.setContent({ - 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: mockDir.resolve('target'), - values: { - id: 'testing', - makePrivate: true, - }, - }, - ); - - expect(modified).toBe(true); - expectLogsToMatch(output, [ - 'Checking Prerequisites:', - `availability ..${sep}target`, - 'creating temp dir', - 'Executing Template:', - 'templating package.json.hbs', - 'copying not-templated.txt', - 'templating templated.txt.hbs', - 'Installing:', - `moving ..${sep}target`, - ]); - await expect(fs.readFile(mockDir.resolve('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(mockDir.resolve('target/subdir/templated.txt'), 'utf8'), - ).resolves.toBe('Hello testing!'); - await expect( - fs.readFile(mockDir.resolve('target/subdir/not-templated.txt'), 'utf8'), - ).resolves.toBe('Hello {{id}}!'); - }); -}); diff --git a/packages/cli/src/lib/new/factories/common/tasks.ts b/packages/cli/src/lib/new/factories/common/tasks.ts deleted file mode 100644 index 99ffabfb24..0000000000 --- a/packages/cli/src/lib/new/factories/common/tasks.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 { resolve as resolvePath, relative as relativePath } from 'path'; -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 = relativePath(paths.targetRoot, targetDir); - 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), - ctx.isMonoRepo, - ); - - // Format package.json if it exists - 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'); - 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/new/factories/common/testUtils.ts b/packages/cli/src/lib/new/factories/common/testUtils.ts deleted file mode 100644 index 1ade13a996..0000000000 --- a/packages/cli/src/lib/new/factories/common/testUtils.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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. - */ - -/* eslint-disable no-control-regex */ - -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) => { - 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; -} - -// Avoid flakes by comparing sorted log lines. File system access is async, which leads to the log line order being indeterministic -export function expectLogsToMatch( - recievedLogs: String[], - expected: String[], -): void { - expect(recievedLogs.filter(Boolean).sort()).toEqual(expected.sort()); -} diff --git a/packages/cli/src/lib/new/factories/common/util.test.ts b/packages/cli/src/lib/new/factories/common/util.test.ts deleted file mode 100644 index 6f168d8035..0000000000 --- a/packages/cli/src/lib/new/factories/common/util.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2024 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 { resolvePackageName } from './util'; - -describe('resolvePackageName', () => { - it('should generate correct name without scope', () => { - expect(resolvePackageName({ baseName: 'test', plugin: true })).toEqual( - 'backstage-plugin-test', - ); - expect(resolvePackageName({ baseName: 'test', plugin: false })).toEqual( - 'test', - ); - }); - - it('should generate correct name for backstage scope', () => { - expect( - resolvePackageName({ - baseName: 'test', - scope: 'backstage', - plugin: true, - }), - ).toEqual('@backstage/plugin-test'); - expect( - resolvePackageName({ - baseName: 'test', - scope: 'backstage', - plugin: false, - }), - ).toEqual('@backstage/test'); - }); - - it('should generate correct name for custom scope', () => { - expect( - resolvePackageName({ - baseName: 'test', - scope: 'custom', - plugin: true, - }), - ).toEqual('@custom/backstage-plugin-test'); - expect( - resolvePackageName({ - baseName: 'test', - scope: 'custom', - plugin: false, - }), - ).toEqual('@custom/test'); - }); - - it('should generate correct name for custom scope and custom prefix', () => { - expect( - resolvePackageName({ - baseName: 'test', - scope: 'custom/myapp.', - plugin: true, - }), - ).toEqual('@custom/myapp.backstage-plugin-test'); - expect( - resolvePackageName({ - baseName: 'test', - scope: 'custom/myapp.', - plugin: false, - }), - ).toEqual('@custom/myapp.test'); - }); -}); diff --git a/packages/cli/src/lib/new/factories/common/util.ts b/packages/cli/src/lib/new/factories/common/util.ts deleted file mode 100644 index d9c6970afd..0000000000 --- a/packages/cli/src/lib/new/factories/common/util.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2024 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 const resolvePackageName = (options: { - baseName: string; - scope?: string; - plugin: boolean; -}) => { - const { baseName, scope, plugin } = options; - if (scope) { - if (plugin) { - const pluginName = scope.startsWith('backstage') - ? 'plugin' - : 'backstage-plugin'; - return scope.includes('/') - ? `@${scope}${pluginName}-${baseName}` - : `@${scope}/${pluginName}-${baseName}`; - } - return scope.includes('/') - ? `@${scope}${baseName}` - : `@${scope}/${baseName}`; - } - - return plugin ? `backstage-plugin-${baseName}` : baseName; -}; diff --git a/packages/cli/src/lib/new/factories/frontendPlugin.test.ts b/packages/cli/src/lib/new/factories/frontendPlugin.test.ts deleted file mode 100644 index a4a89c3245..0000000000 --- a/packages/cli/src/lib/new/factories/frontendPlugin.test.ts +++ /dev/null @@ -1,213 +0,0 @@ -/* - * 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 { sep } from 'path'; -import { Task } from '../../tasks'; -import { FactoryRegistry } from '../FactoryRegistry'; -import { - createMockOutputStream, - expectLogsToMatch, - mockPaths, -} from './common/testUtils'; -import { frontendPlugin } from './frontendPlugin'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -const appTsxContent = ` -import { createApp } from '@backstage/app-defaults'; - -const router = ( - - } /> - -) -`; - -describe('frontendPlugin factory', () => { - const mockDir = createMockDirectory(); - - beforeEach(() => { - mockPaths({ - targetRoot: mockDir.path, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should create a frontend plugin', async () => { - mockDir.setContent({ - packages: { - app: { - 'package.json': JSON.stringify({}), - src: { - 'App.tsx': appTsxContent, - }, - }, - }, - plugins: {}, - }); - - 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'), - license: 'Apache-2.0', - }); - - expect(modified).toBe(true); - - expectLogsToMatch(output, [ - 'Creating frontend plugin backstage-plugin-test', - 'Checking Prerequisites:', - `availability plugins${sep}test`, - 'creating temp dir', - 'Executing Template:', - 'templating .eslintrc.js.hbs', - 'templating README.md.hbs', - 'templating package.json.hbs', - '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${sep}test`, - 'app adding dependency', - 'app adding import', - ]); - - await expect( - fs.readJson(mockDir.resolve('packages/app/package.json')), - ).resolves.toEqual({ - dependencies: { - 'backstage-plugin-test': '^1.0.0', - }, - }); - - await expect( - fs.readFile(mockDir.resolve('packages/app/src/App.tsx'), 'utf8'), - ).resolves.toBe(` -import { createApp } from '@backstage/app-defaults'; -import { TestPage } from 'backstage-plugin-test'; - -const router = ( - - } /> - } /> - -) -`); - - expect(Task.forCommand).toHaveBeenCalledTimes(2); - expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: mockDir.resolve('plugins/test'), - optional: true, - }); - expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: mockDir.resolve('plugins/test'), - optional: true, - }); - }); - - it('should create a frontend plugin with more options and codeowners', async () => { - mockDir.setContent({ - CODEOWNERS: '', - packages: { - app: { - 'package.json': JSON.stringify({}), - src: { - 'App.tsx': appTsxContent, - }, - }, - }, - plugins: {}, - }); - - 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'), - license: 'Apache-2.0', - }); - - await expect( - fs.readJson(mockDir.resolve('packages/app/package.json')), - ).resolves.toEqual({ - dependencies: { - '@internal/backstage-plugin-test': '^1.0.0', - }, - }); - - await expect( - fs.readFile(mockDir.resolve('packages/app/src/App.tsx'), 'utf8'), - ).resolves.toBe(` -import { createApp } from '@backstage/app-defaults'; -import { TestPage } from '@internal/backstage-plugin-test'; - -const router = ( - - } /> - } /> - -) -`); - - expect(Task.forCommand).toHaveBeenCalledTimes(2); - expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: mockDir.resolve('plugins/test'), - optional: true, - }); - expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: mockDir.resolve('plugins/test'), - optional: true, - }); - }); -}); diff --git a/packages/cli/src/lib/new/factories/frontendPlugin.ts b/packages/cli/src/lib/new/factories/frontendPlugin.ts deleted file mode 100644 index 41907d9d3a..0000000000 --- a/packages/cli/src/lib/new/factories/frontendPlugin.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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 camelCase from 'lodash/camelCase'; -import upperFirst from 'lodash/upperFirst'; -import { paths } from '../../paths'; -import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; -import { CreateContext, createFactory } from '../types'; -import { addPackageDependency, Task } from '../../tasks'; -import { ownerPrompt, pluginIdPrompt } from './common/prompts'; -import { executePluginPackageTemplate } from './common/tasks'; -import { resolvePackageName } from './common/util'; - -type Options = { - id: string; - owner?: string; - codeOwnersPath?: string; -}; - -export const frontendPlugin = createFactory({ - name: 'plugin', - description: 'A new frontend plugin', - optionsDiscovery: async () => ({ - codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), - }), - optionsPrompts: [pluginIdPrompt(), ownerPrompt()], - async create(options: Options, ctx: CreateContext) { - const { id } = options; - - const name = resolvePackageName({ - baseName: id, - scope: ctx.scope, - plugin: true, - }); - const extensionName = `${upperFirst(camelCase(id))}Page`; - - Task.log(); - Task.log(`Creating frontend plugin ${chalk.cyan(name)}`); - - const targetDir = ctx.isMonoRepo - ? paths.resolveTargetRoot('plugins', id) - : paths.resolveTargetRoot(`backstage-plugin-${id}`); - - await executePluginPackageTemplate(ctx, { - targetDir, - templateName: 'default-plugin', - values: { - id, - name, - extensionName, - pluginVar: `${camelCase(id)}Plugin`, - pluginVersion: ctx.defaultVersion, - privatePackage: ctx.private, - npmRegistry: ctx.npmRegistry, - license: ctx.license, - }, - }); - - 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.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/src/lib/new/factories/nodeLibraryPackage.test.ts b/packages/cli/src/lib/new/factories/nodeLibraryPackage.test.ts deleted file mode 100644 index 929182d84e..0000000000 --- a/packages/cli/src/lib/new/factories/nodeLibraryPackage.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2022 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 { join as joinPath } from 'path'; -import { Task } from '../../tasks'; -import { FactoryRegistry } from '../FactoryRegistry'; -import { - createMockOutputStream, - expectLogsToMatch, - mockPaths, -} from './common/testUtils'; -import { nodeLibraryPackage } from './nodeLibraryPackage'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -describe('nodeLibraryPackage factory', () => { - const mockDir = createMockDirectory(); - - beforeEach(() => { - mockPaths({ - targetRoot: mockDir.path, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should create a node library package', async () => { - const expectedNodeLibraryPackageName = 'test'; - - mockDir.setContent({ - packages: {}, - }); - - const options = await FactoryRegistry.populateOptions(nodeLibraryPackage, { - id: 'test', // name of node library package - }); - - let modified = false; - - const [output, mockStream] = createMockOutputStream(); - jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); - jest.spyOn(Task, 'forCommand').mockResolvedValue(); - - await nodeLibraryPackage.create(options, { - private: true, - isMonoRepo: true, - defaultVersion: '1.0.0', - markAsModified: () => { - modified = true; - }, - createTemporaryDirectory: () => fs.mkdtemp('test'), - license: 'Apache-2.0', - }); - - expect(modified).toBe(true); - - expectLogsToMatch(output, [ - `Creating node-library package ${expectedNodeLibraryPackageName}`, - 'Checking Prerequisites:', - `availability ${joinPath('packages', expectedNodeLibraryPackageName)}`, - 'creating temp dir', - 'Executing Template:', - 'templating .eslintrc.js.hbs', - 'templating README.md.hbs', - 'templating package.json.hbs', - 'templating index.ts.hbs', - 'copying setupTests.ts', - 'Installing:', - `moving ${joinPath('packages', expectedNodeLibraryPackageName)}`, - ]); - - await expect( - fs.readJson( - mockDir.resolve( - 'packages', - expectedNodeLibraryPackageName, - 'package.json', - ), - ), - ).resolves.toEqual( - expect.objectContaining({ - name: expectedNodeLibraryPackageName, - private: true, - version: '1.0.0', - }), - ); - - expect(Task.forCommand).toHaveBeenCalledTimes(2); - expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: mockDir.resolve('packages', expectedNodeLibraryPackageName), - optional: true, - }); - expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: mockDir.resolve('packages', expectedNodeLibraryPackageName), - optional: true, - }); - }); - - it('should create a node library plugin with options and codeowners', async () => { - const expectedNodeLibraryPackageName = 'test'; - - mockDir.setContent({ - CODEOWNERS: '', - packages: {}, - }); - - const options = await FactoryRegistry.populateOptions(nodeLibraryPackage, { - id: 'test', - owner: '@backstage/test-owners', - }); - - const [, mockStream] = createMockOutputStream(); - jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); - jest.spyOn(Task, 'forCommand').mockResolvedValue(); - - await nodeLibraryPackage.create(options, { - scope: 'internal', - private: true, - isMonoRepo: false, - defaultVersion: '1.0.0', - markAsModified: () => {}, - createTemporaryDirectory: () => fs.mkdtemp('test'), - license: 'Apache-2.0', - }); - - expect(Task.forCommand).toHaveBeenCalledTimes(2); - expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: mockDir.resolve(expectedNodeLibraryPackageName), - optional: true, - }); - expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: mockDir.resolve(expectedNodeLibraryPackageName), - optional: true, - }); - }); -}); diff --git a/packages/cli/src/lib/new/factories/nodeLibraryPackage.ts b/packages/cli/src/lib/new/factories/nodeLibraryPackage.ts deleted file mode 100644 index 3d9029a01d..0000000000 --- a/packages/cli/src/lib/new/factories/nodeLibraryPackage.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2022 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 { CreateContext, createFactory } from '../types'; -import { Task } from '../../tasks'; -import { ownerPrompt, pluginIdPrompt } from './common/prompts'; -import { executePluginPackageTemplate } from './common/tasks'; -import { resolvePackageName } from './common/util'; - -type Options = { - id: string; - owner?: string; - codeOwnersPath?: string; -}; - -export const nodeLibraryPackage = createFactory({ - name: 'node-library', - description: - 'A new node-library package, exporting shared functionality for backend plugins and modules', - optionsDiscovery: async () => ({ - codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), - }), - optionsPrompts: [pluginIdPrompt(), ownerPrompt()], - async create(options: Options, ctx: CreateContext) { - const { id } = options; - const name = resolvePackageName({ - baseName: id, - scope: ctx.scope, - plugin: false, - }); - - Task.log(); - Task.log(`Creating node-library package ${chalk.cyan(name)}`); - - const targetDir = ctx.isMonoRepo - ? paths.resolveTargetRoot('packages', id) - : paths.resolveTargetRoot(`${id}`); - - await executePluginPackageTemplate(ctx, { - targetDir, - templateName: 'node-library-package', - values: { - id, - name, - pluginVersion: ctx.defaultVersion, - privatePackage: ctx.private, - npmRegistry: ctx.npmRegistry, - license: ctx.license, - }, - }); - - if (options.owner) { - await addCodeownersEntry(`/packages/${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/src/lib/new/factories/pluginCommon.test.ts b/packages/cli/src/lib/new/factories/pluginCommon.test.ts deleted file mode 100644 index 6dfcc83536..0000000000 --- a/packages/cli/src/lib/new/factories/pluginCommon.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 { sep } from 'path'; -import { Task } from '../../tasks'; -import { FactoryRegistry } from '../FactoryRegistry'; -import { - createMockOutputStream, - expectLogsToMatch, - mockPaths, -} from './common/testUtils'; -import { pluginCommon } from './pluginCommon'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -describe('pluginCommon factory', () => { - const mockDir = createMockDirectory(); - - beforeEach(() => { - mockPaths({ - targetRoot: mockDir.path, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should create a common plugin package', async () => { - mockDir.setContent({ - plugins: {}, - }); - - 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'), - license: 'Apache-2.0', - }); - - expect(modified).toBe(true); - - expectLogsToMatch(output, [ - 'Creating common plugin package backstage-plugin-test-common', - 'Checking Prerequisites:', - `availability plugins${sep}test-common`, - 'creating temp dir', - 'Executing Template:', - 'templating .eslintrc.js.hbs', - 'templating README.md.hbs', - 'templating package.json.hbs', - 'templating index.ts.hbs', - 'copying setupTests.ts', - 'Installing:', - `moving plugins${sep}test-common`, - ]); - - await expect( - fs.readJson(mockDir.resolve('plugins/test-common/package.json')), - ).resolves.toEqual( - expect.objectContaining({ - name: 'backstage-plugin-test-common', - description: 'Common functionalities for the test plugin', - private: true, - version: '1.0.0', - }), - ); - - expect(Task.forCommand).toHaveBeenCalledTimes(2); - expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: mockDir.resolve('plugins/test-common'), - optional: true, - }); - expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: mockDir.resolve('plugins/test-common'), - optional: true, - }); - }); -}); diff --git a/packages/cli/src/lib/new/factories/pluginCommon.ts b/packages/cli/src/lib/new/factories/pluginCommon.ts deleted file mode 100644 index eb17c70a5c..0000000000 --- a/packages/cli/src/lib/new/factories/pluginCommon.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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 { CreateContext, createFactory } from '../types'; -import { Task } from '../../tasks'; -import { ownerPrompt, pluginIdPrompt } from './common/prompts'; -import { executePluginPackageTemplate } from './common/tasks'; -import { resolvePackageName } from './common/util'; - -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; - const suffix = `${id}-common`; - const name = resolvePackageName({ - baseName: suffix, - scope: ctx.scope, - plugin: true, - }); - - Task.log(); - Task.log(`Creating common plugin package ${chalk.cyan(name)}`); - - const targetDir = ctx.isMonoRepo - ? paths.resolveTargetRoot('plugins', suffix) - : paths.resolveTargetRoot(`backstage-plugin-${suffix}`); - - await executePluginPackageTemplate(ctx, { - targetDir, - templateName: 'default-common-plugin-package', - values: { - id, - name, - privatePackage: ctx.private, - npmRegistry: ctx.npmRegistry, - pluginVersion: ctx.defaultVersion, - license: ctx.license, - }, - }); - - if (options.owner) { - await addCodeownersEntry(`/plugins/${suffix}`, 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/new/factories/pluginNode.test.ts b/packages/cli/src/lib/new/factories/pluginNode.test.ts deleted file mode 100644 index e6ff093e22..0000000000 --- a/packages/cli/src/lib/new/factories/pluginNode.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 { sep } from 'path'; -import { Task } from '../../tasks'; -import { FactoryRegistry } from '../FactoryRegistry'; -import { - createMockOutputStream, - expectLogsToMatch, - mockPaths, -} from './common/testUtils'; -import { pluginNode } from './pluginNode'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -describe('pluginNode factory', () => { - const mockDir = createMockDirectory(); - - beforeEach(() => { - mockPaths({ - targetRoot: mockDir.path, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should create a node plugin package', async () => { - mockDir.setContent({ - plugins: {}, - }); - - const options = await FactoryRegistry.populateOptions(pluginNode, { - id: 'test', - }); - - let modified = false; - - const [output, mockStream] = createMockOutputStream(); - jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); - jest.spyOn(Task, 'forCommand').mockResolvedValue(); - - await pluginNode.create(options, { - private: true, - isMonoRepo: true, - defaultVersion: '1.0.0', - markAsModified: () => { - modified = true; - }, - createTemporaryDirectory: () => fs.mkdtemp('test'), - license: 'Apache-2.0', - }); - - expect(modified).toBe(true); - - expectLogsToMatch(output, [ - 'Creating Node.js plugin library backstage-plugin-test-node', - 'Checking Prerequisites:', - `availability plugins${sep}test-node`, - 'creating temp dir', - 'Executing Template:', - 'templating .eslintrc.js.hbs', - 'templating README.md.hbs', - 'templating package.json.hbs', - 'templating index.ts.hbs', - 'copying setupTests.ts', - 'Installing:', - `moving plugins${sep}test-node`, - ]); - - await expect( - fs.readJson(mockDir.resolve('plugins/test-node/package.json')), - ).resolves.toEqual( - expect.objectContaining({ - name: 'backstage-plugin-test-node', - description: 'Node.js library for the test plugin', - private: true, - version: '1.0.0', - }), - ); - - expect(Task.forCommand).toHaveBeenCalledTimes(2); - expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: mockDir.resolve('plugins/test-node'), - optional: true, - }); - expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: mockDir.resolve('plugins/test-node'), - optional: true, - }); - }); -}); diff --git a/packages/cli/src/lib/new/factories/pluginNode.ts b/packages/cli/src/lib/new/factories/pluginNode.ts deleted file mode 100644 index 8eaab3cadb..0000000000 --- a/packages/cli/src/lib/new/factories/pluginNode.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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 { CreateContext, createFactory } from '../types'; -import { Task } from '../../tasks'; -import { ownerPrompt, pluginIdPrompt } from './common/prompts'; -import { executePluginPackageTemplate } from './common/tasks'; -import { resolvePackageName } from './common/util'; - -type Options = { - id: string; - owner?: string; - codeOwnersPath?: string; -}; - -export const pluginNode = createFactory({ - name: 'plugin-node', - description: 'A new Node.js library plugin package', - optionsDiscovery: async () => ({ - codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), - }), - optionsPrompts: [pluginIdPrompt(), ownerPrompt()], - async create(options: Options, ctx: CreateContext) { - const { id } = options; - const suffix = `${id}-node`; - const name = resolvePackageName({ - baseName: suffix, - scope: ctx.scope, - plugin: true, - }); - - Task.log(); - Task.log(`Creating Node.js plugin library ${chalk.cyan(name)}`); - - const targetDir = ctx.isMonoRepo - ? paths.resolveTargetRoot('plugins', suffix) - : paths.resolveTargetRoot(`backstage-plugin-${suffix}`); - - await executePluginPackageTemplate(ctx, { - targetDir, - templateName: 'default-node-plugin-package', - values: { - id, - name, - privatePackage: ctx.private, - npmRegistry: ctx.npmRegistry, - pluginVersion: ctx.defaultVersion, - license: ctx.license, - }, - }); - - if (options.owner) { - await addCodeownersEntry(`/plugins/${suffix}`, 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/new/factories/pluginWeb.test.ts b/packages/cli/src/lib/new/factories/pluginWeb.test.ts deleted file mode 100644 index ddc4920fb8..0000000000 --- a/packages/cli/src/lib/new/factories/pluginWeb.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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 { sep } from 'path'; -import { Task } from '../../tasks'; -import { FactoryRegistry } from '../FactoryRegistry'; -import { - createMockOutputStream, - expectLogsToMatch, - mockPaths, -} from './common/testUtils'; -import { pluginWeb } from './pluginWeb'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -describe('pluginWeb factory', () => { - const mockDir = createMockDirectory(); - - beforeEach(() => { - mockPaths({ - targetRoot: mockDir.path, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should create a react plugin package', async () => { - mockDir.setContent({ - plugins: {}, - }); - - const options = await FactoryRegistry.populateOptions(pluginWeb, { - id: 'test', - }); - - let modified = false; - - const [output, mockStream] = createMockOutputStream(); - jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); - jest.spyOn(Task, 'forCommand').mockResolvedValue(); - - await pluginWeb.create(options, { - private: true, - isMonoRepo: true, - defaultVersion: '1.0.0', - markAsModified: () => { - modified = true; - }, - createTemporaryDirectory: () => fs.mkdtemp('test'), - license: 'Apache-2.0', - }); - - expect(modified).toBe(true); - - expectLogsToMatch(output, [ - 'Creating web plugin library backstage-plugin-test-react', - 'Checking Prerequisites:', - `availability plugins${sep}test-react`, - 'creating temp dir', - 'Executing Template:', - 'templating .eslintrc.js.hbs', - 'templating README.md.hbs', - 'templating package.json.hbs', - 'templating index.ts.hbs', - 'copying setupTests.ts', - 'copying index.ts', - 'copying ExampleComponent.test.tsx', - 'copying ExampleComponent.tsx', - 'copying index.ts', - 'copying index.ts', - 'copying index.ts', - 'copying useExample.ts', - 'Installing:', - `moving plugins${sep}test-react`, - ]); - - await expect( - fs.readJson(mockDir.resolve('plugins/test-react/package.json')), - ).resolves.toEqual( - expect.objectContaining({ - name: 'backstage-plugin-test-react', - description: 'Web library for the test plugin', - private: true, - version: '1.0.0', - }), - ); - - expect(Task.forCommand).toHaveBeenCalledTimes(2); - expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: mockDir.resolve('plugins/test-react'), - optional: true, - }); - expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: mockDir.resolve('plugins/test-react'), - optional: true, - }); - }); -}); diff --git a/packages/cli/src/lib/new/factories/pluginWeb.ts b/packages/cli/src/lib/new/factories/pluginWeb.ts deleted file mode 100644 index 6635b0b0e1..0000000000 --- a/packages/cli/src/lib/new/factories/pluginWeb.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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 { CreateContext, createFactory } from '../types'; -import { Task } from '../../tasks'; -import { ownerPrompt, pluginIdPrompt } from './common/prompts'; -import { executePluginPackageTemplate } from './common/tasks'; -import { resolvePackageName } from './common/util'; - -type Options = { - id: string; - owner?: string; - codeOwnersPath?: string; -}; - -export const pluginWeb = createFactory({ - name: 'plugin-react', - description: 'A new web library plugin package', - optionsDiscovery: async () => ({ - codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), - }), - optionsPrompts: [pluginIdPrompt(), ownerPrompt()], - async create(options: Options, ctx: CreateContext) { - const { id } = options; - const suffix = `${id}-react`; - const name = resolvePackageName({ - baseName: suffix, - scope: ctx.scope, - plugin: true, - }); - - Task.log(); - Task.log(`Creating web plugin library ${chalk.cyan(name)}`); - - const targetDir = ctx.isMonoRepo - ? paths.resolveTargetRoot('plugins', suffix) - : paths.resolveTargetRoot(`backstage-plugin-${suffix}`); - - await executePluginPackageTemplate(ctx, { - targetDir, - templateName: 'default-react-plugin-package', - values: { - id, - name, - privatePackage: ctx.private, - npmRegistry: ctx.npmRegistry, - pluginVersion: ctx.defaultVersion, - license: ctx.license, - }, - }); - - if (options.owner) { - await addCodeownersEntry(`/plugins/${suffix}`, 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/new/factories/scaffolderModule.test.ts b/packages/cli/src/lib/new/factories/scaffolderModule.test.ts deleted file mode 100644 index 160177f5c0..0000000000 --- a/packages/cli/src/lib/new/factories/scaffolderModule.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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 { sep } from 'path'; -import { Task } from '../../tasks'; -import { FactoryRegistry } from '../FactoryRegistry'; -import { - createMockOutputStream, - expectLogsToMatch, - mockPaths, -} from './common/testUtils'; -import { scaffolderModule } from './scaffolderModule'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -const backendIndexTsContent = ` -import { createBackend } from '@backstage/backend-defaults'; - -const backend = createBackend(); - -backend.start(); -`; - -describe('scaffolderModule factory', () => { - const mockDir = createMockDirectory(); - - beforeEach(() => { - mockPaths({ - targetRoot: mockDir.path, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should create a scaffolder backend module package', async () => { - mockDir.setContent({ - packages: { - backend: { - 'package.json': JSON.stringify({}), - src: { - 'index.ts': backendIndexTsContent, - }, - }, - }, - plugins: {}, - }); - - 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), - license: 'Apache-2.0', - }); - - expect(modified).toBe(true); - - expectLogsToMatch(output, [ - 'Creating module backstage-plugin-scaffolder-backend-module-test', - 'Checking Prerequisites:', - `availability plugins${sep}scaffolder-backend-module-test`, - 'creating temp dir', - 'Executing Template:', - 'templating .eslintrc.js.hbs', - 'templating README.md.hbs', - 'templating package.json.hbs', - 'templating index.ts.hbs', - 'copying example.test.ts', - 'copying example.ts', - 'copying module.ts', - 'Installing:', - `moving plugins${sep}scaffolder-backend-module-test`, - 'backend adding dependency', - 'backend adding module', - ]); - - await expect( - fs.readFile(mockDir.resolve('packages/backend/src/index.ts'), 'utf8'), - ).resolves.toBe(` -import { createBackend } from '@backstage/backend-defaults'; - -const backend = createBackend(); - -backend.add(import('backstage-plugin-scaffolder-backend-module-test')); -backend.start(); -`); - - await expect( - fs.readJson(mockDir.resolve('packages/backend/package.json')), - ).resolves.toEqual({ - dependencies: { - 'backstage-plugin-scaffolder-backend-module-test': '^1.0.0', - }, - }); - - await expect( - fs.readJson( - mockDir.resolve('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: mockDir.resolve('plugins/scaffolder-backend-module-test'), - optional: true, - }); - expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: mockDir.resolve('plugins/scaffolder-backend-module-test'), - optional: true, - }); - }); -}); diff --git a/packages/cli/src/lib/new/factories/scaffolderModule.ts b/packages/cli/src/lib/new/factories/scaffolderModule.ts deleted file mode 100644 index 6cea7a36ff..0000000000 --- a/packages/cli/src/lib/new/factories/scaffolderModule.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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 { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; -import { CreateContext, createFactory } from '../types'; -import { addPackageDependency, addToBackend, Task } from '../../tasks'; -import { ownerPrompt } from './common/prompts'; -import { executePluginPackageTemplate } from './common/tasks'; -import { resolvePackageName } from './common/util'; - -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}`; - - const name = resolvePackageName({ - baseName: slug, - scope: ctx.scope, - plugin: true, - }); - - 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, - license: ctx.license, - }, - }); - - 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}`, - }, - }, - ); - }); - } - - await addToBackend(name, { - type: 'module', - }); - - 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/new/factories/webLibraryPackage.test.ts b/packages/cli/src/lib/new/factories/webLibraryPackage.test.ts deleted file mode 100644 index 942b66f350..0000000000 --- a/packages/cli/src/lib/new/factories/webLibraryPackage.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2022 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 { join as joinPath } from 'path'; -import { Task } from '../../tasks'; -import { FactoryRegistry } from '../FactoryRegistry'; -import { - createMockOutputStream, - expectLogsToMatch, - mockPaths, -} from './common/testUtils'; -import { webLibraryPackage } from './webLibraryPackage'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -describe('webLibraryPackage factory', () => { - const mockDir = createMockDirectory(); - - beforeEach(() => { - mockPaths({ - targetRoot: mockDir.path, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should create a web library package', async () => { - const expectedwebLibraryPackageName = 'test'; - - mockDir.setContent({ - packages: {}, - }); - - const options = await FactoryRegistry.populateOptions(webLibraryPackage, { - id: 'test', // name of web library package - }); - - let modified = false; - - const [output, mockStream] = createMockOutputStream(); - jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); - jest.spyOn(Task, 'forCommand').mockResolvedValue(); - - await webLibraryPackage.create(options, { - private: true, - isMonoRepo: true, - defaultVersion: '1.0.0', - markAsModified: () => { - modified = true; - }, - createTemporaryDirectory: () => fs.mkdtemp('test'), - license: 'Apache-2.0', - }); - - expect(modified).toBe(true); - - expectLogsToMatch(output, [ - `Creating web-library package ${expectedwebLibraryPackageName}`, - 'Checking Prerequisites:', - `availability ${joinPath('packages', expectedwebLibraryPackageName)}`, - 'creating temp dir', - 'Executing Template:', - 'templating .eslintrc.js.hbs', - 'templating README.md.hbs', - 'templating package.json.hbs', - 'templating index.ts.hbs', - 'copying setupTests.ts', - 'Installing:', - `moving ${joinPath('packages', expectedwebLibraryPackageName)}`, - ]); - - await expect( - fs.readJson( - mockDir.resolve( - 'packages', - expectedwebLibraryPackageName, - 'package.json', - ), - ), - ).resolves.toEqual( - expect.objectContaining({ - name: expectedwebLibraryPackageName, - private: true, - version: '1.0.0', - }), - ); - - expect(Task.forCommand).toHaveBeenCalledTimes(2); - expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: mockDir.resolve('packages', expectedwebLibraryPackageName), - optional: true, - }); - expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: mockDir.resolve('packages', expectedwebLibraryPackageName), - optional: true, - }); - }); - - it('should create a web library plugin with options and codeowners', async () => { - const expectedwebLibraryPackageName = 'test'; - - mockDir.setContent({ - CODEOWNERS: '', - packages: {}, - }); - - const options = await FactoryRegistry.populateOptions(webLibraryPackage, { - id: 'test', - owner: '@backstage/test-owners', - }); - - const [, mockStream] = createMockOutputStream(); - jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); - jest.spyOn(Task, 'forCommand').mockResolvedValue(); - - await webLibraryPackage.create(options, { - scope: 'internal', - private: true, - isMonoRepo: false, - defaultVersion: '1.0.0', - markAsModified: () => {}, - createTemporaryDirectory: () => fs.mkdtemp('test'), - license: 'Apache-2.0', - }); - - expect(Task.forCommand).toHaveBeenCalledTimes(2); - expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: mockDir.resolve(expectedwebLibraryPackageName), - optional: true, - }); - expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: mockDir.resolve(expectedwebLibraryPackageName), - optional: true, - }); - }); -}); diff --git a/packages/cli/src/lib/new/factories/webLibraryPackage.ts b/packages/cli/src/lib/new/factories/webLibraryPackage.ts deleted file mode 100644 index 3c63127d7d..0000000000 --- a/packages/cli/src/lib/new/factories/webLibraryPackage.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2022 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 { CreateContext, createFactory } from '../types'; -import { Task } from '../../tasks'; -import { ownerPrompt, pluginIdPrompt } from './common/prompts'; -import { executePluginPackageTemplate } from './common/tasks'; -import { resolvePackageName } from './common/util'; - -type Options = { - id: string; - owner?: string; - codeOwnersPath?: string; -}; - -export const webLibraryPackage = createFactory({ - name: 'web-library', - description: - 'A new web-library package, exporting shared functionality for frontend plugins', - optionsDiscovery: async () => ({ - codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), - }), - optionsPrompts: [pluginIdPrompt(), ownerPrompt()], - async create(options: Options, ctx: CreateContext) { - const { id } = options; - const name = resolvePackageName({ - baseName: id, - scope: ctx.scope, - plugin: false, - }); - - Task.log(); - Task.log(`Creating web-library package ${chalk.cyan(name)}`); - - const targetDir = ctx.isMonoRepo - ? paths.resolveTargetRoot('packages', id) - : paths.resolveTargetRoot(`${id}`); - - await executePluginPackageTemplate(ctx, { - targetDir, - templateName: 'web-library-package', - values: { - id, - name, - pluginVersion: ctx.defaultVersion, - privatePackage: ctx.private, - npmRegistry: ctx.npmRegistry, - license: ctx.license, - }, - }); - - if (options.owner) { - await addCodeownersEntry(`/packages/${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/src/lib/new/preparation/collectPortableTemplateInput.test.ts b/packages/cli/src/lib/new/preparation/collectPortableTemplateInput.test.ts new file mode 100644 index 0000000000..905ec0b0c5 --- /dev/null +++ b/packages/cli/src/lib/new/preparation/collectPortableTemplateInput.test.ts @@ -0,0 +1,152 @@ +/* + * Copyright 2024 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 { PortableTemplateConfig } from '../types'; +import { collectPortableTemplateInput } from './collectPortableTemplateInput'; +import { withLogCollector } from '@backstage/test-utils'; + +describe('collectTemplateParams', () => { + const baseOptions = { + config: { + isUsingDefaultTemplates: false, + templatePointers: [], + version: '0.1.0', + license: 'Apache-2.0', + private: true, + packageNamePrefix: '@internal/', + packageNamePluginInfix: 'plugin-', + } satisfies PortableTemplateConfig, + template: { + name: 'test', + role: 'frontend-plugin' as const, + files: [], + values: {}, + }, + prefilledParams: {}, + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should prompt for missing parameters', async () => { + jest.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ pluginId: 'other' }); + + await expect( + collectPortableTemplateInput({ + ...baseOptions, + prefilledParams: {}, + }), + ).resolves.toEqual({ + roleParams: { + role: 'frontend-plugin', + pluginId: 'other', + }, + owner: undefined, + version: '0.1.0', + license: 'Apache-2.0', + private: true, + packageName: '@internal/plugin-other', + packagePath: 'plugins/other', + }); + }); + + it('should pick up prefilled parameters', async () => { + await expect( + collectPortableTemplateInput({ + ...baseOptions, + prefilledParams: { + pluginId: 'test1', + owner: 'me', + }, + }), + ).resolves.toEqual({ + roleParams: { + role: 'frontend-plugin', + pluginId: 'test1', + }, + owner: 'me', + version: '0.1.0', + license: 'Apache-2.0', + private: true, + packageName: '@internal/plugin-test1', + packagePath: 'plugins/test1', + }); + }); + + it('should pick up template values', async () => { + await expect( + collectPortableTemplateInput({ + ...baseOptions, + template: { + ...baseOptions.template, + values: { + pluginId: 'test2', + owner: 'me', + }, + }, + }), + ).resolves.toEqual({ + roleParams: { + role: 'frontend-plugin', + pluginId: 'test2', + }, + owner: 'me', + version: '0.1.0', + license: 'Apache-2.0', + private: true, + packageName: '@internal/plugin-test2', + packagePath: 'plugins/test2', + }); + }); + + it('should map deprecated id param to pluginId', async () => { + const logs = await withLogCollector(async () => { + await expect( + collectPortableTemplateInput({ + ...baseOptions, + config: { + ...baseOptions.config, + isUsingDefaultTemplates: true, + }, + prefilledParams: { + id: 'test3', + owner: 'me', + }, + }), + ).resolves.toEqual({ + roleParams: { + role: 'frontend-plugin', + pluginId: 'test3', + }, + owner: 'me', + version: '0.1.0', + license: 'Apache-2.0', + private: true, + packageName: '@internal/plugin-test3', + packagePath: 'plugins/test3', + }); + }); + expect(logs).toEqual({ + error: [], + log: [], + warn: [ + `DEPRECATION WARNING: The 'id' parameter is deprecated, use 'pluginId' instead`, + ], + }); + }); +}); diff --git a/packages/cli/src/lib/new/preparation/collectPortableTemplateInput.ts b/packages/cli/src/lib/new/preparation/collectPortableTemplateInput.ts new file mode 100644 index 0000000000..720993e25c --- /dev/null +++ b/packages/cli/src/lib/new/preparation/collectPortableTemplateInput.ts @@ -0,0 +1,195 @@ +/* + * Copyright 2025 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, { DistinctQuestion } from 'inquirer'; +import { getCodeownersFilePath, parseOwnerIds } from '../../codeowners'; +import { paths } from '../../paths'; +import { + PortableTemplateConfig, + PortableTemplateInput, + PortableTemplateInputRoleParams, + PortableTemplateParams, + PortableTemplateRole, +} from '../types'; +import { PortableTemplate } from '../types'; +import { resolvePackageParams } from './resolvePackageParams'; + +type CollectTemplateParamsOptions = { + config: PortableTemplateConfig; + template: PortableTemplate; + prefilledParams: PortableTemplateParams; +}; + +export async function collectPortableTemplateInput( + options: CollectTemplateParamsOptions, +): Promise { + const { config, template, prefilledParams } = options; + + const codeOwnersFilePath = await getCodeownersFilePath(paths.targetRoot); + + const prompts = getPromptsForRole(template.role); + + if (codeOwnersFilePath) { + prompts.push(ownerPrompt()); + } + + const deprecatedParams: PortableTemplateParams = {}; + if (config.isUsingDefaultTemplates && prefilledParams.id) { + console.warn( + `DEPRECATION WARNING: The 'id' parameter is deprecated, use 'pluginId' instead`, + ); + deprecatedParams.pluginId = prefilledParams.id; + } + + const parameters = { + ...template.values, + ...prefilledParams, + ...deprecatedParams, + }; + + const needsAnswer = []; + const prefilledAnswers = {} as PortableTemplateParams; + for (const prompt of prompts) { + if (prompt.name && parameters[prompt.name] !== undefined) { + prefilledAnswers[prompt.name] = parameters[prompt.name]; + } else { + needsAnswer.push(prompt); + } + } + + const promptAnswers = await inquirer.prompt( + needsAnswer, + ); + + const answers = { + ...prefilledAnswers, + ...promptAnswers, + }; + + const roleParams = { + role: template.role, + name: answers.name, + pluginId: answers.pluginId, + moduleId: answers.moduleId, + } as PortableTemplateInputRoleParams; + + const packageParams = resolvePackageParams({ + roleParams, + packagePrefix: config.packageNamePrefix, + pluginInfix: config.packageNamePluginInfix, + }); + + return { + roleParams, + owner: answers.owner as string | undefined, + license: config.license, + version: config.version, + private: config.private, + publishRegistry: config.publishRegistry, + packageName: packageParams.packageName, + packagePath: packageParams.packagePath, + }; +} + +export function namePrompt(): DistinctQuestion { + return { + type: 'input', + name: 'name', + message: 'Enter the name of the package, without scope [required]', + validate: (value: string) => { + if (!value) { + return 'Please enter the name of the package'; + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return 'Package names must be lowercase and contain only letters, digits, and dashes.'; + } + return true; + }, + }; +} + +export function pluginIdPrompt(): DistinctQuestion { + return { + type: 'input', + name: 'pluginId', + 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 moduleIdIdPrompt(): DistinctQuestion { + return { + type: 'input', + name: 'moduleId', + message: 'Enter the ID of the module [required]', + validate: (value: string) => { + if (!value) { + return 'Please enter the ID of the module'; + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return 'Module IDs must be lowercase and contain only letters, digits, and dashes.'; + } + return true; + }, + }; +} + +export function getPromptsForRole( + role: PortableTemplateRole, +): Array { + switch (role) { + case 'web-library': + case 'node-library': + case 'common-library': + return [namePrompt()]; + case 'plugin-web-library': + case 'plugin-node-library': + case 'plugin-common-library': + case 'frontend-plugin': + case 'backend-plugin': + return [pluginIdPrompt()]; + case 'frontend-plugin-module': + case 'backend-plugin-module': + return [pluginIdPrompt(), moduleIdIdPrompt()]; + default: + return []; + } +} + +export function ownerPrompt(): DistinctQuestion { + return { + type: 'input', + name: 'owner', + message: 'Enter an owner to add to CODEOWNERS [optional]', + 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/new/preparation/index.ts b/packages/cli/src/lib/new/preparation/index.ts new file mode 100644 index 0000000000..8b2b253997 --- /dev/null +++ b/packages/cli/src/lib/new/preparation/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2025 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 { collectPortableTemplateInput } from './collectPortableTemplateInput'; +export { loadPortableTemplateConfig } from './loadPortableTemplateConfig'; +export { selectTemplateInteractively } from './selectTemplateInteractively'; +export { loadPortableTemplate } from './loadPortableTemplate'; diff --git a/packages/cli/src/lib/new/preparation/loadPortableTemplate.test.ts b/packages/cli/src/lib/new/preparation/loadPortableTemplate.test.ts new file mode 100644 index 0000000000..21734f3da9 --- /dev/null +++ b/packages/cli/src/lib/new/preparation/loadPortableTemplate.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2025 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 { createMockDirectory } from '@backstage/backend-test-utils'; +import { loadPortableTemplate } from './loadPortableTemplate'; +import { TEMPLATE_FILE_NAME } from '../types'; + +describe('loadTemplate', () => { + const mockDir = createMockDirectory(); + + afterEach(() => { + mockDir.clear(); + }); + + it('should load a valid template', async () => { + mockDir.setContent({ + 'path/to': { + [TEMPLATE_FILE_NAME]: ` + name: template1 + role: frontend-plugin + values: + foo: bar + `, + }, + 'path/to/hello.txt': 'hello world', + }); + + await expect( + loadPortableTemplate({ + name: 'template1', + target: mockDir.resolve('path/to', TEMPLATE_FILE_NAME), + }), + ).resolves.toEqual({ + name: 'template1', + role: 'frontend-plugin', + files: [{ path: 'hello.txt', content: 'hello world' }], + values: { foo: 'bar' }, + }); + }); + + it('should throw an error if template file does not exist', async () => { + mockDir.setContent({}); + + await expect( + loadPortableTemplate({ + name: 'template1', + target: mockDir.resolve('path/to/template1.yaml'), + }), + ).rejects.toThrow( + /^Failed to load template definition from '.*'; caused by Error: ENOENT/, + ); + }); + + it('should throw an error if template definition is invalid', async () => { + mockDir.setContent({ + 'path/to/template1.yaml': `invalid: definition`, + }); + + await expect( + loadPortableTemplate({ + name: 'template1', + target: mockDir.resolve('path/to/template1.yaml'), + }), + ).rejects.toThrow( + /Invalid template definition at '.*'; caused by Validation error/, + ); + }); + + it('should throw an error if target is a remote URL', async () => { + await expect( + loadPortableTemplate({ + name: 'template1', + target: 'http://example.com', + }), + ).rejects.toThrow('Remote templates are not supported yet'); + }); + + it('should throw an error if the package role is invalid', async () => { + mockDir.setContent({ + 'path/to/template1.yaml': ` + name: x + role: invalid-role + `, + }); + + await expect( + loadPortableTemplate({ + name: 'template1', + target: mockDir.resolve('path/to/template1.yaml'), + }), + ).rejects.toThrow( + `Invalid template definition at '${mockDir.resolve( + 'path/to/template1.yaml', + )}'; caused by Validation error: Invalid enum value`, + ); + }); +}); diff --git a/packages/cli/src/lib/new/preparation/loadPortableTemplate.ts b/packages/cli/src/lib/new/preparation/loadPortableTemplate.ts new file mode 100644 index 0000000000..c792119ae6 --- /dev/null +++ b/packages/cli/src/lib/new/preparation/loadPortableTemplate.ts @@ -0,0 +1,108 @@ +/* + * Copyright 2025 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 { z } from 'zod'; +import fs from 'fs-extra'; +import recursiveReaddir from 'recursive-readdir'; +import { resolve as resolvePath, relative as relativePath } from 'path'; +import { dirname } from 'node:path'; +import { parse as parseYaml } from 'yaml'; +import { paths } from '../../paths'; +import { + PortableTemplateFile, + PortableTemplatePointer, + TEMPLATE_ROLES, +} from '../types'; +import { PortableTemplate } from '../types'; +import { ForwardedError } from '@backstage/errors'; +import { fromZodError } from 'zod-validation-error'; + +const templateDefinitionSchema = z + .object({ + name: z.string(), + role: z.enum(TEMPLATE_ROLES), + description: z.string().optional(), + values: z.record(z.string()).optional(), + }) + .strict(); + +export async function loadPortableTemplate( + pointer: PortableTemplatePointer, +): Promise { + if (pointer.target.match(/https?:\/\//)) { + throw new Error('Remote templates are not supported yet'); + } + const templateContent = await fs + .readFile(paths.resolveTargetRoot(pointer.target), 'utf-8') + .catch(error => { + throw new ForwardedError( + `Failed to load template definition from '${pointer.target}'`, + error, + ); + }); + const rawTemplate = parseYaml(templateContent); + + const parsed = templateDefinitionSchema.safeParse(rawTemplate); + if (!parsed.success) { + throw new ForwardedError( + `Invalid template definition at '${pointer.target}'`, + fromZodError(parsed.error), + ); + } + + const { role, values = {} } = parsed.data; + + const templatePath = resolvePath(dirname(pointer.target)); + const filePaths = await recursiveReaddir(templatePath).catch(error => { + throw new ForwardedError( + `Failed to load template contents from '${templatePath}'`, + error, + ); + }); + + const loadedFiles = new Array(); + + for (const filePath of filePaths) { + const path = relativePath(templatePath, filePath); + if (filePath === pointer.target) { + continue; + } + + const content = await fs.readFile(filePath, 'utf-8').catch(error => { + throw new ForwardedError( + `Failed to load file contents from '${path}'`, + error, + ); + }); + + if (path.endsWith('.hbs')) { + loadedFiles.push({ + path: path.slice(0, -4), + content, + syntax: 'handlebars', + }); + } else { + loadedFiles.push({ path, content }); + } + } + + return { + name: pointer.name, + role, + files: loadedFiles, + values, + }; +} diff --git a/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.test.ts b/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.test.ts new file mode 100644 index 0000000000..b8a7940bcf --- /dev/null +++ b/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.test.ts @@ -0,0 +1,339 @@ +/* + * Copyright 2025 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 { realpathSync } from 'node:fs'; +import { loadPortableTemplateConfig } from './loadPortableTemplateConfig'; +import { defaultTemplates } from '../defaultTemplates'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { TEMPLATE_FILE_NAME } from '../types'; +import { basename } from 'node:path'; + +describe('loadPortableTemplateConfig', () => { + const mockDir = createMockDirectory(); + + afterEach(() => { + mockDir.clear(); + }); + + it('should load configuration from package.json', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({ + backstage: { + cli: { + new: { + templates: ['./path/to/template1'], + globals: { + license: 'MIT', + private: true, + namePrefix: '@acme/', + namePluginInfix: 'backstage-plugin-', + }, + }, + }, + }, + }), + 'path/to/template1': { + [TEMPLATE_FILE_NAME]: 'name: template1\nrole: frontend-plugin\n', + }, + }); + + await expect( + loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + }), + ).resolves.toEqual({ + isUsingDefaultTemplates: false, + templatePointers: [ + { + name: 'template1', + target: mockDir.resolve('path/to/template1', TEMPLATE_FILE_NAME), + }, + ], + license: 'MIT', + private: true, + version: '0.1.0', + packageNamePrefix: '@acme/', + packageNamePluginInfix: 'backstage-plugin-', + }); + + await expect( + loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + overrides: { + license: 'nope', + private: false, + }, + }), + ).resolves.toEqual({ + isUsingDefaultTemplates: false, + templatePointers: [ + { + name: 'template1', + target: mockDir.resolve('path/to/template1', TEMPLATE_FILE_NAME), + }, + ], + license: 'nope', + version: '0.1.0', + private: false, + packageNamePrefix: '@acme/', + packageNamePluginInfix: 'backstage-plugin-', + }); + }); + + it('should support pointing to built-in templates', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({ + backstage: { + cli: { + new: { + templates: ['@my/package/templates/plugin', 'my-package'], + globals: { + license: 'MIT', + private: true, + namePrefix: '@acme/', + namePluginInfix: 'backstage-plugin-', + }, + }, + }, + }, + }), + node_modules: { + '@my': { + package: { + templates: { + plugin: { + [TEMPLATE_FILE_NAME]: + 'name: frontend-plugin\nrole: frontend-plugin\n', + }, + }, + }, + }, + 'my-package': { + [TEMPLATE_FILE_NAME]: 'name: backend-plugin\nrole: backend-plugin\n', + }, + }, + }); + + await expect( + loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + }), + ).resolves.toEqual({ + isUsingDefaultTemplates: false, + templatePointers: [ + { + name: 'frontend-plugin', + target: realpathSync( + mockDir.resolve( + 'node_modules/@my/package/templates/plugin', + TEMPLATE_FILE_NAME, + ), + ), + }, + { + name: 'backend-plugin', + target: realpathSync( + mockDir.resolve('node_modules/my-package', TEMPLATE_FILE_NAME), + ), + }, + ], + license: 'MIT', + private: true, + version: '0.1.0', + packageNamePrefix: '@acme/', + packageNamePluginInfix: 'backstage-plugin-', + }); + }); + + it('should use default templates if none are specified', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({ + backstage: { + cli: { + new: { + globals: { + license: 'MIT', + private: true, + }, + }, + }, + }, + }), + node_modules: Object.fromEntries( + defaultTemplates.map(t => [ + t, + { [TEMPLATE_FILE_NAME]: `name: ${basename(t)}\nrole: web-library\n` }, + ]), + ), + }); + + await expect( + loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + }), + ).resolves.toEqual({ + isUsingDefaultTemplates: true, + templatePointers: defaultTemplates.map(t => ({ + name: basename(t), + target: realpathSync( + mockDir.resolve(`node_modules/${t}`, TEMPLATE_FILE_NAME), + ), + })), + license: 'MIT', + private: true, + version: '0.1.0', + packageNamePrefix: '@internal/', + packageNamePluginInfix: 'plugin-', + }); + }); + + it('should reject templates with conflicting names', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({ + backstage: { + cli: { + new: { + templates: ['./template1', './template2'], + }, + }, + }, + }), + template1: { + [TEMPLATE_FILE_NAME]: 'name: test\nrole: frontend-plugin\n', + }, + template2: { + [TEMPLATE_FILE_NAME]: 'name: test\nrole: backend-plugin\n', + }, + }); + + await expect( + loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + }), + ).rejects.toThrow( + `Invalid template configuration, received conflicting template name 'test' from './template1' and './template2'`, + ); + }); + + it('should throw an error if package.json is invalid', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({ + backstage: { + cli: { + new: { + templates: 'invalid', + }, + }, + }, + }), + }); + + await expect( + loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + }), + ).rejects.toThrow( + /^Failed to load templating configuration from '.*'; caused by Validation error: Expected array/, + ); + }); + + it('should throw an error if built-in template does not exist', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({ + backstage: { + cli: { + new: { + templates: ['./invalid'], + }, + }, + }, + }), + }); + + await expect( + loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + }), + ).rejects.toThrow( + `Failed to load template definition '.\/invalid'; caused by Error: ENOENT`, + ); + }); + + it('should throw an error if template point is absolute', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({ + backstage: { + cli: { + new: { + templates: ['/invalid'], + }, + }, + }, + }), + }); + + await expect( + loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + }), + ).rejects.toThrow( + "Failed to load template definition '/invalid'; caused by Error: Template target may not be an absolute path", + ); + }); + + it('should handle missing backstage.new configuration', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({}), + node_modules: Object.fromEntries( + defaultTemplates.map(t => [ + t, + { [TEMPLATE_FILE_NAME]: `name: ${basename(t)}\nrole: web-library\n` }, + ]), + ), + }); + + await expect( + loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + }), + ).resolves.toEqual({ + isUsingDefaultTemplates: true, + templatePointers: expect.any(Array), + license: 'Apache-2.0', + version: '0.1.0', + private: true, + packageNamePrefix: '@internal/', + packageNamePluginInfix: 'plugin-', + }); + + await expect( + loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + overrides: { + license: 'nope', + }, + }), + ).resolves.toEqual({ + isUsingDefaultTemplates: true, + templatePointers: expect.any(Array), + license: 'nope', + version: '0.1.0', + private: true, + packageNamePrefix: '@internal/', + packageNamePluginInfix: 'plugin-', + }); + }); +}); diff --git a/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.ts b/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.ts new file mode 100644 index 0000000000..d4705b0320 --- /dev/null +++ b/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.ts @@ -0,0 +1,175 @@ +/* + * Copyright 2025 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 { resolve as resolvePath, dirname, isAbsolute } from 'node:path'; +import { paths } from '../../paths'; +import { defaultTemplates } from '../defaultTemplates'; +import { + PortableTemplateConfig, + PortableTemplatePointer, + TEMPLATE_FILE_NAME, +} from '../types'; +import { parse as parseYaml } from 'yaml'; +import { z } from 'zod'; +import { fromZodError } from 'zod-validation-error'; +import { ForwardedError } from '@backstage/errors'; + +const defaults = { + license: 'Apache-2.0', + version: '0.1.0', + private: true, + publishRegistry: undefined, + packageNamePrefix: '@internal/', + packageNamePluginInfix: 'plugin-', +}; + +const newConfigSchema = z + .object({ + templates: z.array(z.string()).optional(), + globals: z + .object({ + license: z.string().optional(), + version: z.string().optional(), + private: z.boolean().optional(), + publishRegistry: z.string().optional(), + namePrefix: z.string().optional(), + namePluginInfix: z.string().optional(), + }) + .optional(), + }) + .strict(); + +const pkgJsonWithNewConfigSchema = z.object({ + backstage: z + .object({ + cli: z + .object({ + new: newConfigSchema.optional(), + }) + .optional(), + }) + .optional(), +}); + +type LoadConfigOptions = { + packagePath?: string; + overrides?: Partial; +}; + +export async function loadPortableTemplateConfig( + options: LoadConfigOptions = {}, +): Promise { + const { overrides = {} } = options; + const pkgPath = + options.packagePath ?? paths.resolveTargetRoot('package.json'); + const pkgJson = await fs.readJson(pkgPath); + + const parsed = pkgJsonWithNewConfigSchema.safeParse(pkgJson); + if (!parsed.success) { + throw new ForwardedError( + `Failed to load templating configuration from '${pkgPath}'`, + fromZodError(parsed.error), + ); + } + + const config = parsed.data.backstage?.cli?.new; + + const basePath = dirname(pkgPath); + const templatePointerEntries = await Promise.all( + (config?.templates ?? defaultTemplates).map(async rawPointer => { + try { + const templatePath = resolveLocalTemplatePath(rawPointer, basePath); + + const pointer = await peekLocalTemplateDefinition(templatePath); + return { pointer, rawPointer }; + } catch (error) { + throw new ForwardedError( + `Failed to load template definition '${rawPointer}'`, + error, + ); + } + }), + ); + + const templateNameConflicts = new Map(); + for (const { pointer, rawPointer } of templatePointerEntries) { + const conflict = templateNameConflicts.get(pointer.name); + if (conflict) { + throw new Error( + `Invalid template configuration, received conflicting template name '${pointer.name}' from '${conflict}' and '${rawPointer}'`, + ); + } + templateNameConflicts.set(pointer.name, rawPointer); + } + + return { + isUsingDefaultTemplates: !config?.templates, + templatePointers: templatePointerEntries.map(({ pointer }) => pointer), + license: overrides.license ?? config?.globals?.license ?? defaults.license, + version: overrides.version ?? config?.globals?.version ?? defaults.version, + private: overrides.private ?? config?.globals?.private ?? defaults.private, + publishRegistry: + overrides.publishRegistry ?? + config?.globals?.publishRegistry ?? + defaults.publishRegistry, + packageNamePrefix: + overrides.packageNamePrefix ?? + config?.globals?.namePrefix ?? + defaults.packageNamePrefix, + packageNamePluginInfix: + overrides.packageNamePluginInfix ?? + config?.globals?.namePluginInfix ?? + defaults.packageNamePluginInfix, + }; +} + +function resolveLocalTemplatePath(pointer: string, basePath: string): string { + if (isAbsolute(pointer)) { + throw new Error(`Template target may not be an absolute path`); + } + + if (pointer.startsWith('.')) { + return resolvePath(basePath, pointer, TEMPLATE_FILE_NAME); + } + + return require.resolve(`${pointer}/${TEMPLATE_FILE_NAME}`, { + paths: [basePath], + }); +} + +const partialTemplateDefinitionSchema = z.object({ + name: z.string(), + description: z.string().optional(), +}); + +async function peekLocalTemplateDefinition( + target: string, +): Promise { + const content = await fs.readFile(target, 'utf8'); + + const rawTemplate = parseYaml(content); + const parsed = partialTemplateDefinitionSchema.safeParse(rawTemplate); + if (!parsed.success) { + throw fromZodError(parsed.error); + } + + return { + name: parsed.data.name, + description: parsed.data.description, + target, + }; +} diff --git a/packages/cli/src/lib/new/preparation/resolvePackageParams.test.ts b/packages/cli/src/lib/new/preparation/resolvePackageParams.test.ts new file mode 100644 index 0000000000..df128d4755 --- /dev/null +++ b/packages/cli/src/lib/new/preparation/resolvePackageParams.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright 2024 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 { resolvePackageParams } from './resolvePackageParams'; + +describe.each([ + [ + { role: 'web-library', name: 'test' }, + { + packageName: '@internal/test', + packagePath: 'packages/test', + }, + ], + [ + { role: 'node-library', name: 'test' }, + { + packageName: '@internal/test', + packagePath: 'packages/test', + }, + ], + [ + { role: 'common-library', name: 'test' }, + { + packageName: '@internal/test', + packagePath: 'packages/test', + }, + ], + [ + { role: 'plugin-web-library', pluginId: 'test' }, + { + packageName: '@internal/plugin-test-react', + packagePath: 'plugins/test-react', + }, + ], + [ + { role: 'plugin-node-library', pluginId: 'test' }, + { + packageName: '@internal/plugin-test-node', + packagePath: 'plugins/test-node', + }, + ], + [ + { role: 'plugin-common-library', pluginId: 'test' }, + { + packageName: '@internal/plugin-test-common', + packagePath: 'plugins/test-common', + }, + ], + [ + { role: 'frontend-plugin', pluginId: 'test' }, + { + packageName: '@internal/plugin-test', + packagePath: 'plugins/test', + }, + ], + [ + { role: 'backend-plugin', pluginId: 'test' }, + { + packageName: '@internal/plugin-test-backend', + packagePath: 'plugins/test-backend', + }, + ], + [ + { role: 'frontend-plugin-module', pluginId: 'test1', moduleId: 'test2' }, + { + packageName: '@internal/plugin-test1-module-test2', + packagePath: 'plugins/test1-module-test2', + }, + ], + [ + { role: 'backend-plugin-module', pluginId: 'test1', moduleId: 'test2' }, + { + packageName: '@internal/plugin-test1-backend-module-test2', + packagePath: 'plugins/test1-backend-module-test2', + }, + ], +] as const)('resolvePackageInfo', (roleParams, packageInfo) => { + it(`should generate correct info with default config for ${roleParams.role}`, () => { + expect( + resolvePackageParams({ + roleParams, + packagePrefix: '@internal/', + pluginInfix: 'plugin-', + }), + ).toEqual(packageInfo); + }); +}); diff --git a/packages/cli/src/lib/new/preparation/resolvePackageParams.ts b/packages/cli/src/lib/new/preparation/resolvePackageParams.ts new file mode 100644 index 0000000000..8ec1ca3069 --- /dev/null +++ b/packages/cli/src/lib/new/preparation/resolvePackageParams.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2025 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 { join as joinPath } from 'path'; +import { PortableTemplateInputRoleParams } from '../types'; + +export type ResolvePackageParamsOptions = { + roleParams: PortableTemplateInputRoleParams; + pluginInfix: string; + packagePrefix: string; +}; + +export type PortableTemplatePackageInfo = { + packageName: string; + packagePath: string; +}; + +export function resolvePackageParams( + options: ResolvePackageParamsOptions, +): PortableTemplatePackageInfo { + const baseName = getBaseNameForRole(options.roleParams); + const isPlugin = options.roleParams.role.includes('plugin'); + const pluginInfix = isPlugin ? options.pluginInfix : ''; + return { + packageName: `${options.packagePrefix}${pluginInfix}${baseName}`, + packagePath: joinPath(isPlugin ? 'plugins' : 'packages', baseName), + }; +} + +function getBaseNameForRole( + roleParams: PortableTemplateInputRoleParams, +): string { + switch (roleParams.role) { + case 'web-library': + case 'node-library': + case 'common-library': + return roleParams.name; + case 'plugin-web-library': + return `${roleParams.pluginId}-react`; + case 'plugin-node-library': + return `${roleParams.pluginId}-node`; + case 'plugin-common-library': + return `${roleParams.pluginId}-common`; + case 'frontend-plugin': + return `${roleParams.pluginId}`; + case 'frontend-plugin-module': + return `${roleParams.pluginId}-module-${roleParams.moduleId}`; + case 'backend-plugin': + return `${roleParams.pluginId}-backend`; + case 'backend-plugin-module': + return `${roleParams.pluginId}-backend-module-${roleParams.moduleId}`; + default: + throw new Error(`Unknown role ${(roleParams as { role: string }).role}`); + } +} diff --git a/packages/cli/src/lib/new/preparation/selectTemplateInteractively.test.ts b/packages/cli/src/lib/new/preparation/selectTemplateInteractively.test.ts new file mode 100644 index 0000000000..adf815e7a6 --- /dev/null +++ b/packages/cli/src/lib/new/preparation/selectTemplateInteractively.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2025 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 { PortableTemplateConfig } from '../types'; +import inquirer from 'inquirer'; +import { withLogCollector } from '@backstage/test-utils'; +import { selectTemplateInteractively } from './selectTemplateInteractively'; + +describe('selectTemplateInteractively', () => { + const mockConfig = { + isUsingDefaultTemplates: false, + templatePointers: [ + { name: 'template1', target: '/path/to/template1' }, + { name: 'template2', target: '/path/to/template2' }, + ], + } as PortableTemplateConfig; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should select a template interactively', async () => { + jest.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ name: 'template1' }); + + const result = await selectTemplateInteractively(mockConfig); + + expect(result).toEqual({ name: 'template1', target: '/path/to/template1' }); + }); + + it('should error if interactive selections is not found', async () => { + jest + .spyOn(inquirer, 'prompt') + .mockResolvedValueOnce({ name: 'nonexistent' }); + + await expect(selectTemplateInteractively(mockConfig)).rejects.toThrow( + "Template 'nonexistent' not found", + ); + }); + + it('should use preselected template name', async () => { + const result = await selectTemplateInteractively(mockConfig, 'template2'); + + expect(result).toEqual({ name: 'template2', target: '/path/to/template2' }); + }); + + it('should throw an error if template is not found', async () => { + await expect( + selectTemplateInteractively(mockConfig, 'nonexistent'), + ).rejects.toThrow("Template 'nonexistent' not found"); + }); + + it('should rewrite plugin to frontend-plugin if default templates are used', async () => { + await expect( + selectTemplateInteractively(mockConfig, 'plugin'), + ).rejects.toThrow("Template 'plugin' not found"); + + const logs = await withLogCollector(async () => { + await expect( + selectTemplateInteractively( + { ...mockConfig, isUsingDefaultTemplates: true }, + 'plugin', + ), + ).rejects.toThrow("Template 'frontend-plugin' not found"); + }); + expect(logs).toEqual({ + log: [], + warn: [ + "DEPRECATION WARNING: The 'plugin' template is deprecated, use 'frontend-plugin' instead", + ], + error: [], + }); + }); +}); diff --git a/packages/cli/src/lib/new/preparation/selectTemplateInteractively.ts b/packages/cli/src/lib/new/preparation/selectTemplateInteractively.ts new file mode 100644 index 0000000000..154e8af56f --- /dev/null +++ b/packages/cli/src/lib/new/preparation/selectTemplateInteractively.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2025 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 { PortableTemplateConfig, PortableTemplatePointer } from '../types'; + +export async function selectTemplateInteractively( + config: PortableTemplateConfig, + preselectedTemplateName?: string, +): Promise { + let selectedName = preselectedTemplateName; + + if (config.isUsingDefaultTemplates && selectedName === 'plugin') { + console.warn( + `DEPRECATION WARNING: The 'plugin' template is deprecated, use 'frontend-plugin' instead`, + ); + selectedName = 'frontend-plugin'; + } + + if (!selectedName) { + const answers = await inquirer.prompt<{ name: string }>([ + { + type: 'list', + name: 'name', + message: 'What do you want to create?', + choices: config.templatePointers.map(t => + t.description + ? { name: `${t.name} - ${t.description}`, value: t.name } + : t.name, + ), + }, + ]); + selectedName = answers.name; + } + + const template = config.templatePointers.find(t => t.name === selectedName); + if (!template) { + throw new Error(`Template '${selectedName}' not found`); + } + return template; +} diff --git a/packages/cli/src/lib/new/types.ts b/packages/cli/src/lib/new/types.ts index 534f99f94f..9f221ea7aa 100644 --- a/packages/cli/src/lib/new/types.ts +++ b/packages/cli/src/lib/new/types.ts @@ -14,68 +14,104 @@ * limitations under the License. */ -import { Answers, DistinctQuestion } from 'inquirer'; +export type PortableTemplateConfig = { + /** + * The pointers to templates that can be used. + */ + templatePointers: PortableTemplatePointer[]; + + /** + * Whether the default set of templates are being used or not. + */ + isUsingDefaultTemplates: boolean; -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; - /** License to use for new packages */ license: string; - /** Creates a temporary directory. This will always be deleted after creation is done. */ - createTemporaryDirectory(name: string): Promise; + version: string; - /** Signal that the creation process got to a point where permanent modifications were made */ - markAsModified(): void; -} + private: boolean; -export type AnyOptions = Record; + publishRegistry?: string; -export type Prompt = DistinctQuestion & { - name: string; + packageNamePrefix: string; + + packageNamePluginInfix: string; }; -export interface Factory { - /** - * The name used for this factory. - */ +export const TEMPLATE_FILE_NAME = 'portable-template.yaml'; + +export type PortableTemplatePointer = { name: string; + description?: string; + target: string; +}; - /** - * A description that describes what this factory creates to the user. - */ - description: string; +export const TEMPLATE_ROLES = [ + 'web-library', + 'node-library', + 'common-library', + 'plugin-web-library', + 'plugin-node-library', + 'plugin-common-library', + 'frontend-plugin', + 'frontend-plugin-module', + 'backend-plugin', + 'backend-plugin-module', +] as const; - /** - * An optional options discovery step that is run - * before the prompts to potentially fill in some of the options. - */ - optionsDiscovery?(): Promise>; +export type PortableTemplateRole = (typeof TEMPLATE_ROLES)[number]; - /** - * Inquirer prompts that will be filled in either interactively or - * through command line arguments. - */ - optionsPrompts?: ReadonlyArray>; +export type PortableTemplateFile = { + path: string; + content: string; + syntax?: 'handlebars'; +}; - /** - * The main method of the factory that handles creation. - */ - create(options: TOptions, context?: CreateContext): Promise; -} +export type PortableTemplate = { + name: string; + role: PortableTemplateRole; + files: PortableTemplateFile[]; + values: Record; +}; -export type AnyFactory = Factory; +export type PortableTemplateParams = { + [KName in string]?: string | number | boolean; +}; -export function createFactory( - config: Factory, -): AnyFactory { - return config as AnyFactory; -} +export type PortableTemplateInputRoleParams = + | { + role: 'web-library' | 'node-library' | 'common-library'; + name: string; + } + | { + role: + | 'plugin-web-library' + | 'plugin-node-library' + | 'plugin-common-library' + | 'frontend-plugin' + | 'backend-plugin'; + pluginId: string; + } + | { + role: 'frontend-plugin-module' | 'backend-plugin-module'; + pluginId: string; + moduleId: string; + }; + +export type PortableTemplateInput = { + roleParams: PortableTemplateInputRoleParams; + + owner?: string; + + license: string; + + version: string; + + private: boolean; + + publishRegistry?: string; + + packageName: string; + + packagePath: string; +}; diff --git a/packages/cli/src/lib/tasks.test.ts b/packages/cli/src/lib/tasks.test.ts deleted file mode 100644 index 1119adb7f7..0000000000 --- a/packages/cli/src/lib/tasks.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 fs from 'fs-extra'; -import { templatingTask } from './tasks'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -describe('templatingTask', () => { - const mockDir = createMockDirectory(); - - it('should template a directory with mix of regular files and templates', async () => { - // Testing template directory - const tmplDir = 'test-tmpl'; - - // Temporary dest dir to write the template to - const destDir = 'test-dest'; - - // Files content - const testFileContent = 'testing'; - const testVersionFileContent = - "version: {{pluginVersion}} {{versionQuery 'mock-pkg'}}"; - - mockDir.setContent({ - [tmplDir]: { - sub: { - 'version.txt.hbs': testVersionFileContent, - }, - 'test.txt': testFileContent, - }, - [destDir]: {}, - }); - - await templatingTask( - mockDir.resolve(tmplDir), - mockDir.resolve(destDir), - { - pluginVersion: '0.0.0', - }, - () => '^0.1.2', - true, - ); - - await expect( - fs.readFile(mockDir.resolve(destDir, 'test.txt'), 'utf8'), - ).resolves.toBe(testFileContent); - await expect( - fs.readFile(mockDir.resolve(destDir, 'sub/version.txt'), 'utf8'), - ).resolves.toBe('version: 0.0.0 ^0.1.2'); - }); -}); diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index ffff2892fb..2922f34f35 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -15,15 +15,10 @@ */ 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 { assertError } from '@backstage/errors'; -import { paths } from './paths'; const exec = promisify(execCb); @@ -96,138 +91,3 @@ export class Task { } } } - -export async function templatingTask( - templateDir: string, - destinationDir: string, - context: any, - versionProvider: (name: string, versionHint?: string) => string, - isMonoRepo: boolean, -) { - const files = await recursive(templateDir).catch(error => { - throw new Error(`Failed to read template directory: ${error.message}`); - }); - - for (const file of files) { - const destinationFile = file.replace(templateDir, destinationDir); - await fs.ensureDir(dirname(destinationFile)); - - if (file.endsWith('.hbs')) { - await Task.forItem('templating', basename(file), async () => { - const destination = destinationFile.replace(/\.hbs$/, ''); - - const template = await fs.readFile(file); - const compiled = handlebars.compile(template.toString(), { - strict: true, - }); - const contents = compiled( - { name: basename(destination), ...context }, - { - helpers: { - versionQuery(name: string, versionHint: string | unknown) { - return versionProvider( - name, - typeof versionHint === 'string' ? versionHint : undefined, - ); - }, - }, - }, - ); - - await fs.writeFile(destination, contents).catch(error => { - throw new Error( - `Failed to create file: ${destination}: ${error.message}`, - ); - }); - }); - } else { - if (isMonoRepo && file.match('tsconfig.json')) { - continue; - } - - await Task.forItem('copying', basename(file), async () => { - await fs.copyFile(file, destinationFile).catch(error => { - const destination = destinationFile; - throw new Error( - `Failed to copy file to ${destination} : ${error.message}`, - ); - }); - }); - } - } -} - -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}`); - } -} - -export async function addToBackend( - name: string, - options: { - type: 'plugin' | 'module'; - }, -) { - if (await fs.pathExists(paths.resolveTargetRoot('packages/backend'))) { - await Task.forItem('backend', `adding ${options.type}`, async () => { - const backendFilePath = paths.resolveTargetRoot( - 'packages/backend/src/index.ts', - ); - if (!(await fs.pathExists(backendFilePath))) { - return; - } - - const content = await fs.readFile(backendFilePath, 'utf8'); - const lines = content.split('\n'); - const backendAddLine = `backend.add(import('${name}'));`; - - const backendStartIndex = lines.findIndex(line => - line.match(/backend.start/), - ); - - if (backendStartIndex !== -1) { - const [indentation] = lines[backendStartIndex].match(/^\s*/)!; - lines.splice(backendStartIndex, 0, `${indentation}${backendAddLine}`); - - const newContent = lines.join('\n'); - await fs.writeFile(backendFilePath, newContent, 'utf8'); - } - }); - } -} diff --git a/packages/cli/templates/default-backend-module/.eslintrc.js.hbs b/packages/cli/templates/backend-plugin-module/.eslintrc.js.hbs similarity index 100% rename from packages/cli/templates/default-backend-module/.eslintrc.js.hbs rename to packages/cli/templates/backend-plugin-module/.eslintrc.js.hbs diff --git a/packages/cli/templates/default-backend-module/README.md.hbs b/packages/cli/templates/backend-plugin-module/README.md.hbs similarity index 86% rename from packages/cli/templates/default-backend-module/README.md.hbs rename to packages/cli/templates/backend-plugin-module/README.md.hbs index ad7669beda..b389ac8d1e 100644 --- a/packages/cli/templates/default-backend-module/README.md.hbs +++ b/packages/cli/templates/backend-plugin-module/README.md.hbs @@ -1,4 +1,4 @@ -# {{name}} +# {{packageName}} The {{moduleId}} backend module for the {{pluginId}} plugin. diff --git a/packages/cli/templates/default-backend-module/package.json.hbs b/packages/cli/templates/backend-plugin-module/package.json.hbs similarity index 81% rename from packages/cli/templates/default-backend-module/package.json.hbs rename to packages/cli/templates/backend-plugin-module/package.json.hbs index 7e82e047ce..20d78380ca 100644 --- a/packages/cli/templates/default-backend-module/package.json.hbs +++ b/packages/cli/templates/backend-plugin-module/package.json.hbs @@ -1,17 +1,9 @@ { - "name": "{{name}}", + "name": "{{packageName}}", "description": "The {{moduleId}} backend module for the {{pluginId}} plugin.", - "version": "{{packageVersion}}", "main": "src/index.ts", "types": "src/index.ts", - "license": "{{license}}", -{{#if privatePackage}} - "private": {{privatePackage}}, -{{/if}} "publishConfig": { -{{#if npmRegistry}} - "registry": "{{npmRegistry}}", -{{/if}} "access": "public", "main": "dist/index.cjs.js", "types": "dist/index.d.ts" diff --git a/packages/cli/templates/backend-plugin-module/portable-template.yaml b/packages/cli/templates/backend-plugin-module/portable-template.yaml new file mode 100644 index 0000000000..b929d84591 --- /dev/null +++ b/packages/cli/templates/backend-plugin-module/portable-template.yaml @@ -0,0 +1,5 @@ +name: backend-plugin-module +role: backend-plugin-module +description: A new backend module that extends an existing backend plugin +values: + moduleVar: '{{ camelCase pluginId }}Module{{ upperFirst ( camelCase moduleId ) }}' diff --git a/packages/cli/templates/default-backend-module/src/index.ts.hbs b/packages/cli/templates/backend-plugin-module/src/index.ts.hbs similarity index 100% rename from packages/cli/templates/default-backend-module/src/index.ts.hbs rename to packages/cli/templates/backend-plugin-module/src/index.ts.hbs diff --git a/packages/cli/templates/default-backend-module/src/module.ts.hbs b/packages/cli/templates/backend-plugin-module/src/module.ts.hbs similarity index 100% rename from packages/cli/templates/default-backend-module/src/module.ts.hbs rename to packages/cli/templates/backend-plugin-module/src/module.ts.hbs diff --git a/packages/cli/templates/default-backend-plugin/.eslintrc.js.hbs b/packages/cli/templates/backend-plugin/.eslintrc.js.hbs similarity index 100% rename from packages/cli/templates/default-backend-plugin/.eslintrc.js.hbs rename to packages/cli/templates/backend-plugin/.eslintrc.js.hbs diff --git a/packages/cli/templates/default-backend-plugin/README.md.hbs b/packages/cli/templates/backend-plugin/README.md.hbs similarity index 73% rename from packages/cli/templates/default-backend-plugin/README.md.hbs rename to packages/cli/templates/backend-plugin/README.md.hbs index 00e83a33ec..76189c07c2 100644 --- a/packages/cli/templates/default-backend-plugin/README.md.hbs +++ b/packages/cli/templates/backend-plugin/README.md.hbs @@ -1,14 +1,14 @@ -# {{id}} +# {{pluginId}} This plugin backend was templated using the Backstage CLI. You should replace this text with a description of your plugin backend. ## Installation -This plugin is installed via the `{{name}}` package. To install it to your backend package, run the following command: +This plugin is installed via the `{{packageName}}` package. To install it to your backend package, run the following command: ```bash # From your root directory -yarn --cwd packages/backend add {{name}} +yarn --cwd packages/backend add {{packageName}} ``` Then add the plugin to your backend in `packages/backend/src/index.ts`: @@ -16,7 +16,7 @@ Then add the plugin to your backend in `packages/backend/src/index.ts`: ```ts const backend = createBackend(); // ... -backend.add(import('{{name}}')); +backend.add(import('{{packageName}}')); ``` ## Development diff --git a/packages/cli/templates/default-backend-plugin/dev/index.ts.hbs b/packages/cli/templates/backend-plugin/dev/index.ts.hbs similarity index 75% rename from packages/cli/templates/default-backend-plugin/dev/index.ts.hbs rename to packages/cli/templates/backend-plugin/dev/index.ts.hbs index ce1d2919c9..765031bb97 100644 --- a/packages/cli/templates/default-backend-plugin/dev/index.ts.hbs +++ b/packages/cli/templates/backend-plugin/dev/index.ts.hbs @@ -11,17 +11,17 @@ import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; // // Create a new todo item, standalone or for the sample component: // -// curl http://localhost:7007/api/{{id}}/todos -H 'Content-Type: application/json' -d '{"title": "My Todo"}' -// curl http://localhost:7007/api/{{id}}/todos -H 'Content-Type: application/json' -d '{"title": "My Todo", "entityRef": "component:default/sample"}' +// curl http://localhost:7007/api/{{pluginId}}/todos -H 'Content-Type: application/json' -d '{"title": "My Todo"}' +// curl http://localhost:7007/api/{{pluginId}}/todos -H 'Content-Type: application/json' -d '{"title": "My Todo", "entityRef": "component:default/sample"}' // // List TODOs: // -// curl http://localhost:7007/api/{{id}}/todos +// curl http://localhost:7007/api/{{pluginId}}/todos // // Explicitly make an unauthenticated request, or with service auth: // -// curl http://localhost:7007/api/{{id}}/todos -H 'Authorization: Bearer mock-none-token' -// curl http://localhost:7007/api/{{id}}/todos -H 'Authorization: Bearer mock-service-token' +// curl http://localhost:7007/api/{{pluginId}}/todos -H 'Authorization: Bearer mock-none-token' +// curl http://localhost:7007/api/{{pluginId}}/todos -H 'Authorization: Bearer mock-service-token' const backend = createBackend(); diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/backend-plugin/package.json.hbs similarity index 88% rename from packages/cli/templates/default-backend-plugin/package.json.hbs rename to packages/cli/templates/backend-plugin/package.json.hbs index 2f7c0931d8..ad3810dff3 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/backend-plugin/package.json.hbs @@ -1,16 +1,8 @@ { - "name": "{{name}}", - "version": "{{pluginVersion}}", + "name": "{{packageName}}", "main": "src/index.ts", "types": "src/index.ts", - "license": "{{license}}", -{{#if privatePackage}} - "private": {{privatePackage}}, -{{/if}} "publishConfig": { -{{#if npmRegistry}} - "registry": "{{npmRegistry}}", -{{/if}} "access": "public", "main": "dist/index.cjs.js", "types": "dist/index.d.ts" diff --git a/packages/cli/templates/backend-plugin/portable-template.yaml b/packages/cli/templates/backend-plugin/portable-template.yaml new file mode 100644 index 0000000000..d305ea77f3 --- /dev/null +++ b/packages/cli/templates/backend-plugin/portable-template.yaml @@ -0,0 +1,5 @@ +name: backend-plugin +role: backend-plugin +description: A new backend plugin +values: + pluginVar: '{{ camelCase pluginId }}Plugin' diff --git a/packages/cli/templates/default-backend-plugin/src/index.ts.hbs b/packages/cli/templates/backend-plugin/src/index.ts.hbs similarity index 100% rename from packages/cli/templates/default-backend-plugin/src/index.ts.hbs rename to packages/cli/templates/backend-plugin/src/index.ts.hbs diff --git a/packages/cli/templates/default-backend-plugin/src/plugin.test.ts.hbs b/packages/cli/templates/backend-plugin/src/plugin.test.ts.hbs similarity index 90% rename from packages/cli/templates/default-backend-plugin/src/plugin.test.ts.hbs rename to packages/cli/templates/backend-plugin/src/plugin.test.ts.hbs index b6f387383e..e105c6ebd2 100644 --- a/packages/cli/templates/default-backend-plugin/src/plugin.test.ts.hbs +++ b/packages/cli/templates/backend-plugin/src/plugin.test.ts.hbs @@ -17,12 +17,12 @@ describe('plugin', () => { features: [{{pluginVar}}], }); - await request(server).get('/api/{{id}}/todos').expect(200, { + await request(server).get('/api/{{pluginId}}/todos').expect(200, { items: [], }); const createRes = await request(server) - .post('/api/{{id}}/todos') + .post('/api/{{pluginId}}/todos') .send({ title: 'My Todo' }); expect(createRes.status).toBe(201); @@ -36,13 +36,13 @@ describe('plugin', () => { const createdTodoItem = createRes.body; await request(server) - .get('/api/{{id}}/todos') + .get('/api/{{pluginId}}/todos') .expect(200, { items: [createdTodoItem], }); await request(server) - .get(`/api/{{id}}/todos/${createdTodoItem.id}`) + .get(`/api/{{pluginId}}/todos/${createdTodoItem.id}`) .expect(200, createdTodoItem); }); @@ -71,7 +71,7 @@ describe('plugin', () => { }); const createRes = await request(server) - .post('/api/{{id}}/todos') + .post('/api/{{pluginId}}/todos') .send({ title: 'My Todo', entityRef: 'component:default/my-component' }); expect(createRes.status).toBe(201); diff --git a/packages/cli/templates/default-backend-plugin/src/plugin.ts.hbs b/packages/cli/templates/backend-plugin/src/plugin.ts.hbs similarity index 97% rename from packages/cli/templates/default-backend-plugin/src/plugin.ts.hbs rename to packages/cli/templates/backend-plugin/src/plugin.ts.hbs index a9cccc2af2..b71850ea84 100644 --- a/packages/cli/templates/default-backend-plugin/src/plugin.ts.hbs +++ b/packages/cli/templates/backend-plugin/src/plugin.ts.hbs @@ -12,7 +12,7 @@ import { createTodoListService } from './services/TodoListService'; * @public */ export const {{pluginVar}} = createBackendPlugin({ - pluginId: '{{id}}', + pluginId: '{{pluginId}}', register(env) { env.registerInit({ deps: { diff --git a/packages/cli/templates/default-backend-plugin/src/router.test.ts b/packages/cli/templates/backend-plugin/src/router.test.ts similarity index 100% rename from packages/cli/templates/default-backend-plugin/src/router.test.ts rename to packages/cli/templates/backend-plugin/src/router.test.ts diff --git a/packages/cli/templates/default-backend-plugin/src/router.ts b/packages/cli/templates/backend-plugin/src/router.ts similarity index 100% rename from packages/cli/templates/default-backend-plugin/src/router.ts rename to packages/cli/templates/backend-plugin/src/router.ts diff --git a/packages/cli/templates/default-backend-plugin/src/services/TodoListService/createTodoListService.ts b/packages/cli/templates/backend-plugin/src/services/TodoListService/createTodoListService.ts similarity index 100% rename from packages/cli/templates/default-backend-plugin/src/services/TodoListService/createTodoListService.ts rename to packages/cli/templates/backend-plugin/src/services/TodoListService/createTodoListService.ts diff --git a/packages/cli/templates/default-backend-plugin/src/services/TodoListService/index.ts b/packages/cli/templates/backend-plugin/src/services/TodoListService/index.ts similarity index 100% rename from packages/cli/templates/default-backend-plugin/src/services/TodoListService/index.ts rename to packages/cli/templates/backend-plugin/src/services/TodoListService/index.ts diff --git a/packages/cli/templates/default-backend-plugin/src/services/TodoListService/types.ts b/packages/cli/templates/backend-plugin/src/services/TodoListService/types.ts similarity index 100% rename from packages/cli/templates/default-backend-plugin/src/services/TodoListService/types.ts rename to packages/cli/templates/backend-plugin/src/services/TodoListService/types.ts diff --git a/packages/cli/templates/default-backend-plugin/src/setupTests.ts b/packages/cli/templates/backend-plugin/src/setupTests.ts similarity index 100% rename from packages/cli/templates/default-backend-plugin/src/setupTests.ts rename to packages/cli/templates/backend-plugin/src/setupTests.ts diff --git a/packages/cli/templates/default-backend-module/tsconfig.json b/packages/cli/templates/default-backend-module/tsconfig.json deleted file mode 100644 index 5ae9aeb62d..0000000000 --- a/packages/cli/templates/default-backend-module/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "@backstage/cli/config/tsconfig.json", - "include": ["src"], - "exclude": ["node_modules"], - "compilerOptions": { - "outDir": "dist-types", - "rootDir": "." - } -} diff --git a/packages/cli/templates/default-backend-plugin/tsconfig.json b/packages/cli/templates/default-backend-plugin/tsconfig.json deleted file mode 100644 index d77f0fe3b4..0000000000 --- a/packages/cli/templates/default-backend-plugin/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "@backstage/cli/config/tsconfig.json", - "include": [ - "src", - "dev", - "migrations" - ], - "exclude": ["node_modules"], - "compilerOptions": { - "outDir": "dist-types", - "rootDir": "." - } -} diff --git a/packages/cli/templates/default-common-plugin-package/README.md.hbs b/packages/cli/templates/default-common-plugin-package/README.md.hbs deleted file mode 100644 index 917e18d4b9..0000000000 --- a/packages/cli/templates/default-common-plugin-package/README.md.hbs +++ /dev/null @@ -1,5 +0,0 @@ -# {{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/tsconfig.json b/packages/cli/templates/default-common-plugin-package/tsconfig.json deleted file mode 100644 index 5ae9aeb62d..0000000000 --- a/packages/cli/templates/default-common-plugin-package/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "@backstage/cli/config/tsconfig.json", - "include": ["src"], - "exclude": ["node_modules"], - "compilerOptions": { - "outDir": "dist-types", - "rootDir": "." - } -} diff --git a/packages/cli/templates/default-node-plugin-package/README.md.hbs b/packages/cli/templates/default-node-plugin-package/README.md.hbs deleted file mode 100644 index a9fc97935e..0000000000 --- a/packages/cli/templates/default-node-plugin-package/README.md.hbs +++ /dev/null @@ -1,5 +0,0 @@ -# {{name}} - -Welcome to the Node.js library package for the {{id}} plugin! - -_This plugin was created through the Backstage CLI_ diff --git a/packages/cli/templates/default-node-plugin-package/tsconfig.json b/packages/cli/templates/default-node-plugin-package/tsconfig.json deleted file mode 100644 index 5ae9aeb62d..0000000000 --- a/packages/cli/templates/default-node-plugin-package/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "@backstage/cli/config/tsconfig.json", - "include": ["src"], - "exclude": ["node_modules"], - "compilerOptions": { - "outDir": "dist-types", - "rootDir": "." - } -} diff --git a/packages/cli/templates/default-plugin/tsconfig.json b/packages/cli/templates/default-plugin/tsconfig.json deleted file mode 100644 index b61e496175..0000000000 --- a/packages/cli/templates/default-plugin/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@backstage/cli/config/tsconfig.json", - "include": [ - "src", - "dev" - ], - "exclude": ["node_modules"], - "compilerOptions": { - "outDir": "dist-types", - "rootDir": "." - } -} diff --git a/packages/cli/templates/default-react-plugin-package/README.md.hbs b/packages/cli/templates/default-react-plugin-package/README.md.hbs deleted file mode 100644 index ab1c36da6b..0000000000 --- a/packages/cli/templates/default-react-plugin-package/README.md.hbs +++ /dev/null @@ -1,5 +0,0 @@ -# {{name}} - -Welcome to the web library package for the {{id}} plugin! - -_This plugin was created through the Backstage CLI_ diff --git a/packages/cli/templates/default-react-plugin-package/tsconfig.json b/packages/cli/templates/default-react-plugin-package/tsconfig.json deleted file mode 100644 index ce3409d31f..0000000000 --- a/packages/cli/templates/default-react-plugin-package/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "@backstage/cli/config/tsconfig.json", - "include": [ - "src", - ], - "exclude": ["node_modules"], - "compilerOptions": { - "outDir": "dist-types", - "rootDir": "." - } -} diff --git a/packages/cli/templates/default-common-plugin-package/.eslintrc.js.hbs b/packages/cli/templates/frontend-plugin/.eslintrc.js.hbs similarity index 100% rename from packages/cli/templates/default-common-plugin-package/.eslintrc.js.hbs rename to packages/cli/templates/frontend-plugin/.eslintrc.js.hbs diff --git a/packages/cli/templates/default-plugin/README.md.hbs b/packages/cli/templates/frontend-plugin/README.md.hbs similarity index 77% rename from packages/cli/templates/default-plugin/README.md.hbs rename to packages/cli/templates/frontend-plugin/README.md.hbs index 6d68ca8250..5eae32b7dc 100644 --- a/packages/cli/templates/default-plugin/README.md.hbs +++ b/packages/cli/templates/frontend-plugin/README.md.hbs @@ -1,12 +1,12 @@ -# {{id}} +# {{pluginId}} -Welcome to the {{id}} plugin! +Welcome to the {{pluginId}} plugin! _This plugin was created through the Backstage CLI_ ## Getting started -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/{{id}}](http://localhost:3000/{{id}}). +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/{{pluginId}}](http://localhost:3000/{{pluginId}}). You can also serve the plugin in isolation by running `yarn start` in the plugin directory. This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. diff --git a/packages/cli/templates/default-plugin/dev/index.tsx.hbs b/packages/cli/templates/frontend-plugin/dev/index.tsx.hbs similarity index 91% rename from packages/cli/templates/default-plugin/dev/index.tsx.hbs rename to packages/cli/templates/frontend-plugin/dev/index.tsx.hbs index da6c4fee04..61cd8f2695 100644 --- a/packages/cli/templates/default-plugin/dev/index.tsx.hbs +++ b/packages/cli/templates/frontend-plugin/dev/index.tsx.hbs @@ -7,6 +7,6 @@ createDevApp() .addPage({ element: <{{ extensionName }} />, title: 'Root Page', - path: '/{{ id }}', + path: '/{{pluginId}}', }) .render(); diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/frontend-plugin/package.json.hbs similarity index 90% rename from packages/cli/templates/default-plugin/package.json.hbs rename to packages/cli/templates/frontend-plugin/package.json.hbs index b4034fd742..64888c2d37 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/frontend-plugin/package.json.hbs @@ -1,16 +1,8 @@ { - "name": "{{name}}", - "version": "{{pluginVersion}}", + "name": "{{packageName}}", "main": "src/index.ts", "types": "src/index.ts", - "license": "{{license}}", -{{#if privatePackage}} - "private": {{privatePackage}}, -{{/if}} "publishConfig": { -{{#if npmRegistry}} - "registry": "{{npmRegistry}}", -{{/if}} "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts" diff --git a/packages/cli/templates/frontend-plugin/portable-template.yaml b/packages/cli/templates/frontend-plugin/portable-template.yaml new file mode 100644 index 0000000000..3afb60eb22 --- /dev/null +++ b/packages/cli/templates/frontend-plugin/portable-template.yaml @@ -0,0 +1,6 @@ +name: frontend-plugin +role: frontend-plugin +description: A new frontend plugin +values: + pluginVar: '{{ camelCase pluginId }}Plugin' + extensionName: '{{ upperFirst ( camelCase pluginId ) }}Page' diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs similarity index 93% rename from packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs rename to packages/cli/templates/frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index c79977f301..81b4dec2c3 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli/templates/frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -23,7 +23,7 @@ describe('ExampleComponent', () => { it('should render', async () => { await renderInTestApp(); expect( - screen.getByText('Welcome to {{ id }}!'), + screen.getByText('Welcome to {{pluginId}}!'), ).toBeInTheDocument(); }); }); diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs b/packages/cli/templates/frontend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs similarity index 93% rename from packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs rename to packages/cli/templates/frontend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs index 1820671fa5..95a8f044d1 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs +++ b/packages/cli/templates/frontend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs @@ -13,7 +13,7 @@ import { ExampleFetchComponent } from '../ExampleFetchComponent'; export const ExampleComponent = () => ( -
+
diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/index.ts b/packages/cli/templates/frontend-plugin/src/components/ExampleComponent/index.ts similarity index 100% rename from packages/cli/templates/default-plugin/src/components/ExampleComponent/index.ts rename to packages/cli/templates/frontend-plugin/src/components/ExampleComponent/index.ts diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli/templates/frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs similarity index 100% rename from packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs rename to packages/cli/templates/frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs b/packages/cli/templates/frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs similarity index 100% rename from packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs rename to packages/cli/templates/frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/index.ts b/packages/cli/templates/frontend-plugin/src/components/ExampleFetchComponent/index.ts similarity index 100% rename from packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/index.ts rename to packages/cli/templates/frontend-plugin/src/components/ExampleFetchComponent/index.ts diff --git a/packages/cli/templates/default-plugin/src/index.ts.hbs b/packages/cli/templates/frontend-plugin/src/index.ts.hbs similarity index 100% rename from packages/cli/templates/default-plugin/src/index.ts.hbs rename to packages/cli/templates/frontend-plugin/src/index.ts.hbs diff --git a/packages/cli/templates/default-plugin/src/plugin.test.ts.hbs b/packages/cli/templates/frontend-plugin/src/plugin.test.ts.hbs similarity index 80% rename from packages/cli/templates/default-plugin/src/plugin.test.ts.hbs rename to packages/cli/templates/frontend-plugin/src/plugin.test.ts.hbs index 9d44a9c497..cf27fa33d2 100644 --- a/packages/cli/templates/default-plugin/src/plugin.test.ts.hbs +++ b/packages/cli/templates/frontend-plugin/src/plugin.test.ts.hbs @@ -1,6 +1,6 @@ import { {{ pluginVar }} } from './plugin'; -describe('{{ id }}', () => { +describe('{{pluginId}}', () => { it('should export plugin', () => { expect({{ pluginVar }}).toBeDefined(); }); diff --git a/packages/cli/templates/default-plugin/src/plugin.ts.hbs b/packages/cli/templates/frontend-plugin/src/plugin.ts.hbs similarity index 95% rename from packages/cli/templates/default-plugin/src/plugin.ts.hbs rename to packages/cli/templates/frontend-plugin/src/plugin.ts.hbs index 942eacf113..1ae984b20e 100644 --- a/packages/cli/templates/default-plugin/src/plugin.ts.hbs +++ b/packages/cli/templates/frontend-plugin/src/plugin.ts.hbs @@ -6,7 +6,7 @@ import { import { rootRouteRef } from './routes'; export const {{ pluginVar }} = createPlugin({ - id: '{{ id }}', + id: '{{pluginId}}', routes: { root: rootRouteRef, }, diff --git a/packages/cli/templates/default-plugin/src/routes.ts.hbs b/packages/cli/templates/frontend-plugin/src/routes.ts.hbs similarity index 83% rename from packages/cli/templates/default-plugin/src/routes.ts.hbs rename to packages/cli/templates/frontend-plugin/src/routes.ts.hbs index 7e5d8f85f0..491b05d933 100644 --- a/packages/cli/templates/default-plugin/src/routes.ts.hbs +++ b/packages/cli/templates/frontend-plugin/src/routes.ts.hbs @@ -1,5 +1,5 @@ import { createRouteRef } from '@backstage/core-plugin-api'; export const rootRouteRef = createRouteRef({ - id: '{{ id }}', + id: '{{pluginId}}', }); diff --git a/packages/cli/templates/default-plugin/src/setupTests.ts b/packages/cli/templates/frontend-plugin/src/setupTests.ts similarity index 100% rename from packages/cli/templates/default-plugin/src/setupTests.ts rename to packages/cli/templates/frontend-plugin/src/setupTests.ts diff --git a/packages/cli/templates/node-library-package/tsconfig.json b/packages/cli/templates/node-library-package/tsconfig.json deleted file mode 100644 index ce3409d31f..0000000000 --- a/packages/cli/templates/node-library-package/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "@backstage/cli/config/tsconfig.json", - "include": [ - "src", - ], - "exclude": ["node_modules"], - "compilerOptions": { - "outDir": "dist-types", - "rootDir": "." - } -} diff --git a/packages/cli/templates/default-node-plugin-package/.eslintrc.js.hbs b/packages/cli/templates/node-library/.eslintrc.js.hbs similarity index 100% rename from packages/cli/templates/default-node-plugin-package/.eslintrc.js.hbs rename to packages/cli/templates/node-library/.eslintrc.js.hbs diff --git a/packages/cli/templates/node-library-package/README.md.hbs b/packages/cli/templates/node-library/README.md.hbs similarity index 78% rename from packages/cli/templates/node-library-package/README.md.hbs rename to packages/cli/templates/node-library/README.md.hbs index 0e2813c87a..0f5f9a08b6 100644 --- a/packages/cli/templates/node-library-package/README.md.hbs +++ b/packages/cli/templates/node-library/README.md.hbs @@ -1,4 +1,4 @@ -# {{name}} +# {{packageName}} _This package was created through the Backstage CLI_. @@ -8,5 +8,5 @@ Install the package via Yarn: ```sh cd # if within a monorepo -yarn add {{name}} +yarn add {{packageName}} ``` diff --git a/packages/cli/templates/node-library-package/package.json.hbs b/packages/cli/templates/node-library/package.json.hbs similarity index 76% rename from packages/cli/templates/node-library-package/package.json.hbs rename to packages/cli/templates/node-library/package.json.hbs index d56a51bcd6..941bf309fd 100644 --- a/packages/cli/templates/node-library-package/package.json.hbs +++ b/packages/cli/templates/node-library/package.json.hbs @@ -1,16 +1,8 @@ { - "name": "{{name}}", - "version": "{{pluginVersion}}", + "name": "{{packageName}}", "main": "src/index.ts", "types": "src/index.ts", - "license": "{{license}}", -{{#if privatePackage}} - "private": {{privatePackage}}, -{{/if}} "publishConfig": { -{{#if npmRegistry}} - "registry": "{{npmRegistry}}", -{{/if}} "access": "public", "main": "dist/index.cjs.js", "types": "dist/index.d.ts" diff --git a/packages/cli/templates/node-library/portable-template.yaml b/packages/cli/templates/node-library/portable-template.yaml new file mode 100644 index 0000000000..a49f31043d --- /dev/null +++ b/packages/cli/templates/node-library/portable-template.yaml @@ -0,0 +1,3 @@ +name: node-library +role: node-library +description: A library package, exporting shared functionality for Node.js environments diff --git a/packages/cli/templates/node-library-package/src/index.ts.hbs b/packages/cli/templates/node-library/src/index.ts.hbs similarity index 100% rename from packages/cli/templates/node-library-package/src/index.ts.hbs rename to packages/cli/templates/node-library/src/index.ts.hbs diff --git a/packages/cli/templates/default-common-plugin-package/src/setupTests.ts b/packages/cli/templates/node-library/src/setupTests.ts similarity index 100% rename from packages/cli/templates/default-common-plugin-package/src/setupTests.ts rename to packages/cli/templates/node-library/src/setupTests.ts diff --git a/packages/cli/templates/default-plugin/.eslintrc.js.hbs b/packages/cli/templates/plugin-common-library/.eslintrc.js.hbs similarity index 100% rename from packages/cli/templates/default-plugin/.eslintrc.js.hbs rename to packages/cli/templates/plugin-common-library/.eslintrc.js.hbs diff --git a/packages/cli/templates/plugin-common-library/README.md.hbs b/packages/cli/templates/plugin-common-library/README.md.hbs new file mode 100644 index 0000000000..c29c19f19b --- /dev/null +++ b/packages/cli/templates/plugin-common-library/README.md.hbs @@ -0,0 +1,5 @@ +# {{packageName}} + +Welcome to the common package for the {{pluginId}} 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/plugin-common-library/package.json.hbs similarity index 71% rename from packages/cli/templates/default-common-plugin-package/package.json.hbs rename to packages/cli/templates/plugin-common-library/package.json.hbs index 689c3e6f53..085ccb40dd 100644 --- a/packages/cli/templates/default-common-plugin-package/package.json.hbs +++ b/packages/cli/templates/plugin-common-library/package.json.hbs @@ -1,17 +1,9 @@ { - "name": "{{name}}", - "description": "Common functionalities for the {{id}} plugin", - "version": "{{pluginVersion}}", + "name": "{{packageName}}", + "description": "Common functionalities for the {{pluginId}} plugin", "main": "src/index.ts", "types": "src/index.ts", - "license": "{{license}}", -{{#if privatePackage}} - "private": {{privatePackage}}, -{{/if}} "publishConfig": { -{{#if npmRegistry}} - "registry": "{{npmRegistry}}", -{{/if}} "access": "public", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", diff --git a/packages/cli/templates/plugin-common-library/portable-template.yaml b/packages/cli/templates/plugin-common-library/portable-template.yaml new file mode 100644 index 0000000000..34414bd10c --- /dev/null +++ b/packages/cli/templates/plugin-common-library/portable-template.yaml @@ -0,0 +1,3 @@ +name: plugin-common-library +role: plugin-common-library +description: A new isomorphic common plugin package diff --git a/packages/cli/templates/default-common-plugin-package/src/index.ts.hbs b/packages/cli/templates/plugin-common-library/src/index.ts.hbs similarity index 85% rename from packages/cli/templates/default-common-plugin-package/src/index.ts.hbs rename to packages/cli/templates/plugin-common-library/src/index.ts.hbs index 6584512572..c347bc7a65 100644 --- a/packages/cli/templates/default-common-plugin-package/src/index.ts.hbs +++ b/packages/cli/templates/plugin-common-library/src/index.ts.hbs @@ -1,6 +1,6 @@ /***/ /** - * Common functionalities for the {{id}} plugin. + * Common functionalities for the {{pluginId}} plugin. * * @packageDocumentation */ diff --git a/packages/cli/templates/default-node-plugin-package/src/setupTests.ts b/packages/cli/templates/plugin-common-library/src/setupTests.ts similarity index 100% rename from packages/cli/templates/default-node-plugin-package/src/setupTests.ts rename to packages/cli/templates/plugin-common-library/src/setupTests.ts diff --git a/packages/cli/templates/default-react-plugin-package/.eslintrc.js.hbs b/packages/cli/templates/plugin-node-library/.eslintrc.js.hbs similarity index 100% rename from packages/cli/templates/default-react-plugin-package/.eslintrc.js.hbs rename to packages/cli/templates/plugin-node-library/.eslintrc.js.hbs diff --git a/packages/cli/templates/plugin-node-library/README.md.hbs b/packages/cli/templates/plugin-node-library/README.md.hbs new file mode 100644 index 0000000000..7eaa344f7c --- /dev/null +++ b/packages/cli/templates/plugin-node-library/README.md.hbs @@ -0,0 +1,5 @@ +# {{packageName}} + +Welcome to the Node.js library package for the {{pluginId}} plugin! + +_This plugin was created through the Backstage CLI_ diff --git a/packages/cli/templates/default-node-plugin-package/package.json.hbs b/packages/cli/templates/plugin-node-library/package.json.hbs similarity index 69% rename from packages/cli/templates/default-node-plugin-package/package.json.hbs rename to packages/cli/templates/plugin-node-library/package.json.hbs index d58f95b3b2..182ef1c93f 100644 --- a/packages/cli/templates/default-node-plugin-package/package.json.hbs +++ b/packages/cli/templates/plugin-node-library/package.json.hbs @@ -1,17 +1,9 @@ { - "name": "{{name}}", - "description": "Node.js library for the {{id}} plugin", - "version": "{{pluginVersion}}", + "name": "{{packageName}}", + "description": "Node.js library for the {{pluginId}} plugin", "main": "src/index.ts", "types": "src/index.ts", - "license": "{{license}}", -{{#if privatePackage}} - "private": {{privatePackage}}, -{{/if}} "publishConfig": { -{{#if npmRegistry}} - "registry": "{{npmRegistry}}", -{{/if}} "access": "public", "main": "dist/index.cjs.js", "types": "dist/index.d.ts" diff --git a/packages/cli/templates/plugin-node-library/portable-template.yaml b/packages/cli/templates/plugin-node-library/portable-template.yaml new file mode 100644 index 0000000000..e71a55d40a --- /dev/null +++ b/packages/cli/templates/plugin-node-library/portable-template.yaml @@ -0,0 +1,3 @@ +name: plugin-node-library +role: plugin-node-library +description: A new Node.js library plugin package diff --git a/packages/cli/templates/default-node-plugin-package/src/index.ts.hbs b/packages/cli/templates/plugin-node-library/src/index.ts.hbs similarity index 84% rename from packages/cli/templates/default-node-plugin-package/src/index.ts.hbs rename to packages/cli/templates/plugin-node-library/src/index.ts.hbs index 355b35c373..ce180ec7ec 100644 --- a/packages/cli/templates/default-node-plugin-package/src/index.ts.hbs +++ b/packages/cli/templates/plugin-node-library/src/index.ts.hbs @@ -1,6 +1,6 @@ /***/ /** - * Node.js library for the {{id}} plugin. + * Node.js library for the {{pluginId}} plugin. * * @packageDocumentation */ diff --git a/packages/cli/templates/node-library-package/src/setupTests.ts b/packages/cli/templates/plugin-node-library/src/setupTests.ts similarity index 100% rename from packages/cli/templates/node-library-package/src/setupTests.ts rename to packages/cli/templates/plugin-node-library/src/setupTests.ts diff --git a/packages/cli/templates/node-library-package/.eslintrc.js.hbs b/packages/cli/templates/plugin-web-library/.eslintrc.js.hbs similarity index 100% rename from packages/cli/templates/node-library-package/.eslintrc.js.hbs rename to packages/cli/templates/plugin-web-library/.eslintrc.js.hbs diff --git a/packages/cli/templates/plugin-web-library/README.md.hbs b/packages/cli/templates/plugin-web-library/README.md.hbs new file mode 100644 index 0000000000..8d8647399a --- /dev/null +++ b/packages/cli/templates/plugin-web-library/README.md.hbs @@ -0,0 +1,5 @@ +# {{packageName}} + +Welcome to the web library package for the {{pluginId}} plugin! + +_This plugin was created through the Backstage CLI_ diff --git a/packages/cli/templates/default-react-plugin-package/package.json.hbs b/packages/cli/templates/plugin-web-library/package.json.hbs similarity index 83% rename from packages/cli/templates/default-react-plugin-package/package.json.hbs rename to packages/cli/templates/plugin-web-library/package.json.hbs index bea066630f..d104a29305 100644 --- a/packages/cli/templates/default-react-plugin-package/package.json.hbs +++ b/packages/cli/templates/plugin-web-library/package.json.hbs @@ -1,17 +1,9 @@ { - "name": "{{name}}", - "description": "Web library for the {{id}} plugin", - "version": "{{pluginVersion}}", + "name": "{{packageName}}", + "description": "Web library for the {{pluginId}} plugin", "main": "src/index.ts", "types": "src/index.ts", - "license": "{{license}}", -{{#if privatePackage}} - "private": {{privatePackage}}, -{{/if}} "publishConfig": { -{{#if npmRegistry}} - "registry": "{{npmRegistry}}", -{{/if}} "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts" diff --git a/packages/cli/templates/plugin-web-library/portable-template.yaml b/packages/cli/templates/plugin-web-library/portable-template.yaml new file mode 100644 index 0000000000..19be5e79a3 --- /dev/null +++ b/packages/cli/templates/plugin-web-library/portable-template.yaml @@ -0,0 +1,3 @@ +name: plugin-web-library +role: plugin-web-library +description: A new web library plugin package diff --git a/packages/cli/templates/default-react-plugin-package/src/components/ExampleComponent/ExampleComponent.test.tsx b/packages/cli/templates/plugin-web-library/src/components/ExampleComponent/ExampleComponent.test.tsx similarity index 100% rename from packages/cli/templates/default-react-plugin-package/src/components/ExampleComponent/ExampleComponent.test.tsx rename to packages/cli/templates/plugin-web-library/src/components/ExampleComponent/ExampleComponent.test.tsx diff --git a/packages/cli/templates/default-react-plugin-package/src/components/ExampleComponent/ExampleComponent.tsx b/packages/cli/templates/plugin-web-library/src/components/ExampleComponent/ExampleComponent.tsx similarity index 100% rename from packages/cli/templates/default-react-plugin-package/src/components/ExampleComponent/ExampleComponent.tsx rename to packages/cli/templates/plugin-web-library/src/components/ExampleComponent/ExampleComponent.tsx diff --git a/packages/cli/templates/default-react-plugin-package/src/components/ExampleComponent/index.ts b/packages/cli/templates/plugin-web-library/src/components/ExampleComponent/index.ts similarity index 100% rename from packages/cli/templates/default-react-plugin-package/src/components/ExampleComponent/index.ts rename to packages/cli/templates/plugin-web-library/src/components/ExampleComponent/index.ts diff --git a/packages/cli/templates/default-react-plugin-package/src/components/index.ts b/packages/cli/templates/plugin-web-library/src/components/index.ts similarity index 100% rename from packages/cli/templates/default-react-plugin-package/src/components/index.ts rename to packages/cli/templates/plugin-web-library/src/components/index.ts diff --git a/packages/cli/templates/default-react-plugin-package/src/hooks/index.ts b/packages/cli/templates/plugin-web-library/src/hooks/index.ts similarity index 100% rename from packages/cli/templates/default-react-plugin-package/src/hooks/index.ts rename to packages/cli/templates/plugin-web-library/src/hooks/index.ts diff --git a/packages/cli/templates/default-react-plugin-package/src/hooks/useExample/index.ts b/packages/cli/templates/plugin-web-library/src/hooks/useExample/index.ts similarity index 100% rename from packages/cli/templates/default-react-plugin-package/src/hooks/useExample/index.ts rename to packages/cli/templates/plugin-web-library/src/hooks/useExample/index.ts diff --git a/packages/cli/templates/default-react-plugin-package/src/hooks/useExample/useExample.ts b/packages/cli/templates/plugin-web-library/src/hooks/useExample/useExample.ts similarity index 100% rename from packages/cli/templates/default-react-plugin-package/src/hooks/useExample/useExample.ts rename to packages/cli/templates/plugin-web-library/src/hooks/useExample/useExample.ts diff --git a/packages/cli/templates/default-react-plugin-package/src/index.ts.hbs b/packages/cli/templates/plugin-web-library/src/index.ts.hbs similarity index 83% rename from packages/cli/templates/default-react-plugin-package/src/index.ts.hbs rename to packages/cli/templates/plugin-web-library/src/index.ts.hbs index 9b22677a3e..70f1987895 100644 --- a/packages/cli/templates/default-react-plugin-package/src/index.ts.hbs +++ b/packages/cli/templates/plugin-web-library/src/index.ts.hbs @@ -1,6 +1,6 @@ /***/ /** - * Web library for the {{id}} plugin. + * Web library for the {{pluginId}} plugin. * * @packageDocumentation */ diff --git a/packages/cli/templates/default-react-plugin-package/src/setupTests.ts b/packages/cli/templates/plugin-web-library/src/setupTests.ts similarity index 100% rename from packages/cli/templates/default-react-plugin-package/src/setupTests.ts rename to packages/cli/templates/plugin-web-library/src/setupTests.ts diff --git a/packages/cli/templates/scaffolder-module/.eslintrc.js.hbs b/packages/cli/templates/scaffolder-backend-module/.eslintrc.js.hbs similarity index 100% rename from packages/cli/templates/scaffolder-module/.eslintrc.js.hbs rename to packages/cli/templates/scaffolder-backend-module/.eslintrc.js.hbs diff --git a/packages/cli/templates/scaffolder-backend-module/README.md.hbs b/packages/cli/templates/scaffolder-backend-module/README.md.hbs new file mode 100644 index 0000000000..141699922d --- /dev/null +++ b/packages/cli/templates/scaffolder-backend-module/README.md.hbs @@ -0,0 +1,5 @@ +# {{packageName}} + +The {{moduleId}} 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-backend-module/package.json.hbs similarity index 77% rename from packages/cli/templates/scaffolder-module/package.json.hbs rename to packages/cli/templates/scaffolder-backend-module/package.json.hbs index 25cd8616f2..3866a83672 100644 --- a/packages/cli/templates/scaffolder-module/package.json.hbs +++ b/packages/cli/templates/scaffolder-backend-module/package.json.hbs @@ -1,17 +1,9 @@ { - "name": "{{name}}", - "description": "The {{id}} module for @backstage/plugin-scaffolder-backend", - "version": "{{pluginVersion}}", + "name": "{{packageName}}", + "description": "The {{moduleId}} module for @backstage/plugin-scaffolder-backend", "main": "src/index.ts", "types": "src/index.ts", - "license": "{{license}}", -{{#if privatePackage}} - "private": {{privatePackage}}, -{{/if}} "publishConfig": { -{{#if npmRegistry}} - "registry": "{{npmRegistry}}", -{{/if}} "access": "public", "main": "dist/index.cjs.js", "types": "dist/index.d.ts" diff --git a/packages/cli/templates/scaffolder-backend-module/portable-template.yaml b/packages/cli/templates/scaffolder-backend-module/portable-template.yaml new file mode 100644 index 0000000000..a131c14062 --- /dev/null +++ b/packages/cli/templates/scaffolder-backend-module/portable-template.yaml @@ -0,0 +1,6 @@ +name: scaffolder-backend-module +role: backend-plugin-module +description: A module exporting custom actions for @backstage/plugin-scaffolder-backend +values: + pluginId: scaffolder + moduleVar: '{{ camelCase pluginId }}Module{{ upperFirst ( camelCase moduleId ) }}' diff --git a/packages/cli/templates/scaffolder-module/src/actions/example.test.ts b/packages/cli/templates/scaffolder-backend-module/src/actions/example.test.ts similarity index 100% rename from packages/cli/templates/scaffolder-module/src/actions/example.test.ts rename to packages/cli/templates/scaffolder-backend-module/src/actions/example.test.ts diff --git a/packages/cli/templates/scaffolder-module/src/actions/example.ts b/packages/cli/templates/scaffolder-backend-module/src/actions/example.ts similarity index 100% rename from packages/cli/templates/scaffolder-module/src/actions/example.ts rename to packages/cli/templates/scaffolder-backend-module/src/actions/example.ts diff --git a/packages/cli/templates/scaffolder-module/src/index.ts.hbs b/packages/cli/templates/scaffolder-backend-module/src/index.ts.hbs similarity index 58% rename from packages/cli/templates/scaffolder-module/src/index.ts.hbs rename to packages/cli/templates/scaffolder-backend-module/src/index.ts.hbs index 58d5da4a57..e278e6f4b2 100644 --- a/packages/cli/templates/scaffolder-module/src/index.ts.hbs +++ b/packages/cli/templates/scaffolder-backend-module/src/index.ts.hbs @@ -1,6 +1,6 @@ /***/ /** - * The {{id}} module for @backstage/plugin-scaffolder-backend. + * The {{moduleId}} module for @backstage/plugin-scaffolder-backend. * * @packageDocumentation */ diff --git a/packages/cli/templates/scaffolder-module/src/module.ts b/packages/cli/templates/scaffolder-backend-module/src/module.ts similarity index 100% rename from packages/cli/templates/scaffolder-module/src/module.ts rename to packages/cli/templates/scaffolder-backend-module/src/module.ts diff --git a/packages/cli/templates/scaffolder-module/README.md.hbs b/packages/cli/templates/scaffolder-module/README.md.hbs deleted file mode 100644 index 8ee653a4ac..0000000000 --- a/packages/cli/templates/scaffolder-module/README.md.hbs +++ /dev/null @@ -1,5 +0,0 @@ -# {{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/tsconfig.json b/packages/cli/templates/scaffolder-module/tsconfig.json deleted file mode 100644 index 5ae9aeb62d..0000000000 --- a/packages/cli/templates/scaffolder-module/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "@backstage/cli/config/tsconfig.json", - "include": ["src"], - "exclude": ["node_modules"], - "compilerOptions": { - "outDir": "dist-types", - "rootDir": "." - } -} diff --git a/packages/cli/templates/web-library-package/tsconfig.json b/packages/cli/templates/web-library-package/tsconfig.json deleted file mode 100644 index ce3409d31f..0000000000 --- a/packages/cli/templates/web-library-package/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "@backstage/cli/config/tsconfig.json", - "include": [ - "src", - ], - "exclude": ["node_modules"], - "compilerOptions": { - "outDir": "dist-types", - "rootDir": "." - } -} diff --git a/packages/cli/templates/web-library-package/.eslintrc.js.hbs b/packages/cli/templates/web-library/.eslintrc.js.hbs similarity index 100% rename from packages/cli/templates/web-library-package/.eslintrc.js.hbs rename to packages/cli/templates/web-library/.eslintrc.js.hbs diff --git a/packages/cli/templates/web-library-package/README.md.hbs b/packages/cli/templates/web-library/README.md.hbs similarity index 78% rename from packages/cli/templates/web-library-package/README.md.hbs rename to packages/cli/templates/web-library/README.md.hbs index 0e2813c87a..0f5f9a08b6 100644 --- a/packages/cli/templates/web-library-package/README.md.hbs +++ b/packages/cli/templates/web-library/README.md.hbs @@ -1,4 +1,4 @@ -# {{name}} +# {{packageName}} _This package was created through the Backstage CLI_. @@ -8,5 +8,5 @@ Install the package via Yarn: ```sh cd # if within a monorepo -yarn add {{name}} +yarn add {{packageName}} ``` diff --git a/packages/cli/templates/web-library-package/package.json.hbs b/packages/cli/templates/web-library/package.json.hbs similarity index 78% rename from packages/cli/templates/web-library-package/package.json.hbs rename to packages/cli/templates/web-library/package.json.hbs index 64a693dd4c..52e54b1783 100644 --- a/packages/cli/templates/web-library-package/package.json.hbs +++ b/packages/cli/templates/web-library/package.json.hbs @@ -1,16 +1,8 @@ { - "name": "{{name}}", - "version": "{{pluginVersion}}", + "name": "{{packageName}}", "main": "src/index.ts", "types": "src/index.ts", - "license": "{{license}}", -{{#if privatePackage}} - "private": {{privatePackage}}, -{{/if}} "publishConfig": { -{{#if npmRegistry}} - "registry": "{{npmRegistry}}", -{{/if}} "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts" diff --git a/packages/cli/templates/web-library/portable-template.yaml b/packages/cli/templates/web-library/portable-template.yaml new file mode 100644 index 0000000000..595fc13616 --- /dev/null +++ b/packages/cli/templates/web-library/portable-template.yaml @@ -0,0 +1,3 @@ +name: web-library +role: web-library +description: A library package, exporting shared functionality for web environments diff --git a/packages/cli/templates/web-library-package/src/index.ts.hbs b/packages/cli/templates/web-library/src/index.ts.hbs similarity index 100% rename from packages/cli/templates/web-library-package/src/index.ts.hbs rename to packages/cli/templates/web-library/src/index.ts.hbs diff --git a/packages/cli/templates/web-library-package/src/setupTests.ts b/packages/cli/templates/web-library/src/setupTests.ts similarity index 100% rename from packages/cli/templates/web-library-package/src/setupTests.ts rename to packages/cli/templates/web-library/src/setupTests.ts diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 7b9413e63e..af519905e9 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -22,7 +22,7 @@ "lint": "backstage-cli repo lint --since origin/{{defaultBranch}}", "lint:all": "backstage-cli repo lint", "prettier:check": "prettier --check .", - "new": "backstage-cli new --scope internal" + "new": "backstage-cli new" }, "workspaces": { "packages": [ diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 77e2e83c4c..50ce3c6781 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -40,7 +40,7 @@ import { OptionValues } from 'commander'; const paths = findPaths(__dirname); const templatePackagePaths = [ - 'packages/cli/templates/default-plugin/package.json.hbs', + 'packages/cli/templates/frontend-plugin/package.json.hbs', 'packages/create-app/templates/default-app/package.json.hbs', 'packages/create-app/templates/default-app/packages/app/package.json.hbs', 'packages/create-app/templates/default-app/packages/backend/package.json.hbs', diff --git a/yarn.lock b/yarn.lock index 074909bfa3..8ae7f225e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4050,6 +4050,7 @@ __metadata: yml-loader: ^2.1.0 yn: ^4.0.0 zod: ^3.22.4 + zod-validation-error: ^3.4.0 peerDependencies: "@rspack/core": ^1.0.10 "@rspack/dev-server": ^1.0.9