diff --git a/.changeset/add-cli-plugin-role.md b/.changeset/add-cli-plugin-role.md new file mode 100644 index 0000000000..1f09a08ade --- /dev/null +++ b/.changeset/add-cli-plugin-role.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': patch +--- + +Added a new `cli-module` package role for packages that provide CLI plugin extensions. diff --git a/.changeset/cli-defaults-introduce.md b/.changeset/cli-defaults-introduce.md new file mode 100644 index 0000000000..b6bb175e62 --- /dev/null +++ b/.changeset/cli-defaults-introduce.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-defaults': minor +--- + +Introduced `@backstage/cli-defaults`, a convenience package that bundles all standard Backstage CLI modules. Install this single package as a `devDependency` to get the full default set of CLI commands without listing each module individually. diff --git a/.changeset/cli-discover-modules.md b/.changeset/cli-discover-modules.md new file mode 100644 index 0000000000..dac5f73f75 --- /dev/null +++ b/.changeset/cli-discover-modules.md @@ -0,0 +1,41 @@ +--- +'@backstage/cli': minor +--- + +The CLI now automatically discovers CLI modules from the project root's `dependencies` and `devDependencies`. Any installed package with the `cli-module` Backstage role will be loaded automatically without needing to be hardcoded in the CLI itself. + +If no CLI modules are found in the project dependencies, the CLI falls back to the built-in set of modules and prints a deprecation warning. This fallback will be removed in a future release. To prepare for this, add `@backstage/cli-defaults` as a `devDependency` in your root `package.json`: + +```json +{ + "devDependencies": { + "@backstage/cli-defaults": "backstage:^" + } +} +``` + +If you are not using the Backstage Yarn plugin, run the following instead: + +```sh +yarn workspace root add --dev @backstage/cli-defaults +``` + +For fine-grained control you can instead install individual CLI modules: + +```json +{ + "devDependencies": { + "@backstage/cli-module-auth": "backstage:^", + "@backstage/cli-module-build": "backstage:^", + "@backstage/cli-module-config": "backstage:^", + "@backstage/cli-module-github": "backstage:^", + "@backstage/cli-module-info": "backstage:^", + "@backstage/cli-module-lint": "backstage:^", + "@backstage/cli-module-maintenance": "backstage:^", + "@backstage/cli-module-migrate": "backstage:^", + "@backstage/cli-module-new": "backstage:^", + "@backstage/cli-module-test-jest": "backstage:^", + "@backstage/cli-module-translations": "backstage:^" + } +} +``` diff --git a/.changeset/cli-node-add-cli-plugin.md b/.changeset/cli-node-add-cli-plugin.md new file mode 100644 index 0000000000..2a6a5f20bc --- /dev/null +++ b/.changeset/cli-node-add-cli-plugin.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': minor +--- + +Added `createCliModule` API and related types for building Backstage CLI plugins. diff --git a/.changeset/cli-support-cli-plugin-role.md b/.changeset/cli-support-cli-plugin-role.md new file mode 100644 index 0000000000..b336f7f551 --- /dev/null +++ b/.changeset/cli-support-cli-plugin-role.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added support for the new `cli-module` package role in the build system, ESLint configuration, Jest configuration, and maintenance commands. diff --git a/.changeset/cli-use-cli-plugin-api.md b/.changeset/cli-use-cli-plugin-api.md new file mode 100644 index 0000000000..c155483c10 --- /dev/null +++ b/.changeset/cli-use-cli-plugin-api.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Migrated CLI plugin modules to use `createCliModule` from `@backstage/cli-node`. diff --git a/.changeset/create-app-cli-modules.md b/.changeset/create-app-cli-modules.md new file mode 100644 index 0000000000..c1a8ca9a26 --- /dev/null +++ b/.changeset/create-app-cli-modules.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +The create-app templates now include `@backstage/cli-defaults` as a `devDependency`, enabling the CLI's automatic module discovery for newly created projects. diff --git a/.changeset/introduce-cli-modules.md b/.changeset/introduce-cli-modules.md new file mode 100644 index 0000000000..7381742621 --- /dev/null +++ b/.changeset/introduce-cli-modules.md @@ -0,0 +1,15 @@ +--- +'@backstage/cli-module-auth': minor +'@backstage/cli-module-build': minor +'@backstage/cli-module-config': minor +'@backstage/cli-module-github': minor +'@backstage/cli-module-info': minor +'@backstage/cli-module-lint': minor +'@backstage/cli-module-maintenance': minor +'@backstage/cli-module-migrate': minor +'@backstage/cli-module-new': minor +'@backstage/cli-module-test-jest': minor +'@backstage/cli-module-translations': minor +--- + +Initial release of the CLI module packages. Each module provides a set of commands that can be discovered automatically by `@backstage/cli` or executed standalone. diff --git a/package.json b/package.json index d94bdadcbd..c89dcefce6 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,6 @@ "plugins/*" ], "scripts": { - "build-storybook": "storybook build --output-dir dist-storybook", - "build-storybook:chromatic": "STORYBOOK_STORY_SET=chromatic storybook build --stats-json --output-dir dist-storybook", "build:all": "backstage-cli repo build --all", "build:api-docs": "LANG=en_EN yarn build:api-reports --docs --exclude 'plugins/@(api-docs|api-docs-module-protoc-gen-doc|app-visualizer|catalog-graph|catalog-import|catalog-unprocessed-entities|config-schema|example-todo-list|example-todo-list-backend)'", "build:api-reports": "yarn build:api-reports:only --tsc", @@ -30,6 +28,8 @@ "build:backend": "yarn workspace example-backend build", "build:knip-reports": "backstage-repo-tools knip-reports", "build:plugins-report": "node ./scripts/build-plugins-report", + "build-storybook": "storybook build --output-dir dist-storybook", + "build-storybook:chromatic": "STORYBOOK_STORY_SET=chromatic storybook build --stats-json --output-dir dist-storybook", "clean": "backstage-cli repo clean", "create-plugin": "echo \"use 'yarn new' instead\"", "dev": "echo \"use 'yarn start' instead\"", @@ -52,10 +52,10 @@ "snyk:test": "npx snyk test --yarn-workspaces --strict-out-of-sync=false", "snyk:test:package": "yarn snyk:test --include", "start": "backstage-cli repo start", - "start-backend": "echo \"Use 'yarn start example-backend' instead\"", "start:docker": "docker compose -f docker-compose.deps.yml up --wait && BACKSTAGE_ENV=docker yarn start", "start:legacy": "yarn start example-app-legacy example-backend", "start:microsite": "cd microsite/ && yarn start", + "start-backend": "echo \"Use 'yarn start example-backend' instead\"", "storybook": "storybook dev -p 6006", "sync-issue-templates": "node ./.github/ISSUE_TEMPLATE/sync.js", "techdocs-cli": "node scripts/techdocs-cli.js", @@ -125,6 +125,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:*", + "@backstage/cli-defaults": "workspace:*", "@backstage/codemods": "workspace:*", "@backstage/create-app": "workspace:*", "@backstage/e2e-test-utils": "workspace:*", diff --git a/packages/cli-defaults/.eslintrc.js b/packages/cli-defaults/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-defaults/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-defaults/README.md b/packages/cli-defaults/README.md new file mode 100644 index 0000000000..2066aa1e35 --- /dev/null +++ b/packages/cli-defaults/README.md @@ -0,0 +1,26 @@ +# @backstage/cli-defaults + +The default set of CLI modules for the Backstage CLI. Installing this single package provides all standard CLI commands without needing to list each module individually. + +## Included Modules + +| Module | Description | +| :----------------------------------------------------------------- | :--------------------------------------- | +| [`@backstage/cli-module-auth`](../cli-module-auth) | Authentication commands | +| [`@backstage/cli-module-build`](../cli-module-build) | Build, start, and packaging commands | +| [`@backstage/cli-module-config`](../cli-module-config) | Configuration inspection commands | +| [`@backstage/cli-module-github`](../cli-module-github) | GitHub App creation | +| [`@backstage/cli-module-info`](../cli-module-info) | Environment and dependency info | +| [`@backstage/cli-module-lint`](../cli-module-lint) | Linting commands | +| [`@backstage/cli-module-maintenance`](../cli-module-maintenance) | Repository maintenance commands | +| [`@backstage/cli-module-migrate`](../cli-module-migrate) | Migration and version management | +| [`@backstage/cli-module-new`](../cli-module-new) | Scaffolding for new plugins and packages | +| [`@backstage/cli-module-test-jest`](../cli-module-test-jest) | Jest-based testing commands | +| [`@backstage/cli-module-translations`](../cli-module-translations) | Translation management commands | + +For fine-grained control over which CLI commands are available, you can install individual modules instead. + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-defaults/catalog-info.yaml b/packages/cli-defaults/catalog-info.yaml new file mode 100644 index 0000000000..3cac883c36 --- /dev/null +++ b/packages/cli-defaults/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-defaults + title: '@backstage/cli-defaults' + description: Default set of CLI modules for the Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-defaults/package.json b/packages/cli-defaults/package.json new file mode 100644 index 0000000000..8a065fd4d7 --- /dev/null +++ b/packages/cli-defaults/package.json @@ -0,0 +1,48 @@ +{ + "name": "@backstage/cli-defaults", + "version": "0.0.0", + "description": "Default set of CLI modules for the Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-defaults" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/cli-module-auth": "workspace:^", + "@backstage/cli-module-build": "workspace:^", + "@backstage/cli-module-config": "workspace:^", + "@backstage/cli-module-github": "workspace:^", + "@backstage/cli-module-info": "workspace:^", + "@backstage/cli-module-lint": "workspace:^", + "@backstage/cli-module-maintenance": "workspace:^", + "@backstage/cli-module-migrate": "workspace:^", + "@backstage/cli-module-new": "workspace:^", + "@backstage/cli-module-test-jest": "workspace:^", + "@backstage/cli-module-translations": "workspace:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } +} diff --git a/packages/cli-defaults/report.api.md b/packages/cli-defaults/report.api.md new file mode 100644 index 0000000000..573f8ee454 --- /dev/null +++ b/packages/cli-defaults/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public +const _default: CliModule[]; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli-defaults/src/index.ts b/packages/cli-defaults/src/index.ts new file mode 100644 index 0000000000..dd7aed0c70 --- /dev/null +++ b/packages/cli-defaults/src/index.ts @@ -0,0 +1,45 @@ +/* + * 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 auth from '@backstage/cli-module-auth'; +import build from '@backstage/cli-module-build'; +import config from '@backstage/cli-module-config'; +import github from '@backstage/cli-module-github'; +import info from '@backstage/cli-module-info'; +import lint from '@backstage/cli-module-lint'; +import maintenance from '@backstage/cli-module-maintenance'; +import migrate from '@backstage/cli-module-migrate'; +import newModule from '@backstage/cli-module-new'; +import testJest from '@backstage/cli-module-test-jest'; +import translations from '@backstage/cli-module-translations'; + +/** + * The default set of CLI modules for the Backstage CLI. + * + * @public + */ +export default [ + auth, + build, + config, + github, + info, + lint, + maintenance, + migrate, + newModule, + testJest, + translations, +]; diff --git a/packages/cli-internal/.eslintrc.js b/packages/cli-internal/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-internal/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-internal/catalog-info.yaml b/packages/cli-internal/catalog-info.yaml new file mode 100644 index 0000000000..2f2a1c837a --- /dev/null +++ b/packages/cli-internal/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: internal-cli + title: '@internal/cli' +spec: + lifecycle: experimental + type: backstage-node-library + owner: tooling-maintainers diff --git a/packages/cli-internal/package.json b/packages/cli-internal/package.json new file mode 100644 index 0000000000..068735bac3 --- /dev/null +++ b/packages/cli-internal/package.json @@ -0,0 +1,30 @@ +{ + "name": "@internal/cli", + "version": "0.0.1", + "backstage": { + "role": "node-library", + "inline": true + }, + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-internal" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "lint": "backstage-cli package lint", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-node": "workspace:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } +} diff --git a/packages/cli-internal/src/InternalCliModule.ts b/packages/cli-internal/src/InternalCliModule.ts new file mode 100644 index 0000000000..213e309371 --- /dev/null +++ b/packages/cli-internal/src/InternalCliModule.ts @@ -0,0 +1,30 @@ +/* + * 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 { CliCommand, CliModule } from '@backstage/cli-node'; +import { OpaqueType } from '@internal/opaque'; + +export const OpaqueCliModule = OpaqueType.create<{ + public: CliModule; + versions: { + readonly version: 'v1'; + readonly packageName: string; + readonly commands: Promise>; + }; +}>({ + type: '@backstage/CliModule', + versions: ['v1'], +}); diff --git a/packages/cli-internal/src/InternalCommandNode.ts b/packages/cli-internal/src/InternalCommandNode.ts new file mode 100644 index 0000000000..233e9128cb --- /dev/null +++ b/packages/cli-internal/src/InternalCommandNode.ts @@ -0,0 +1,69 @@ +/* + * 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 { CliCommand, CliModule } from '@backstage/cli-node'; +import { OpaqueType } from '@internal/opaque'; + +/** @internal */ +export interface CommandTreeNode { + readonly $$type: '@backstage/CommandTreeNode'; +} + +/** @internal */ +export interface CommandLeafNode { + readonly $$type: '@backstage/CommandLeafNode'; +} + +export type CommandNode = CommandTreeNode | CommandLeafNode; + +export const OpaqueCommandTreeNode = OpaqueType.create<{ + public: CommandTreeNode; + versions: { + readonly version: 'v1'; + readonly name: string; + readonly children: CommandNode[]; + }; +}>({ + type: '@backstage/CommandTreeNode', + versions: ['v1'], +}); + +export const OpaqueCommandLeafNode = OpaqueType.create<{ + public: CommandLeafNode; + versions: { + readonly version: 'v1'; + readonly name: string; + readonly command: CliCommand; + readonly module?: CliModule; + }; +}>({ + type: '@backstage/CommandLeafNode', + versions: ['v1'], +}); + +/** + * Checks whether a command node should be hidden from help output. + * Leaf nodes are hidden if they are deprecated or experimental. + * Tree nodes are hidden if all of their children are hidden. + */ +export function isCommandNodeHidden(node: CommandNode): boolean { + if (OpaqueCommandLeafNode.isType(node)) { + const { command } = OpaqueCommandLeafNode.toInternal(node); + return !!command.deprecated || !!command.experimental; + } + const { children } = OpaqueCommandTreeNode.toInternal(node); + return children.every(child => isCommandNodeHidden(child)); +} diff --git a/packages/cli/src/wiring/describeParentCallSite.ts b/packages/cli-internal/src/index.ts similarity index 61% rename from packages/cli/src/wiring/describeParentCallSite.ts rename to packages/cli-internal/src/index.ts index 35603e33b0..0e69d45032 100644 --- a/packages/cli/src/wiring/describeParentCallSite.ts +++ b/packages/cli-internal/src/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * 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. @@ -14,7 +14,14 @@ * limitations under the License. */ -// Single re-export to avoid doing this import in multiple places, but still -// avoid duplicate declarations because this one is a bit tricky. -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -export { describeParentCallSite } from '../../../frontend-plugin-api/src/routing/describeParentCallSite'; +export { OpaqueCliModule } from './InternalCliModule'; +export type { + CommandNode, + CommandTreeNode, + CommandLeafNode, +} from './InternalCommandNode'; +export { + OpaqueCommandTreeNode, + OpaqueCommandLeafNode, + isCommandNodeHidden, +} from './InternalCommandNode'; diff --git a/packages/cli-module-auth/.eslintrc.js b/packages/cli-module-auth/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-auth/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-auth/README.md b/packages/cli-module-auth/README.md new file mode 100644 index 0000000000..2a98761ac3 --- /dev/null +++ b/packages/cli-module-auth/README.md @@ -0,0 +1,19 @@ +# @backstage/cli-module-auth + +CLI module that provides authentication commands for the Backstage CLI, enabling login, logout, and credential management for Backstage instances. + +## Commands + +| Command | Description | +| :----------------- | :------------------------------------------------------- | +| `auth login` | Log in the CLI to a Backstage instance | +| `auth logout` | Log out the CLI and clear stored credentials | +| `auth show` | Show details of an authenticated instance | +| `auth list` | List authenticated instances | +| `auth print-token` | Print an access token to stdout (auto-refresh if needed) | +| `auth select` | Select the default instance | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-auth/bin/backstage-cli-module-auth b/packages/cli-module-auth/bin/backstage-cli-module-auth new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-auth/bin/backstage-cli-module-auth @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-auth/catalog-info.yaml b/packages/cli-module-auth/catalog-info.yaml new file mode 100644 index 0000000000..091bcad199 --- /dev/null +++ b/packages/cli-module-auth/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-auth + title: '@backstage/cli-module-auth' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-auth/cli-report.md b/packages/cli-module-auth/cli-report.md new file mode 100644 index 0000000000..8b4c98766f --- /dev/null +++ b/packages/cli-module-auth/cli-report.md @@ -0,0 +1,96 @@ +## CLI Report file for "@backstage/cli-module-auth" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-auth` + +``` +Usage: @backstage/cli-module-auth [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + auth [command] + help [command] +``` + +### `backstage-cli-module-auth auth` + +``` +Usage: @backstage/cli-module-auth auth [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + list + login + logout + print-token + select + show +``` + +### `backstage-cli-module-auth auth list` + +``` +Usage: @backstage/cli-module-auth auth list + +Options: + -h, --help +``` + +### `backstage-cli-module-auth auth login` + +``` +Usage: @backstage/cli-module-auth auth login + +Options: + --backend-url + --instance + --no-browser + -h, --help +``` + +### `backstage-cli-module-auth auth logout` + +``` +Usage: @backstage/cli-module-auth auth logout + +Options: + --instance + -h, --help +``` + +### `backstage-cli-module-auth auth print-token` + +``` +Usage: @backstage/cli-module-auth auth print-token + +Options: + --instance + -h, --help +``` + +### `backstage-cli-module-auth auth select` + +``` +Usage: @backstage/cli-module-auth auth select + +Options: + --instance + -h, --help +``` + +### `backstage-cli-module-auth auth show` + +``` +Usage: @backstage/cli-module-auth auth show + +Options: + --instance + -h, --help +``` diff --git a/packages/cli-module-auth/package.json b/packages/cli-module-auth/package.json new file mode 100644 index 0000000000..86bb491f7a --- /dev/null +++ b/packages/cli-module-auth/package.json @@ -0,0 +1,56 @@ +{ + "name": "@backstage/cli-module-auth", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-auth" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-node": "workspace:^", + "@backstage/errors": "workspace:^", + "cleye": "^2.3.0", + "cross-fetch": "^4.0.0", + "fs-extra": "^11.2.0", + "glob": "^7.1.7", + "inquirer": "^8.2.0", + "proper-lockfile": "^4.1.2", + "yaml": "^2.0.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^11.0.0", + "@types/proper-lockfile": "^4" + }, + "optionalDependencies": { + "keytar": "^7.9.0" + }, + "bin": "bin/backstage-cli-module-auth" +} diff --git a/packages/cli-module-auth/report.api.md b/packages/cli-module-auth/report.api.md new file mode 100644 index 0000000000..510bcaabe7 --- /dev/null +++ b/packages/cli-module-auth/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-auth" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/auth/commands/list.ts b/packages/cli-module-auth/src/commands/list.ts similarity index 89% rename from packages/cli/src/modules/auth/commands/list.ts rename to packages/cli-module-auth/src/commands/list.ts index c245072c88..3da2118800 100644 --- a/packages/cli/src/modules/auth/commands/list.ts +++ b/packages/cli-module-auth/src/commands/list.ts @@ -15,10 +15,10 @@ */ import { cli } from 'cleye'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; import { getAllInstances } from '../lib/storage'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info }, undefined, args); const { instances, selected } = await getAllInstances(); diff --git a/packages/cli/src/modules/auth/commands/login.ts b/packages/cli-module-auth/src/commands/login.ts similarity index 98% rename from packages/cli/src/modules/auth/commands/login.ts rename to packages/cli-module-auth/src/commands/login.ts index 777fcb1807..b16060f956 100644 --- a/packages/cli/src/modules/auth/commands/login.ts +++ b/packages/cli-module-auth/src/commands/login.ts @@ -15,7 +15,7 @@ */ import { cli } from 'cleye'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; import { startCallbackServer } from '../lib/localServer'; import { spawn } from 'node:child_process'; import { challengeFromVerifier, generateVerifier } from '../lib/pkce'; @@ -37,7 +37,7 @@ import inquirer from 'inquirer'; const TOKEN_EXCHANGE_TIMEOUT_MS = 30_000; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { backendUrl, noBrowser, instance: instanceFlag }, } = cli( diff --git a/packages/cli/src/modules/auth/commands/logout.ts b/packages/cli-module-auth/src/commands/logout.ts similarity index 94% rename from packages/cli/src/modules/auth/commands/logout.ts rename to packages/cli-module-auth/src/commands/logout.ts index 8277ea6335..f79ed2ef35 100644 --- a/packages/cli/src/modules/auth/commands/logout.ts +++ b/packages/cli-module-auth/src/commands/logout.ts @@ -15,7 +15,7 @@ */ import { cli } from 'cleye'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; import { getSecretStore } from '../lib/secretStore'; import { removeInstance, @@ -25,7 +25,7 @@ import { import { httpJson } from '../lib/http'; import { pickInstance } from '../lib/prompt'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { instance: instanceFlag }, } = cli( diff --git a/packages/cli/src/modules/auth/commands/printToken.ts b/packages/cli-module-auth/src/commands/printToken.ts similarity index 92% rename from packages/cli/src/modules/auth/commands/printToken.ts rename to packages/cli-module-auth/src/commands/printToken.ts index ea848a112a..39a78c1832 100644 --- a/packages/cli/src/modules/auth/commands/printToken.ts +++ b/packages/cli-module-auth/src/commands/printToken.ts @@ -15,12 +15,12 @@ */ import { cli } from 'cleye'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; import { accessTokenNeedsRefresh, refreshAccessToken } from '../lib/auth'; import { getSelectedInstance } from '../lib/storage'; import { getSecretStore } from '../lib/secretStore'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { instance: instanceFlag }, } = cli( diff --git a/packages/cli/src/modules/auth/commands/select.ts b/packages/cli-module-auth/src/commands/select.ts similarity index 90% rename from packages/cli/src/modules/auth/commands/select.ts rename to packages/cli-module-auth/src/commands/select.ts index 839fee691e..ebce23b1cb 100644 --- a/packages/cli/src/modules/auth/commands/select.ts +++ b/packages/cli-module-auth/src/commands/select.ts @@ -15,11 +15,11 @@ */ import { cli } from 'cleye'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; import { setSelectedInstance } from '../lib/storage'; import { pickInstance } from '../lib/prompt'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { instance: instanceFlag }, } = cli( diff --git a/packages/cli/src/modules/auth/commands/show.ts b/packages/cli-module-auth/src/commands/show.ts similarity index 94% rename from packages/cli/src/modules/auth/commands/show.ts rename to packages/cli-module-auth/src/commands/show.ts index cd55430f91..e1b7c62f72 100644 --- a/packages/cli/src/modules/auth/commands/show.ts +++ b/packages/cli-module-auth/src/commands/show.ts @@ -15,13 +15,13 @@ */ import { cli } from 'cleye'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; import { httpJson } from '../lib/http'; import { getSelectedInstance } from '../lib/storage'; import { accessTokenNeedsRefresh, refreshAccessToken } from '../lib/auth'; import { getSecretStore } from '../lib/secretStore'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { instance: instanceFlag }, } = cli( diff --git a/packages/cli/src/modules/auth/index.ts b/packages/cli-module-auth/src/index.ts similarity index 92% rename from packages/cli/src/modules/auth/index.ts rename to packages/cli-module-auth/src/index.ts index d5766219b2..fef74ec8a5 100644 --- a/packages/cli/src/modules/auth/index.ts +++ b/packages/cli-module-auth/src/index.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; -export default createCliPlugin({ - pluginId: 'auth', +export default createCliModule({ + packageJson, init: async reg => { reg.addCommand({ path: ['auth', 'login'], diff --git a/packages/cli/src/modules/auth/lib/auth.test.ts b/packages/cli-module-auth/src/lib/auth.test.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/auth.test.ts rename to packages/cli-module-auth/src/lib/auth.test.ts diff --git a/packages/cli/src/modules/auth/lib/auth.ts b/packages/cli-module-auth/src/lib/auth.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/auth.ts rename to packages/cli-module-auth/src/lib/auth.ts diff --git a/packages/cli/src/modules/auth/lib/http.test.ts b/packages/cli-module-auth/src/lib/http.test.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/http.test.ts rename to packages/cli-module-auth/src/lib/http.test.ts diff --git a/packages/cli/src/modules/auth/lib/http.ts b/packages/cli-module-auth/src/lib/http.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/http.ts rename to packages/cli-module-auth/src/lib/http.ts diff --git a/packages/cli/src/modules/auth/lib/localServer.test.ts b/packages/cli-module-auth/src/lib/localServer.test.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/localServer.test.ts rename to packages/cli-module-auth/src/lib/localServer.test.ts diff --git a/packages/cli/src/modules/auth/lib/localServer.ts b/packages/cli-module-auth/src/lib/localServer.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/localServer.ts rename to packages/cli-module-auth/src/lib/localServer.ts diff --git a/packages/cli/src/modules/auth/lib/pkce.test.ts b/packages/cli-module-auth/src/lib/pkce.test.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/pkce.test.ts rename to packages/cli-module-auth/src/lib/pkce.test.ts diff --git a/packages/cli/src/modules/auth/lib/pkce.ts b/packages/cli-module-auth/src/lib/pkce.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/pkce.ts rename to packages/cli-module-auth/src/lib/pkce.ts diff --git a/packages/cli/src/modules/auth/lib/prompt.test.ts b/packages/cli-module-auth/src/lib/prompt.test.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/prompt.test.ts rename to packages/cli-module-auth/src/lib/prompt.test.ts diff --git a/packages/cli/src/modules/auth/lib/prompt.ts b/packages/cli-module-auth/src/lib/prompt.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/prompt.ts rename to packages/cli-module-auth/src/lib/prompt.ts diff --git a/packages/cli/src/modules/auth/lib/secretStore.test.ts b/packages/cli-module-auth/src/lib/secretStore.test.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/secretStore.test.ts rename to packages/cli-module-auth/src/lib/secretStore.test.ts diff --git a/packages/cli/src/modules/auth/lib/secretStore.ts b/packages/cli-module-auth/src/lib/secretStore.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/secretStore.ts rename to packages/cli-module-auth/src/lib/secretStore.ts diff --git a/packages/cli/src/modules/auth/lib/storage.test.ts b/packages/cli-module-auth/src/lib/storage.test.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/storage.test.ts rename to packages/cli-module-auth/src/lib/storage.test.ts diff --git a/packages/cli/src/modules/auth/lib/storage.ts b/packages/cli-module-auth/src/lib/storage.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/storage.ts rename to packages/cli-module-auth/src/lib/storage.ts diff --git a/packages/cli-module-build/.eslintrc.js b/packages/cli-module-build/.eslintrc.js new file mode 100644 index 0000000000..a070dc55ee --- /dev/null +++ b/packages/cli-module-build/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + ignorePatterns: ['config/**', 'templates/**'], +}); diff --git a/packages/cli-module-build/README.md b/packages/cli-module-build/README.md new file mode 100644 index 0000000000..355891c907 --- /dev/null +++ b/packages/cli-module-build/README.md @@ -0,0 +1,23 @@ +# @backstage/cli-module-build + +CLI module that provides build, start, and packaging commands for the Backstage CLI. + +## Commands + +| Command | Description | +| :----------------- | :------------------------------------------------------------------------ | +| `package build` | Build a package for production deployment or publishing | +| `package start` | Start a package for local development | +| `package clean` | Delete cache directories | +| `package prepack` | Prepares a package for packaging before publishing | +| `package postpack` | Restores the changes made by the prepack command | +| `repo build` | Build packages in the project, excluding bundled app and backend packages | +| `repo start` | Starts packages in the repo for local development | +| `repo clean` | Delete cache and output directories | +| `build-workspace` | Builds a temporary dist workspace from the provided packages | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) +- [Build System](https://backstage.io/docs/tooling/cli/build-system) diff --git a/packages/cli-module-build/bin/backstage-cli-module-build b/packages/cli-module-build/bin/backstage-cli-module-build new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-build/bin/backstage-cli-module-build @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-build/catalog-info.yaml b/packages/cli-module-build/catalog-info.yaml new file mode 100644 index 0000000000..65715cce2a --- /dev/null +++ b/packages/cli-module-build/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-build + title: '@backstage/cli-module-build' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-build/cli-report.md b/packages/cli-module-build/cli-report.md new file mode 100644 index 0000000000..1c11022dc8 --- /dev/null +++ b/packages/cli-module-build/cli-report.md @@ -0,0 +1,156 @@ +## CLI Report file for "@backstage/cli-module-build" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-build` + +``` +Usage: @backstage/cli-module-build [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + build-workspace + help [command] + package [command] + repo [command] +``` + +### `backstage-cli-module-build build-workspace` + +``` +Usage: @backstage/cli-module-build build-workspace [packages...] + +Options: + --always-pack + -h, --help +``` + +### `backstage-cli-module-build package` + +``` +Usage: @backstage/cli-module-build package [options] [command] [command] + +Options: + -h, --help + +Commands: + build + clean + help [command] + postpack + prepack + start +``` + +### `backstage-cli-module-build package build` + +``` +Usage: @backstage/cli-module-build package build + +Options: + --config + --minify + --module-federation + --role + --skip-build-dependencies + --stats + -h, --help +``` + +### `backstage-cli-module-build package clean` + +``` +Usage: @backstage/cli-module-build package clean + +Options: + -h, --help +``` + +### `backstage-cli-module-build package postpack` + +``` +Usage: @backstage/cli-module-build package postpack + +Options: + -h, --help +``` + +### `backstage-cli-module-build package prepack` + +``` +Usage: @backstage/cli-module-build package prepack + +Options: + -h, --help +``` + +### `backstage-cli-module-build package start` + +``` +Usage: @backstage/cli-module-build package start + +Options: + --check + --config + --entrypoint + --inspect + --inspect-brk + --link + --require + --role + -h, --help +``` + +### `backstage-cli-module-build repo` + +``` +Usage: @backstage/cli-module-build repo [options] [command] [command] + +Options: + -h, --help + +Commands: + build + clean + help [command] + start +``` + +### `backstage-cli-module-build repo build` + +``` +Usage: @backstage/cli-module-build repo build + +Options: + --all + --minify + --since + -h, --help +``` + +### `backstage-cli-module-build repo clean` + +``` +Usage: @backstage/cli-module-build repo clean + +Options: + -h, --help +``` + +### `backstage-cli-module-build repo start` + +``` +Usage: @backstage/cli-module-build repo start [packages...] + +Options: + --config + --inspect + --inspect-brk + --link + --plugin + --require + -h, --help +``` diff --git a/packages/cli-module-build/config/webpack-public-path.js b/packages/cli-module-build/config/webpack-public-path.js new file mode 100644 index 0000000000..3e14e96eb9 --- /dev/null +++ b/packages/cli-module-build/config/webpack-public-path.js @@ -0,0 +1,31 @@ +/* + * 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. + */ + +// This script is used to pick up and set the public path of the Webpack bundle +// at runtime. The meta tag is injected by the app build, but only present in +// the `index.html.tmpl` file. The runtime value of the meta tag is populated by +// the app backend, when it templates the final `index.html` file. +// +// This is needed for additional chunks to use the correct public path, and it +// is not possible to set the `__webpack_public_path__` variable outside of the +// build itself. The Webpack output also does not read any tags or +// similar, this seems to be the only way to dynamically configure the public +// path at runtime. +const el = document.querySelector('meta[name="backstage-public-path"]'); +const path = el?.getAttribute('content'); +if (path) { + __webpack_public_path__ = path; +} diff --git a/packages/cli-module-build/package.json b/packages/cli-module-build/package.json new file mode 100644 index 0000000000..d13fa4bac0 --- /dev/null +++ b/packages/cli-module-build/package.json @@ -0,0 +1,106 @@ +{ + "name": "@backstage/cli-module-build", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-build" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "bin": "bin/backstage-cli-module-build", + "files": [ + "dist", + "bin", + "config", + "templates" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/config-loader": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/module-federation-common": "workspace:^", + "@manypkg/get-packages": "^1.1.3", + "@module-federation/enhanced": "^0.21.6", + "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0", + "@rollup/plugin-commonjs": "^26.0.0", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.0.0", + "@rollup/plugin-yaml": "^4.0.0", + "@rspack/core": "^1.4.11", + "@rspack/dev-server": "^1.1.4", + "@rspack/plugin-react-refresh": "^1.4.3", + "@swc/core": "^1.15.6", + "bfj": "^9.0.2", + "buffer": "^6.0.3", + "chalk": "^4.0.0", + "chokidar": "^3.3.1", + "cleye": "^2.3.0", + "cross-spawn": "^7.0.3", + "css-loader": "^6.5.1", + "ctrlc-windows": "^2.1.0", + "esbuild-loader": "^4.0.0", + "eslint-rspack-plugin": "^4.2.1", + "eslint-webpack-plugin": "^4.2.0", + "fork-ts-checker-webpack-plugin": "^9.0.0", + "fs-extra": "^11.2.0", + "glob": "^7.1.7", + "html-webpack-plugin": "^5.6.3", + "lodash": "^4.17.21", + "mini-css-extract-plugin": "^2.4.2", + "node-stdlib-browser": "^1.3.1", + "npm-packlist": "^5.0.0", + "p-queue": "^6.6.2", + "postcss": "^8.1.0", + "postcss-import": "^16.1.0", + "process": "^0.11.10", + "raw-loader": "^4.0.2", + "react-dev-utils": "^12.0.0-next.60", + "react-refresh": "^0.18.0", + "rollup": "^4.27.3", + "rollup-plugin-dts": "^6.1.0", + "rollup-plugin-esbuild": "^6.1.1", + "rollup-plugin-postcss": "^4.0.0", + "rollup-pluginutils": "^2.8.2", + "shell-quote": "^1.8.1", + "style-loader": "^3.3.1", + "swc-loader": "^0.2.3", + "tar": "^7.5.6", + "ts-checker-rspack-plugin": "^1.1.5", + "ts-morph": "^24.0.0", + "util": "^0.12.3", + "webpack": "~5.105.0", + "webpack-dev-server": "^5.0.0", + "yml-loader": "^2.1.0", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^11.0.0", + "@types/lodash": "^4.14.151", + "@types/npm-packlist": "^3.0.0", + "@types/shell-quote": "^1.7.5" + } +} diff --git a/packages/cli-module-build/report.api.md b/packages/cli-module-build/report.api.md new file mode 100644 index 0000000000..34634dd448 --- /dev/null +++ b/packages/cli-module-build/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-build" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/build/commands/buildWorkspace.ts b/packages/cli-module-build/src/commands/buildWorkspace.ts similarity index 93% rename from packages/cli/src/modules/build/commands/buildWorkspace.ts rename to packages/cli-module-build/src/commands/buildWorkspace.ts index f9ca19962f..0abd9e5307 100644 --- a/packages/cli/src/modules/build/commands/buildWorkspace.ts +++ b/packages/cli-module-build/src/commands/buildWorkspace.ts @@ -17,9 +17,9 @@ import fs from 'fs-extra'; import { cli } from 'cleye'; import { createDistWorkspace } from '../lib/packager'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { // Normalize legacy --alwaysYarnPack alias (a genuinely different name, not // just a casing variant — type-flag handles camelCase/kebab-case natively) const normalizedArgs = args.map(a => { diff --git a/packages/cli/src/modules/build/commands/package/build/command.ts b/packages/cli-module-build/src/commands/package/build/command.ts similarity index 97% rename from packages/cli/src/modules/build/commands/package/build/command.ts rename to packages/cli-module-build/src/commands/package/build/command.ts index 7a6280299b..07c6e3a8d7 100644 --- a/packages/cli/src/modules/build/commands/package/build/command.ts +++ b/packages/cli-module-build/src/commands/package/build/command.ts @@ -29,9 +29,9 @@ import { buildFrontend } from '../../../lib/buildFrontend'; import { buildBackend } from '../../../lib/buildBackend'; import { isValidUrl } from '../../../lib/urls'; import chalk from 'chalk'; -import type { CommandContext } from '../../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { role, diff --git a/packages/cli/src/modules/build/commands/package/build/index.ts b/packages/cli-module-build/src/commands/package/build/index.ts similarity index 100% rename from packages/cli/src/modules/build/commands/package/build/index.ts rename to packages/cli-module-build/src/commands/package/build/index.ts diff --git a/packages/cli/src/modules/build/commands/package/clean.ts b/packages/cli-module-build/src/commands/package/clean.ts similarity index 88% rename from packages/cli/src/modules/build/commands/package/clean.ts rename to packages/cli-module-build/src/commands/package/clean.ts index 6aa987f6c1..fffc418795 100644 --- a/packages/cli/src/modules/build/commands/package/clean.ts +++ b/packages/cli-module-build/src/commands/package/clean.ts @@ -17,9 +17,9 @@ import { cli } from 'cleye'; import fs from 'fs-extra'; import { targetPaths } from '@backstage/cli-common'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); await fs.remove(targetPaths.resolve('dist')); await fs.remove(targetPaths.resolve('dist-types')); diff --git a/packages/cli/src/modules/build/commands/package/postpack.ts b/packages/cli-module-build/src/commands/package/postpack.ts similarity index 87% rename from packages/cli/src/modules/build/commands/package/postpack.ts rename to packages/cli-module-build/src/commands/package/postpack.ts index 232d03af6b..56856f7401 100644 --- a/packages/cli/src/modules/build/commands/package/postpack.ts +++ b/packages/cli-module-build/src/commands/package/postpack.ts @@ -17,9 +17,9 @@ import { cli } from 'cleye'; import { targetPaths } from '@backstage/cli-common'; import { revertProductionPack } from '../../lib/packager/productionPack'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); await revertProductionPack(targetPaths.dir); }; diff --git a/packages/cli/src/modules/build/commands/package/prepack.ts b/packages/cli-module-build/src/commands/package/prepack.ts similarity index 90% rename from packages/cli/src/modules/build/commands/package/prepack.ts rename to packages/cli-module-build/src/commands/package/prepack.ts index e6aba2ce3f..4488f92c76 100644 --- a/packages/cli/src/modules/build/commands/package/prepack.ts +++ b/packages/cli-module-build/src/commands/package/prepack.ts @@ -20,9 +20,9 @@ import { targetPaths } from '@backstage/cli-common'; import { productionPack } from '../../lib/packager/productionPack'; import { publishPreflightCheck } from '../../lib/publishing'; import { createTypeDistProject } from '../../lib/typeDistProject'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); publishPreflightCheck({ diff --git a/packages/cli/src/modules/build/commands/package/start/command.ts b/packages/cli-module-build/src/commands/package/start/command.ts similarity index 95% rename from packages/cli/src/modules/build/commands/package/start/command.ts rename to packages/cli-module-build/src/commands/package/start/command.ts index fe871973de..8cee25732f 100644 --- a/packages/cli/src/modules/build/commands/package/start/command.ts +++ b/packages/cli-module-build/src/commands/package/start/command.ts @@ -19,9 +19,9 @@ import { startPackage } from './startPackage'; import { resolveLinkedWorkspace } from './resolveLinkedWorkspace'; import { findRoleFromCommand } from '../../../lib/role'; import { targetPaths } from '@backstage/cli-common'; -import type { CommandContext } from '../../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { config, diff --git a/packages/cli/src/modules/build/commands/package/start/index.ts b/packages/cli-module-build/src/commands/package/start/index.ts similarity index 100% rename from packages/cli/src/modules/build/commands/package/start/index.ts rename to packages/cli-module-build/src/commands/package/start/index.ts diff --git a/packages/cli/src/modules/build/commands/package/start/resolveLinkedWorkspace.ts b/packages/cli-module-build/src/commands/package/start/resolveLinkedWorkspace.ts similarity index 100% rename from packages/cli/src/modules/build/commands/package/start/resolveLinkedWorkspace.ts rename to packages/cli-module-build/src/commands/package/start/resolveLinkedWorkspace.ts diff --git a/packages/cli/src/modules/build/commands/package/start/startBackend.ts b/packages/cli-module-build/src/commands/package/start/startBackend.ts similarity index 100% rename from packages/cli/src/modules/build/commands/package/start/startBackend.ts rename to packages/cli-module-build/src/commands/package/start/startBackend.ts diff --git a/packages/cli/src/modules/build/commands/package/start/startFrontend.ts b/packages/cli-module-build/src/commands/package/start/startFrontend.ts similarity index 100% rename from packages/cli/src/modules/build/commands/package/start/startFrontend.ts rename to packages/cli-module-build/src/commands/package/start/startFrontend.ts diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts b/packages/cli-module-build/src/commands/package/start/startPackage.test.ts similarity index 100% rename from packages/cli/src/modules/build/commands/package/start/startPackage.test.ts rename to packages/cli-module-build/src/commands/package/start/startPackage.test.ts diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.ts b/packages/cli-module-build/src/commands/package/start/startPackage.ts similarity index 100% rename from packages/cli/src/modules/build/commands/package/start/startPackage.ts rename to packages/cli-module-build/src/commands/package/start/startPackage.ts diff --git a/packages/cli/src/modules/build/commands/repo/build.ts b/packages/cli-module-build/src/commands/repo/build.ts similarity index 97% rename from packages/cli/src/modules/build/commands/repo/build.ts rename to packages/cli-module-build/src/commands/repo/build.ts index 7140310464..d0bb9cab6c 100644 --- a/packages/cli/src/modules/build/commands/repo/build.ts +++ b/packages/cli-module-build/src/commands/repo/build.ts @@ -29,9 +29,9 @@ import { import { buildFrontend } from '../../lib/buildFrontend'; import { buildBackend } from '../../lib/buildBackend'; import { createScriptOptionsParser } from '../../lib/optionsParser'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { all, since, minify }, } = cli( diff --git a/packages/cli/src/modules/build/commands/repo/clean.ts b/packages/cli-module-build/src/commands/repo/clean.ts similarity index 93% rename from packages/cli/src/modules/build/commands/repo/clean.ts rename to packages/cli-module-build/src/commands/repo/clean.ts index 7814c1e4b7..12b1daf911 100644 --- a/packages/cli/src/modules/build/commands/repo/clean.ts +++ b/packages/cli-module-build/src/commands/repo/clean.ts @@ -19,9 +19,9 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { PackageGraph } from '@backstage/cli-node'; import { run, targetPaths } from '@backstage/cli-common'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); const packages = await PackageGraph.listTargetPackages(); diff --git a/packages/cli/src/modules/build/commands/repo/start.test.ts b/packages/cli-module-build/src/commands/repo/start.test.ts similarity index 100% rename from packages/cli/src/modules/build/commands/repo/start.test.ts rename to packages/cli-module-build/src/commands/repo/start.test.ts diff --git a/packages/cli/src/modules/build/commands/repo/start.ts b/packages/cli-module-build/src/commands/repo/start.ts similarity index 98% rename from packages/cli/src/modules/build/commands/repo/start.ts rename to packages/cli-module-build/src/commands/repo/start.ts index e4c51a1e46..8cf3dceb1d 100644 --- a/packages/cli/src/modules/build/commands/repo/start.ts +++ b/packages/cli-module-build/src/commands/repo/start.ts @@ -26,7 +26,7 @@ import { cli } from 'cleye'; import { resolveLinkedWorkspace } from '../package/start/resolveLinkedWorkspace'; import { startPackage } from '../package/start/startPackage'; import { parseArgs } from 'node:util'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; const ACCEPTED_PACKAGE_ROLES: Array = [ 'frontend', @@ -35,7 +35,7 @@ const ACCEPTED_PACKAGE_ROLES: Array = [ 'backend-plugin', ]; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { plugin, config, require: requirePath, link, inspect, inspectBrk }, _: namesOrPaths, diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli-module-build/src/index.ts similarity index 94% rename from packages/cli/src/modules/build/index.ts rename to packages/cli-module-build/src/index.ts index 139c591af2..887a4b562c 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli-module-build/src/index.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; -export const buildPlugin = createCliPlugin({ - pluginId: 'build', +export default createCliModule({ + packageJson, init: async reg => { reg.addCommand({ path: ['package', 'build'], @@ -84,5 +85,3 @@ export const buildPlugin = createCliPlugin({ }); }, }); - -export default buildPlugin; diff --git a/packages/cli/src/modules/build/lib/__testUtils__/createFeatureEnvironment.ts b/packages/cli-module-build/src/lib/__testUtils__/createFeatureEnvironment.ts similarity index 100% rename from packages/cli/src/modules/build/lib/__testUtils__/createFeatureEnvironment.ts rename to packages/cli-module-build/src/lib/__testUtils__/createFeatureEnvironment.ts diff --git a/packages/cli/src/modules/build/lib/buildBackend.ts b/packages/cli-module-build/src/lib/buildBackend.ts similarity index 100% rename from packages/cli/src/modules/build/lib/buildBackend.ts rename to packages/cli-module-build/src/lib/buildBackend.ts diff --git a/packages/cli/src/modules/build/lib/buildFrontend.ts b/packages/cli-module-build/src/lib/buildFrontend.ts similarity index 100% rename from packages/cli/src/modules/build/lib/buildFrontend.ts rename to packages/cli-module-build/src/lib/buildFrontend.ts diff --git a/packages/cli/src/modules/build/lib/builder/config.test.ts b/packages/cli-module-build/src/lib/builder/config.test.ts similarity index 100% rename from packages/cli/src/modules/build/lib/builder/config.test.ts rename to packages/cli-module-build/src/lib/builder/config.test.ts diff --git a/packages/cli/src/modules/build/lib/builder/config.ts b/packages/cli-module-build/src/lib/builder/config.ts similarity index 100% rename from packages/cli/src/modules/build/lib/builder/config.ts rename to packages/cli-module-build/src/lib/builder/config.ts diff --git a/packages/cli/src/modules/build/lib/builder/index.ts b/packages/cli-module-build/src/lib/builder/index.ts similarity index 100% rename from packages/cli/src/modules/build/lib/builder/index.ts rename to packages/cli-module-build/src/lib/builder/index.ts diff --git a/packages/cli/src/modules/build/lib/builder/packager.test.ts b/packages/cli-module-build/src/lib/builder/packager.test.ts similarity index 100% rename from packages/cli/src/modules/build/lib/builder/packager.test.ts rename to packages/cli-module-build/src/lib/builder/packager.test.ts diff --git a/packages/cli/src/modules/build/lib/builder/packager.ts b/packages/cli-module-build/src/lib/builder/packager.ts similarity index 100% rename from packages/cli/src/modules/build/lib/builder/packager.ts rename to packages/cli-module-build/src/lib/builder/packager.ts diff --git a/packages/cli/src/modules/build/lib/builder/plugins.test.ts b/packages/cli-module-build/src/lib/builder/plugins.test.ts similarity index 100% rename from packages/cli/src/modules/build/lib/builder/plugins.test.ts rename to packages/cli-module-build/src/lib/builder/plugins.test.ts diff --git a/packages/cli/src/modules/build/lib/builder/plugins.ts b/packages/cli-module-build/src/lib/builder/plugins.ts similarity index 100% rename from packages/cli/src/modules/build/lib/builder/plugins.ts rename to packages/cli-module-build/src/lib/builder/plugins.ts diff --git a/packages/cli/src/modules/build/lib/builder/types.ts b/packages/cli-module-build/src/lib/builder/types.ts similarity index 100% rename from packages/cli/src/modules/build/lib/builder/types.ts rename to packages/cli-module-build/src/lib/builder/types.ts diff --git a/packages/cli/src/modules/build/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts b/packages/cli-module-build/src/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts rename to packages/cli-module-build/src/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts diff --git a/packages/cli/src/modules/build/lib/bundler/bundle.ts b/packages/cli-module-build/src/lib/bundler/bundle.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/bundle.ts rename to packages/cli-module-build/src/lib/bundler/bundle.ts diff --git a/packages/cli/src/modules/build/lib/bundler/config.ts b/packages/cli-module-build/src/lib/bundler/config.ts similarity index 97% rename from packages/cli/src/modules/build/lib/bundler/config.ts rename to packages/cli-module-build/src/lib/bundler/config.ts index c3676ecf70..cee7ba6819 100644 --- a/packages/cli/src/modules/build/lib/bundler/config.ts +++ b/packages/cli-module-build/src/lib/bundler/config.ts @@ -29,10 +29,10 @@ import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack'; import fs from 'fs-extra'; import { optimization as optimizationConfig } from './optimization'; import pickBy from 'lodash/pickBy'; -import { runOutput, targetPaths } from '@backstage/cli-common'; +import { findOwnPaths, runOutput, targetPaths } from '@backstage/cli-common'; import { transforms } from './transforms'; -import { version } from '../../../../wiring/version'; +import { version } from '../../../package.json'; import yn from 'yn'; import { hasReactDomClient } from './hasReactDomClient'; import { createWorkspaceLinkingPlugins } from './linkWorkspaces'; @@ -330,7 +330,8 @@ export async function createConfig( devtool: isDev ? 'eval-cheap-module-source-map' : 'source-map', context: paths.targetPath, entry: [ - require.resolve('@backstage/cli/config/webpack-public-path'), + /* eslint-disable-next-line no-restricted-syntax */ + findOwnPaths(__dirname).resolve('config/webpack-public-path'), ...(options.additionalEntryPoints ?? []), paths.targetEntry, ], diff --git a/packages/cli/src/modules/build/lib/bundler/hasReactDomClient.ts b/packages/cli-module-build/src/lib/bundler/hasReactDomClient.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/hasReactDomClient.ts rename to packages/cli-module-build/src/lib/bundler/hasReactDomClient.ts diff --git a/packages/cli/src/modules/build/lib/bundler/index.ts b/packages/cli-module-build/src/lib/bundler/index.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/index.ts rename to packages/cli-module-build/src/lib/bundler/index.ts diff --git a/packages/cli/src/modules/build/lib/bundler/linkWorkspaces.ts b/packages/cli-module-build/src/lib/bundler/linkWorkspaces.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/linkWorkspaces.ts rename to packages/cli-module-build/src/lib/bundler/linkWorkspaces.ts diff --git a/packages/cli/src/modules/build/lib/bundler/moduleFederation.test.ts b/packages/cli-module-build/src/lib/bundler/moduleFederation.test.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/moduleFederation.test.ts rename to packages/cli-module-build/src/lib/bundler/moduleFederation.test.ts diff --git a/packages/cli/src/modules/build/lib/bundler/moduleFederation.ts b/packages/cli-module-build/src/lib/bundler/moduleFederation.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/moduleFederation.ts rename to packages/cli-module-build/src/lib/bundler/moduleFederation.ts diff --git a/packages/cli/src/modules/build/lib/bundler/optimization.ts b/packages/cli-module-build/src/lib/bundler/optimization.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/optimization.ts rename to packages/cli-module-build/src/lib/bundler/optimization.ts diff --git a/packages/cli/src/modules/build/lib/bundler/packageDetection.ts b/packages/cli-module-build/src/lib/bundler/packageDetection.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/packageDetection.ts rename to packages/cli-module-build/src/lib/bundler/packageDetection.ts diff --git a/packages/cli/src/modules/build/lib/bundler/paths.ts b/packages/cli-module-build/src/lib/bundler/paths.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/paths.ts rename to packages/cli-module-build/src/lib/bundler/paths.ts diff --git a/packages/cli/src/modules/build/lib/bundler/server.ts b/packages/cli-module-build/src/lib/bundler/server.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/server.ts rename to packages/cli-module-build/src/lib/bundler/server.ts diff --git a/packages/cli/src/modules/build/lib/bundler/transforms.ts b/packages/cli-module-build/src/lib/bundler/transforms.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/transforms.ts rename to packages/cli-module-build/src/lib/bundler/transforms.ts diff --git a/packages/cli/src/modules/build/lib/bundler/types.ts b/packages/cli-module-build/src/lib/bundler/types.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/types.ts rename to packages/cli-module-build/src/lib/bundler/types.ts diff --git a/packages/cli/src/modules/build/lib/config.ts b/packages/cli-module-build/src/lib/config.ts similarity index 100% rename from packages/cli/src/modules/build/lib/config.ts rename to packages/cli-module-build/src/lib/config.ts diff --git a/packages/cli/src/modules/build/lib/entryPoints.ts b/packages/cli-module-build/src/lib/entryPoints.ts similarity index 100% rename from packages/cli/src/modules/build/lib/entryPoints.ts rename to packages/cli-module-build/src/lib/entryPoints.ts diff --git a/packages/cli/src/modules/build/lib/ipc/IpcServer.ts b/packages/cli-module-build/src/lib/ipc/IpcServer.ts similarity index 100% rename from packages/cli/src/modules/build/lib/ipc/IpcServer.ts rename to packages/cli-module-build/src/lib/ipc/IpcServer.ts diff --git a/packages/cli/src/modules/build/lib/ipc/ServerDataStore.ts b/packages/cli-module-build/src/lib/ipc/ServerDataStore.ts similarity index 100% rename from packages/cli/src/modules/build/lib/ipc/ServerDataStore.ts rename to packages/cli-module-build/src/lib/ipc/ServerDataStore.ts diff --git a/packages/cli/src/modules/build/lib/ipc/index.ts b/packages/cli-module-build/src/lib/ipc/index.ts similarity index 100% rename from packages/cli/src/modules/build/lib/ipc/index.ts rename to packages/cli-module-build/src/lib/ipc/index.ts diff --git a/packages/cli/src/modules/build/lib/optionsParser.ts b/packages/cli-module-build/src/lib/optionsParser.ts similarity index 100% rename from packages/cli/src/modules/build/lib/optionsParser.ts rename to packages/cli-module-build/src/lib/optionsParser.ts diff --git a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts similarity index 99% rename from packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts rename to packages/cli-module-build/src/lib/packager/createDistWorkspace.ts index 9f63525186..535d54f9dc 100644 --- a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts +++ b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts @@ -26,11 +26,10 @@ import * as tar from 'tar'; import partition from 'lodash/partition'; import { run, targetPaths } from '@backstage/cli-common'; - import { dependencies as cliDependencies, devDependencies as cliDevDependencies, -} from '../../../../../package.json'; +} from '../../../package.json'; import { BuildOptions, buildPackages, diff --git a/packages/cli/src/modules/build/lib/packager/index.ts b/packages/cli-module-build/src/lib/packager/index.ts similarity index 100% rename from packages/cli/src/modules/build/lib/packager/index.ts rename to packages/cli-module-build/src/lib/packager/index.ts diff --git a/packages/cli/src/modules/build/lib/packager/productionPack.ts b/packages/cli-module-build/src/lib/packager/productionPack.ts similarity index 100% rename from packages/cli/src/modules/build/lib/packager/productionPack.ts rename to packages/cli-module-build/src/lib/packager/productionPack.ts diff --git a/packages/cli/src/modules/build/lib/publishing.ts b/packages/cli-module-build/src/lib/publishing.ts similarity index 100% rename from packages/cli/src/modules/build/lib/publishing.ts rename to packages/cli-module-build/src/lib/publishing.ts diff --git a/packages/cli/src/modules/build/lib/role.test.ts b/packages/cli-module-build/src/lib/role.test.ts similarity index 100% rename from packages/cli/src/modules/build/lib/role.test.ts rename to packages/cli-module-build/src/lib/role.test.ts diff --git a/packages/cli/src/modules/build/lib/role.ts b/packages/cli-module-build/src/lib/role.ts similarity index 100% rename from packages/cli/src/modules/build/lib/role.ts rename to packages/cli-module-build/src/lib/role.ts diff --git a/packages/cli/src/modules/build/lib/runner/index.ts b/packages/cli-module-build/src/lib/runner/index.ts similarity index 100% rename from packages/cli/src/modules/build/lib/runner/index.ts rename to packages/cli-module-build/src/lib/runner/index.ts diff --git a/packages/cli/src/modules/build/lib/runner/runBackend.test.ts b/packages/cli-module-build/src/lib/runner/runBackend.test.ts similarity index 100% rename from packages/cli/src/modules/build/lib/runner/runBackend.test.ts rename to packages/cli-module-build/src/lib/runner/runBackend.test.ts diff --git a/packages/cli/src/modules/build/lib/runner/runBackend.ts b/packages/cli-module-build/src/lib/runner/runBackend.ts similarity index 98% rename from packages/cli/src/modules/build/lib/runner/runBackend.ts rename to packages/cli-module-build/src/lib/runner/runBackend.ts index 22fd50b3f4..86c249e8be 100644 --- a/packages/cli/src/modules/build/lib/runner/runBackend.ts +++ b/packages/cli-module-build/src/lib/runner/runBackend.ts @@ -28,8 +28,8 @@ import spawn from 'cross-spawn'; const loaderArgs = [ '--enable-source-maps', '--require', - require.resolve('@backstage/cli/config/nodeTransform.cjs'), - // TODO: Support modules, although there's currently no way to load them since import() is transpiled tp require() + require.resolve('@backstage/cli-node/config/nodeTransform.cjs'), + // TODO: Support modules, although there's currently no way to load them since import() is transpiled to require() ]; export type RunBackendOptions = { diff --git a/packages/cli/src/modules/build/lib/typeDistProject.test.ts b/packages/cli-module-build/src/lib/typeDistProject.test.ts similarity index 99% rename from packages/cli/src/modules/build/lib/typeDistProject.test.ts rename to packages/cli-module-build/src/lib/typeDistProject.test.ts index 6eef53816b..9d14bccc15 100644 --- a/packages/cli/src/modules/build/lib/typeDistProject.test.ts +++ b/packages/cli-module-build/src/lib/typeDistProject.test.ts @@ -33,6 +33,7 @@ describe('typeDistProject', () => { frontend: false, backend: false, cli: false, + 'cli-module': false, 'common-library': false, }; diff --git a/packages/cli/src/modules/build/lib/typeDistProject.ts b/packages/cli-module-build/src/lib/typeDistProject.ts similarity index 100% rename from packages/cli/src/modules/build/lib/typeDistProject.ts rename to packages/cli-module-build/src/lib/typeDistProject.ts diff --git a/packages/cli/src/modules/build/lib/urls.test.ts b/packages/cli-module-build/src/lib/urls.test.ts similarity index 100% rename from packages/cli/src/modules/build/lib/urls.test.ts rename to packages/cli-module-build/src/lib/urls.test.ts diff --git a/packages/cli/src/modules/build/lib/urls.ts b/packages/cli-module-build/src/lib/urls.ts similarity index 100% rename from packages/cli/src/modules/build/lib/urls.ts rename to packages/cli-module-build/src/lib/urls.ts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/.gitignore b/packages/cli-module-build/src/tests/transforms/__fixtures__/.gitignore similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/.gitignore rename to packages/cli-module-build/src/tests/transforms/__fixtures__/.gitignore diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-default.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-default.js similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-default.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-default.js diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-named.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-named.js similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-named.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-named.js diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-default.mjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-default.mjs similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-default.mjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-default.mjs diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-named.mjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-named.mjs similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-named.mjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-named.mjs diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-default.cjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-default.cjs similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-default.cjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-default.cjs diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-named.cjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-named.cjs similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-named.cjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-named.cjs diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.d.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.d.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.d.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.d.ts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.js similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.js diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/package.json b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/package.json similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/package.json rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/package.json diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/a-default.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/a-default.js similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/a-default.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/a-default.js diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/a-named.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/a-named.js similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/a-named.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/a-named.js diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/b-default.mjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/b-default.mjs similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/b-default.mjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/b-default.mjs diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/b-named.mjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/b-named.mjs similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/b-named.mjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/b-named.mjs diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/c-default.cjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/c-default.cjs similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/c-default.cjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/c-default.cjs diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/c-named.cjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/c-named.cjs similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/c-named.cjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/c-named.cjs diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/main.d.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/main.d.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/main.d.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/main.d.ts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/main.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/main.js similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/main.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/main.js diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/package.json b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/package.json similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/package.json rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/package.json diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/a-default.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/a-default.js similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/a-default.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/a-default.js diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/a-named.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/a-named.js similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/a-named.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/a-named.js diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/b-default.mjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/b-default.mjs similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/b-default.mjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/b-default.mjs diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/b-named.mjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/b-named.mjs similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/b-named.mjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/b-named.mjs diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/c-default.cjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/c-default.cjs similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/c-default.cjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/c-default.cjs diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/c-named.cjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/c-named.cjs similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/c-named.cjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/c-named.cjs diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/main.d.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/main.d.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/main.d.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/main.d.ts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/main.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/main.js similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/main.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/main.js diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/package.json b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/package.json similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/package.json rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/package.json diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/a-default.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/a-default.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/a-default.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/a-default.ts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/a-named.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/a-named.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/a-named.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/a-named.ts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/b-default.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/b-default.mts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/b-default.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/b-default.mts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/b-named.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/b-named.mts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/b-named.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/b-named.mts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/c-default.cts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/c-default.cts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/c-default.cts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/c-default.cts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/c-named.cts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/c-named.cts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/c-named.cts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/c-named.cts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/main.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/main.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/main.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/main.ts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/package.json b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/package.json similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/package.json rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/package.json diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/print.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/print.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/print.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/print.ts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/a-default.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/a-default.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/a-default.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/a-default.ts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/a-named.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/a-named.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/a-named.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/a-named.ts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/b-default.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/b-default.mts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/b-default.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/b-default.mts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/b-named.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/b-named.mts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/b-named.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/b-named.mts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/c-default.cts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/c-default.cts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/c-default.cts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/c-default.cts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/c-named.cts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/c-named.cts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/c-named.cts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/c-named.cts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/main.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/main.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/main.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/main.ts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/package.json b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/package.json similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/package.json rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/package.json diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/print.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/print.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/print.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/print.ts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-default-explicit.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-default-explicit.mts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-default-explicit.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-default-explicit.mts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-default.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-default.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-default.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-default.ts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-named-explicit.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-named-explicit.mts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-named-explicit.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-named-explicit.mts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-named.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-named.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-named.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-named.ts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/b-default.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/b-default.mts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/b-default.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/b-default.mts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/b-named.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/b-named.mts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/b-named.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/b-named.mts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/c-default.cts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/c-default.cts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/c-default.cts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/c-default.cts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/c-named.cts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/c-named.cts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/c-named.cts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/c-named.cts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/main-explicit.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/main-explicit.mts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/main-explicit.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/main-explicit.mts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/main.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/main.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/main.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/main.ts diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/package.json b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/package.json similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/package.json rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/package.json diff --git a/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/print.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/print.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/print.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/print.ts diff --git a/packages/cli/src/modules/build/tests/transforms/transforms.test.ts b/packages/cli-module-build/src/tests/transforms/transforms.test.ts similarity index 99% rename from packages/cli/src/modules/build/tests/transforms/transforms.test.ts rename to packages/cli-module-build/src/tests/transforms/transforms.test.ts index 9e4fca8c63..71ee03df76 100644 --- a/packages/cli/src/modules/build/tests/transforms/transforms.test.ts +++ b/packages/cli-module-build/src/tests/transforms/transforms.test.ts @@ -55,7 +55,7 @@ function loadFixture(fixture: string) { 'node', [ '--import', - '@backstage/cli/config/nodeTransform.cjs', + require.resolve('@backstage/cli-node/config/nodeTransform.cjs'), resolvePath(__dirname, `__fixtures__/${fixture}`), ], { encoding: 'utf8' }, diff --git a/packages/cli-module-build/templates/serve_index.html b/packages/cli-module-build/templates/serve_index.html new file mode 100644 index 0000000000..bc91f1bc04 --- /dev/null +++ b/packages/cli-module-build/templates/serve_index.html @@ -0,0 +1,27 @@ + + + + + + + + Backstage + + + +
+ + + diff --git a/packages/cli-module-config/.eslintrc.js b/packages/cli-module-config/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-config/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-config/README.md b/packages/cli-module-config/README.md new file mode 100644 index 0000000000..dc5b266d98 --- /dev/null +++ b/packages/cli-module-config/README.md @@ -0,0 +1,18 @@ +# @backstage/cli-module-config + +CLI module that provides configuration inspection commands for the Backstage CLI. + +## Commands + +| Command | Description | +| :-------------- | :------------------------------------------------------------- | +| `config docs` | Browse the configuration reference documentation | +| `config:print` | Print the app configuration for the current package | +| `config:check` | Validate that the given configuration loads and matches schema | +| `config schema` | Print the JSON schema for the given configuration | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) +- [Static Configuration](https://backstage.io/docs/conf/) diff --git a/packages/cli-module-config/bin/backstage-cli-module-config b/packages/cli-module-config/bin/backstage-cli-module-config new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-config/bin/backstage-cli-module-config @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-config/catalog-info.yaml b/packages/cli-module-config/catalog-info.yaml new file mode 100644 index 0000000000..b7c5d61aba --- /dev/null +++ b/packages/cli-module-config/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-config + title: '@backstage/cli-module-config' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-config/cli-report.md b/packages/cli-module-config/cli-report.md new file mode 100644 index 0000000000..953c347951 --- /dev/null +++ b/packages/cli-module-config/cli-report.md @@ -0,0 +1,109 @@ +## CLI Report file for "@backstage/cli-module-config" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-config` + +``` +Usage: @backstage/cli-module-config [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + config [command] + config:check + config:docs + config:print + config:schema + help [command] +``` + +### `backstage-cli-module-config config` + +``` +Usage: @backstage/cli-module-config config [options] [command] [command] + +Options: + -h, --help + +Commands: + docs + help [command] + schema +``` + +### `backstage-cli-module-config config docs` + +``` +Usage: @backstage/cli-module-config config docs + +Options: + --package + -h, --help +``` + +### `backstage-cli-module-config config schema` + +``` +Usage: @backstage/cli-module-config config schema + +Options: + --format + --merge + --package + -h, --help +``` + +### `backstage-cli-module-config config:check` + +``` +Usage: @backstage/cli-module-config config:check + +Options: + --config + --deprecated + --frontend + --lax + --package + --strict + -h, --help +``` + +### `backstage-cli-module-config config:docs` + +``` +Usage: @backstage/cli-module-config config:docs + +Options: + --package + -h, --help +``` + +### `backstage-cli-module-config config:print` + +``` +Usage: @backstage/cli-module-config config:print + +Options: + --config + --format + --frontend + --lax + --package + --with-secrets + -h, --help +``` + +### `backstage-cli-module-config config:schema` + +``` +Usage: @backstage/cli-module-config config:schema + +Options: + --format + --merge + --package + -h, --help +``` diff --git a/packages/cli-module-config/package.json b/packages/cli-module-config/package.json new file mode 100644 index 0000000000..bbdf6070a3 --- /dev/null +++ b/packages/cli-module-config/package.json @@ -0,0 +1,52 @@ +{ + "name": "@backstage/cli-module-config", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-config" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/config-loader": "workspace:^", + "@backstage/types": "workspace:^", + "@manypkg/get-packages": "^1.1.3", + "chalk": "^4.0.0", + "cleye": "^2.3.0", + "json-schema": "^0.4.0", + "react-dev-utils": "^12.0.0-next.60", + "yaml": "^2.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/json-schema": "^7.0.6" + }, + "bin": "bin/backstage-cli-module-config" +} diff --git a/packages/cli-module-config/report.api.md b/packages/cli-module-config/report.api.md new file mode 100644 index 0000000000..740aef21fb --- /dev/null +++ b/packages/cli-module-config/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-config" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/config/commands/docs.ts b/packages/cli-module-config/src/commands/docs.ts similarity index 94% rename from packages/cli/src/modules/config/commands/docs.ts rename to packages/cli-module-config/src/commands/docs.ts index 84d2f384e1..54cc1832f5 100644 --- a/packages/cli/src/modules/config/commands/docs.ts +++ b/packages/cli-module-config/src/commands/docs.ts @@ -21,11 +21,11 @@ import { JSONSchema7 as JSONSchema } from 'json-schema'; import openBrowser from 'react-dev-utils/openBrowser'; import chalk from 'chalk'; import { loadCliConfig } from '../lib/config'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; const DOCS_URL = 'https://config.backstage.io'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { package: pkg }, } = cli( diff --git a/packages/cli/src/modules/config/commands/print.ts b/packages/cli-module-config/src/commands/print.ts similarity index 96% rename from packages/cli/src/modules/config/commands/print.ts rename to packages/cli-module-config/src/commands/print.ts index 33f7245604..08f785b1d0 100644 --- a/packages/cli/src/modules/config/commands/print.ts +++ b/packages/cli-module-config/src/commands/print.ts @@ -19,9 +19,9 @@ import { stringify as stringifyYaml } from 'yaml'; import { AppConfig, ConfigReader } from '@backstage/config'; import { loadCliConfig } from '../lib/config'; import { ConfigSchema, ConfigVisibility } from '@backstage/config-loader'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { config, lax, frontend, withSecrets, format, package: pkg }, } = cli( diff --git a/packages/cli/src/modules/config/commands/schema.ts b/packages/cli-module-config/src/commands/schema.ts similarity index 94% rename from packages/cli/src/modules/config/commands/schema.ts rename to packages/cli-module-config/src/commands/schema.ts index 13f651534c..4eaa8189ba 100644 --- a/packages/cli/src/modules/config/commands/schema.ts +++ b/packages/cli-module-config/src/commands/schema.ts @@ -20,9 +20,9 @@ import { stringify as stringifyYaml } from 'yaml'; import { loadCliConfig } from '../lib/config'; import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { merge, format, package: pkg }, } = cli( diff --git a/packages/cli/src/modules/config/commands/validate.ts b/packages/cli-module-config/src/commands/validate.ts similarity index 93% rename from packages/cli/src/modules/config/commands/validate.ts rename to packages/cli-module-config/src/commands/validate.ts index f6cf9cc662..326c6b5e42 100644 --- a/packages/cli/src/modules/config/commands/validate.ts +++ b/packages/cli-module-config/src/commands/validate.ts @@ -16,9 +16,9 @@ import { cli } from 'cleye'; import { loadCliConfig } from '../lib/config'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { config, lax, frontend, deprecated, strict, package: pkg }, } = cli( diff --git a/packages/cli/src/modules/config/index.ts b/packages/cli-module-config/src/index.ts similarity index 85% rename from packages/cli/src/modules/config/index.ts rename to packages/cli-module-config/src/index.ts index 0d90037f18..a041f32d6e 100644 --- a/packages/cli/src/modules/config/index.ts +++ b/packages/cli-module-config/src/index.ts @@ -13,17 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; -export const configOption = [ - '--config ', - 'Config files to load instead of app-config.yaml', - (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]), - Array(), -] as const; - -export default createCliPlugin({ - pluginId: 'config', +export default createCliModule({ + packageJson, init: async reg => { reg.addCommand({ path: ['config:docs'], diff --git a/packages/cli/src/modules/config/lib/config.ts b/packages/cli-module-config/src/lib/config.ts similarity index 100% rename from packages/cli/src/modules/config/lib/config.ts rename to packages/cli-module-config/src/lib/config.ts diff --git a/packages/cli-module-github/.eslintrc.js b/packages/cli-module-github/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-github/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-github/README.md b/packages/cli-module-github/README.md new file mode 100644 index 0000000000..6c38376901 --- /dev/null +++ b/packages/cli-module-github/README.md @@ -0,0 +1,14 @@ +# @backstage/cli-module-github + +CLI module that provides the `create-github-app` command for the Backstage CLI, used to create a new GitHub App in your organization for use with Backstage. + +## Commands + +| Command | Description | +| :------------------ | :----------------------------------------- | +| `create-github-app` | Create new GitHub App in your organization | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-github/bin/backstage-cli-module-github b/packages/cli-module-github/bin/backstage-cli-module-github new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-github/bin/backstage-cli-module-github @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-github/catalog-info.yaml b/packages/cli-module-github/catalog-info.yaml new file mode 100644 index 0000000000..b694da6f17 --- /dev/null +++ b/packages/cli-module-github/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-github + title: '@backstage/cli-module-github' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-github/cli-report.md b/packages/cli-module-github/cli-report.md new file mode 100644 index 0000000000..c606ac5988 --- /dev/null +++ b/packages/cli-module-github/cli-report.md @@ -0,0 +1,26 @@ +## CLI Report file for "@backstage/cli-module-github" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-github` + +``` +Usage: @backstage/cli-module-github [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + create-github-app + help [command] +``` + +### `backstage-cli-module-github create-github-app` + +``` +Usage: @backstage/cli-module-github create-github-app + +Options: + -h, --help +``` diff --git a/packages/cli-module-github/package.json b/packages/cli-module-github/package.json new file mode 100644 index 0000000000..1e8ca4fe6a --- /dev/null +++ b/packages/cli-module-github/package.json @@ -0,0 +1,52 @@ +{ + "name": "@backstage/cli-module-github", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-github" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "@octokit/request": "^8.0.0", + "chalk": "^4.0.0", + "cleye": "^2.3.0", + "express": "^4.22.0", + "fs-extra": "^11.2.0", + "inquirer": "^8.2.0", + "react-dev-utils": "^12.0.0-next.60", + "yaml": "^2.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/express": "^4.17.6", + "@types/fs-extra": "^11.0.0" + }, + "bin": "bin/backstage-cli-module-github" +} diff --git a/packages/cli-module-github/report.api.md b/packages/cli-module-github/report.api.md new file mode 100644 index 0000000000..c389a88b5f --- /dev/null +++ b/packages/cli-module-github/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-github" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/create-github-app/commands/create-github-app/GithubCreateAppServer.ts b/packages/cli-module-github/src/commands/create-github-app/GithubCreateAppServer.ts similarity index 100% rename from packages/cli/src/modules/create-github-app/commands/create-github-app/GithubCreateAppServer.ts rename to packages/cli-module-github/src/commands/create-github-app/GithubCreateAppServer.ts diff --git a/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts b/packages/cli-module-github/src/commands/create-github-app/index.ts similarity index 97% rename from packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts rename to packages/cli-module-github/src/commands/create-github-app/index.ts index 9dd9f4c4c4..bb6896c9c1 100644 --- a/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts +++ b/packages/cli-module-github/src/commands/create-github-app/index.ts @@ -23,12 +23,12 @@ import { cli } from 'cleye'; import { GithubCreateAppServer } from './GithubCreateAppServer'; import openBrowser from 'react-dev-utils/openBrowser'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; // This is an experimental command that at this point does not support GitHub Enterprise // due to lacking support for creating apps from manifests. // https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-a-github-app-from-a-manifest -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { _: positionals } = cli( { help: { ...info, usage: `${info.usage} ` }, diff --git a/packages/cli/src/modules/create-github-app/index.ts b/packages/cli-module-github/src/index.ts similarity index 85% rename from packages/cli/src/modules/create-github-app/index.ts rename to packages/cli-module-github/src/index.ts index 9dad46577e..c3bd7cf95f 100644 --- a/packages/cli/src/modules/create-github-app/index.ts +++ b/packages/cli-module-github/src/index.ts @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; -export default createCliPlugin({ - pluginId: 'new', +export default createCliModule({ + packageJson, init: async reg => { reg.addCommand({ path: ['create-github-app'], diff --git a/packages/cli-module-info/.eslintrc.js b/packages/cli-module-info/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-info/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-info/README.md b/packages/cli-module-info/README.md new file mode 100644 index 0000000000..1ba0cb3799 --- /dev/null +++ b/packages/cli-module-info/README.md @@ -0,0 +1,14 @@ +# @backstage/cli-module-info + +CLI module that provides the `info` command for the Backstage CLI, displaying environment and dependency information useful for debugging and bug reports. + +## Commands + +| Command | Description | +| :------ | :-------------------------------------------------------- | +| `info` | Show helpful information for debugging and reporting bugs | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-info/bin/backstage-cli-module-info b/packages/cli-module-info/bin/backstage-cli-module-info new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-info/bin/backstage-cli-module-info @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-info/catalog-info.yaml b/packages/cli-module-info/catalog-info.yaml new file mode 100644 index 0000000000..2f6eb65952 --- /dev/null +++ b/packages/cli-module-info/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-info + title: '@backstage/cli-module-info' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-info/cli-report.md b/packages/cli-module-info/cli-report.md new file mode 100644 index 0000000000..f83583b781 --- /dev/null +++ b/packages/cli-module-info/cli-report.md @@ -0,0 +1,28 @@ +## CLI Report file for "@backstage/cli-module-info" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-info` + +``` +Usage: @backstage/cli-module-info [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + info +``` + +### `backstage-cli-module-info info` + +``` +Usage: @backstage/cli-module-info info + +Options: + --format + --include + -h, --help +``` diff --git a/packages/cli-module-info/package.json b/packages/cli-module-info/package.json new file mode 100644 index 0000000000..db19b358f4 --- /dev/null +++ b/packages/cli-module-info/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/cli-module-info", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-info" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "cleye": "^2.3.0", + "fs-extra": "^11.2.0", + "minimatch": "^10.2.1" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^11.0.0" + }, + "bin": "bin/backstage-cli-module-info" +} diff --git a/packages/cli-module-info/report.api.md b/packages/cli-module-info/report.api.md new file mode 100644 index 0000000000..764b16ea47 --- /dev/null +++ b/packages/cli-module-info/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-info" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/info/commands/info.ts b/packages/cli-module-info/src/commands/info.ts similarity index 88% rename from packages/cli/src/modules/info/commands/info.ts rename to packages/cli-module-info/src/commands/info.ts index ab26714fca..e7307e18c2 100644 --- a/packages/cli/src/modules/info/commands/info.ts +++ b/packages/cli-module-info/src/commands/info.ts @@ -15,7 +15,7 @@ */ import { cli } from 'cleye'; -import { version as cliVersion } from '../../../../package.json'; +import { version as infoModuleVersion } from '../../package.json'; import os from 'node:os'; import { runOutput, targetPaths, findOwnPaths } from '@backstage/cli-common'; import { @@ -25,7 +25,7 @@ import { } from '@backstage/cli-node'; import { minimatch } from 'minimatch'; import fs from 'fs-extra'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; /** * Attempts to read package.json from node_modules for a given package name. @@ -52,7 +52,7 @@ function hasBackstageField(packageName: string, targetPath: string): boolean { return pkg?.backstage !== undefined; } -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { include, format }, } = cli( @@ -99,12 +99,27 @@ export default async ({ args, info }: CommandContext) => { } } - // Build system info + // Try to resolve the CLI package version — it may not be installed + // when this module is executed standalone. + let cliVersion: string | undefined; + try { + cliVersion = ( + require(require.resolve('@backstage/cli/package.json', { + paths: [process.cwd()], + })) as { version: string } + ).version; + } catch { + /* not available */ + } + const systemInfo = { os: `${os.type} ${os.release} - ${os.platform}/${os.arch}`, node: process.version, yarn: yarnVersion, - cli: { version: cliVersion, local: isLocal }, + ...(cliVersion + ? { cli: { version: cliVersion, local: isLocal } } + : undefined), + infoModuleVersion, backstage: backstageVersion, }; @@ -208,8 +223,11 @@ export default async ({ args, info }: CommandContext) => { console.log(`OS: ${systemInfo.os}`); console.log(`node: ${systemInfo.node}`); console.log(`yarn: ${systemInfo.yarn}`); - console.log(`cli: ${cliVersion} (${isLocal ? 'local' : 'installed'})`); - console.log(`backstage: ${backstageVersion}`); + if (cliVersion) { + console.log(`cli: ${cliVersion} (${isLocal ? 'local' : 'installed'})`); + } + console.log(`info module: ${infoModuleVersion}`); + console.log(`backstage: ${backstageVersion}`); console.log(); // Print installed dependencies diff --git a/packages/cli/src/modules/info/index.ts b/packages/cli-module-info/src/index.ts similarity index 85% rename from packages/cli/src/modules/info/index.ts rename to packages/cli-module-info/src/index.ts index 8e1fe9cbff..c7ace24bbd 100644 --- a/packages/cli/src/modules/info/index.ts +++ b/packages/cli-module-info/src/index.ts @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; -export default createCliPlugin({ - pluginId: 'info', +export default createCliModule({ + packageJson, init: async reg => { reg.addCommand({ path: ['info'], diff --git a/packages/cli-module-lint/.eslintrc.js b/packages/cli-module-lint/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-lint/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-lint/README.md b/packages/cli-module-lint/README.md new file mode 100644 index 0000000000..23d1f06e7e --- /dev/null +++ b/packages/cli-module-lint/README.md @@ -0,0 +1,15 @@ +# @backstage/cli-module-lint + +CLI module that provides linting commands for the Backstage CLI. + +## Commands + +| Command | Description | +| :------------- | :---------------- | +| `package lint` | Lint a package | +| `repo lint` | Lint a repository | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-lint/bin/backstage-cli-module-lint b/packages/cli-module-lint/bin/backstage-cli-module-lint new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-lint/bin/backstage-cli-module-lint @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-lint/catalog-info.yaml b/packages/cli-module-lint/catalog-info.yaml new file mode 100644 index 0000000000..d40b7e23e3 --- /dev/null +++ b/packages/cli-module-lint/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-lint + title: '@backstage/cli-module-lint' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-lint/cli-report.md b/packages/cli-module-lint/cli-report.md new file mode 100644 index 0000000000..2a9ff6d61f --- /dev/null +++ b/packages/cli-module-lint/cli-report.md @@ -0,0 +1,73 @@ +## CLI Report file for "@backstage/cli-module-lint" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-lint` + +``` +Usage: @backstage/cli-module-lint [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + package [command] + repo [command] +``` + +### `backstage-cli-module-lint package` + +``` +Usage: @backstage/cli-module-lint package [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + lint +``` + +### `backstage-cli-module-lint package lint` + +``` +Usage: @backstage/cli-module-lint package lint [directories...] + +Options: + --fix + --format + --max-warnings + --output-file + -h, --help +``` + +### `backstage-cli-module-lint repo` + +``` +Usage: @backstage/cli-module-lint repo [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + lint +``` + +### `backstage-cli-module-lint repo lint` + +``` +Usage: @backstage/cli-module-lint repo lint + +Options: + --fix + --format + --max-warnings + --output-file + --since + --success-cache + --success-cache-dir + -h, --help +``` diff --git a/packages/cli-module-lint/package.json b/packages/cli-module-lint/package.json new file mode 100644 index 0000000000..452e10eeed --- /dev/null +++ b/packages/cli-module-lint/package.json @@ -0,0 +1,51 @@ +{ + "name": "@backstage/cli-module-lint", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-lint" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "chalk": "^4.0.0", + "cleye": "^2.3.0", + "eslint": "^8.6.0", + "eslint-formatter-friendly": "^7.0.0", + "fs-extra": "^11.2.0", + "globby": "^11.1.0", + "shell-quote": "^1.8.1" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^11.0.0", + "@types/shell-quote": "^1.7.5" + }, + "bin": "bin/backstage-cli-module-lint" +} diff --git a/packages/cli-module-lint/report.api.md b/packages/cli-module-lint/report.api.md new file mode 100644 index 0000000000..4a861a27a0 --- /dev/null +++ b/packages/cli-module-lint/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-lint" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/lint/commands/package/lint.ts b/packages/cli-module-lint/src/commands/package/lint.ts similarity index 95% rename from packages/cli/src/modules/lint/commands/package/lint.ts rename to packages/cli-module-lint/src/commands/package/lint.ts index 39c74bab73..7fabf0c0ff 100644 --- a/packages/cli/src/modules/lint/commands/package/lint.ts +++ b/packages/cli-module-lint/src/commands/package/lint.ts @@ -18,9 +18,9 @@ import fs from 'fs-extra'; import { cli } from 'cleye'; import { targetPaths } from '@backstage/cli-common'; import { ESLint } from 'eslint'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { fix, format, outputFile, maxWarnings }, _: directories, diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli-module-lint/src/commands/repo/lint.ts similarity index 98% rename from packages/cli/src/modules/lint/commands/repo/lint.ts rename to packages/cli-module-lint/src/commands/repo/lint.ts index 5ce4be72f8..01dfad6339 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli-module-lint/src/commands/repo/lint.ts @@ -29,7 +29,7 @@ import { import { targetPaths } from '@backstage/cli-common'; import { createScriptOptionsParser } from '../../lib/optionsParser'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; function depCount(pkg: BackstagePackageJson) { const deps = pkg.dependencies ? Object.keys(pkg.dependencies).length : 0; @@ -39,7 +39,7 @@ function depCount(pkg: BackstagePackageJson) { return deps + devDeps; } -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { for (const flag of [ 'outputFile', 'successCache', diff --git a/packages/cli/src/modules/lint/index.ts b/packages/cli-module-lint/src/index.ts similarity index 86% rename from packages/cli/src/modules/lint/index.ts rename to packages/cli-module-lint/src/index.ts index 8e02b7653f..d202841e79 100644 --- a/packages/cli/src/modules/lint/index.ts +++ b/packages/cli-module-lint/src/index.ts @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; -export default createCliPlugin({ - pluginId: 'lint', +export default createCliModule({ + packageJson, init: async reg => { reg.addCommand({ path: ['package', 'lint'], diff --git a/packages/cli/src/modules/lint/lib/optionsParser.ts b/packages/cli-module-lint/src/lib/optionsParser.ts similarity index 100% rename from packages/cli/src/modules/lint/lib/optionsParser.ts rename to packages/cli-module-lint/src/lib/optionsParser.ts diff --git a/packages/cli-module-maintenance/.eslintrc.js b/packages/cli-module-maintenance/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-maintenance/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-maintenance/README.md b/packages/cli-module-maintenance/README.md new file mode 100644 index 0000000000..f13e78a32e --- /dev/null +++ b/packages/cli-module-maintenance/README.md @@ -0,0 +1,15 @@ +# @backstage/cli-module-maintenance + +CLI module that provides repository maintenance commands for the Backstage CLI. + +## Commands + +| Command | Description | +| :----------------------- | :---------------------------------------- | +| `repo fix` | Automatically fix packages in the project | +| `repo list-deprecations` | List deprecations | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-maintenance/bin/backstage-cli-module-maintenance b/packages/cli-module-maintenance/bin/backstage-cli-module-maintenance new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-maintenance/bin/backstage-cli-module-maintenance @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-maintenance/catalog-info.yaml b/packages/cli-module-maintenance/catalog-info.yaml new file mode 100644 index 0000000000..df05034045 --- /dev/null +++ b/packages/cli-module-maintenance/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-maintenance + title: '@backstage/cli-module-maintenance' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-maintenance/cli-report.md b/packages/cli-module-maintenance/cli-report.md new file mode 100644 index 0000000000..874d9d781d --- /dev/null +++ b/packages/cli-module-maintenance/cli-report.md @@ -0,0 +1,52 @@ +## CLI Report file for "@backstage/cli-module-maintenance" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-maintenance` + +``` +Usage: @backstage/cli-module-maintenance [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + repo [command] +``` + +### `backstage-cli-module-maintenance repo` + +``` +Usage: @backstage/cli-module-maintenance repo [options] [command] [command] + +Options: + -h, --help + +Commands: + fix + help [command] + list-deprecations +``` + +### `backstage-cli-module-maintenance repo fix` + +``` +Usage: @backstage/cli-module-maintenance repo fix + +Options: + --check + --publish + -h, --help +``` + +### `backstage-cli-module-maintenance repo list-deprecations` + +``` +Usage: @backstage/cli-module-maintenance repo list-deprecations + +Options: + --json + -h, --help +``` diff --git a/packages/cli-module-maintenance/package.json b/packages/cli-module-maintenance/package.json new file mode 100644 index 0000000000..24f7a9b70a --- /dev/null +++ b/packages/cli-module-maintenance/package.json @@ -0,0 +1,47 @@ +{ + "name": "@backstage/cli-module-maintenance", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-maintenance" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "chalk": "^4.0.0", + "cleye": "^2.3.0", + "eslint": "^8.6.0", + "fs-extra": "^11.2.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^11.0.0" + }, + "bin": "bin/backstage-cli-module-maintenance" +} diff --git a/packages/cli-module-maintenance/report.api.md b/packages/cli-module-maintenance/report.api.md new file mode 100644 index 0000000000..9eb59483d7 --- /dev/null +++ b/packages/cli-module-maintenance/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-maintenance" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/maintenance/commands/repo/fix.ts b/packages/cli-module-maintenance/src/commands/repo/fix.ts similarity index 98% rename from packages/cli/src/modules/maintenance/commands/repo/fix.ts rename to packages/cli-module-maintenance/src/commands/repo/fix.ts index dc2e554121..4d6445fe86 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/fix.ts +++ b/packages/cli-module-maintenance/src/commands/repo/fix.ts @@ -294,7 +294,12 @@ export function fixPluginId(pkg: FixablePackage) { return; } - if (role === 'backend' || role === 'frontend' || role === 'cli') { + if ( + role === 'backend' || + role === 'frontend' || + role === 'cli' || + role === 'cli-module' + ) { return; } @@ -376,7 +381,12 @@ export function fixPluginPackages( return; } - if (role === 'backend' || role === 'frontend' || role === 'cli') { + if ( + role === 'backend' || + role === 'frontend' || + role === 'cli' || + role === 'cli-module' + ) { return; } @@ -495,7 +505,7 @@ type PackageFixer = (pkg: FixablePackage, packages: FixablePackage[]) => void; export default async ({ args, info, -}: import('../../../../wiring/types').CommandContext) => { +}: import('@backstage/cli-node').CliCommandContext) => { const { flags: { publish, check }, } = cli( diff --git a/packages/cli/src/modules/maintenance/commands/repo/list-deprecations.ts b/packages/cli-module-maintenance/src/commands/repo/list-deprecations.ts similarity index 95% rename from packages/cli/src/modules/maintenance/commands/repo/list-deprecations.ts rename to packages/cli-module-maintenance/src/commands/repo/list-deprecations.ts index 23f64463cb..e711cb32be 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/list-deprecations.ts +++ b/packages/cli-module-maintenance/src/commands/repo/list-deprecations.ts @@ -20,9 +20,9 @@ import { cli } from 'cleye'; import { relative as relativePath } from 'node:path'; import { PackageGraph } from '@backstage/cli-node'; import { targetPaths } from '@backstage/cli-common'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { json }, } = cli( diff --git a/packages/cli/src/modules/maintenance/index.ts b/packages/cli-module-maintenance/src/index.ts similarity index 87% rename from packages/cli/src/modules/maintenance/index.ts rename to packages/cli-module-maintenance/src/index.ts index a6597559f8..77304eef47 100644 --- a/packages/cli/src/modules/maintenance/index.ts +++ b/packages/cli-module-maintenance/src/index.ts @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; -export default createCliPlugin({ - pluginId: 'maintenance', +export default createCliModule({ + packageJson, init: async reg => { reg.addCommand({ path: ['repo', 'fix'], diff --git a/packages/cli-module-migrate/.eslintrc.js b/packages/cli-module-migrate/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-migrate/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-migrate/README.md b/packages/cli-module-migrate/README.md new file mode 100644 index 0000000000..842f0e645c --- /dev/null +++ b/packages/cli-module-migrate/README.md @@ -0,0 +1,20 @@ +# @backstage/cli-module-migrate + +CLI module that provides migration and version management commands for the Backstage CLI. + +## Commands + +| Command | Description | +| :----------------------------- | :---------------------------------------------------------------- | +| `versions:bump` | Bump Backstage packages to the latest versions | +| `versions:migrate` | Migrate plugins moved to the @backstage-community namespace | +| `migrate package-roles` | Add package role field to packages that don't have it | +| `migrate package-scripts` | Set package scripts according to each package role | +| `migrate package-exports` | Synchronize package subpath export definitions | +| `migrate package-lint-configs` | Migrates all packages to use @backstage/cli/config/eslint-factory | +| `migrate react-router-deps` | Migrates the react-router dependencies to be peer dependencies | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-migrate/bin/backstage-cli-module-migrate b/packages/cli-module-migrate/bin/backstage-cli-module-migrate new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-migrate/bin/backstage-cli-module-migrate @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-migrate/catalog-info.yaml b/packages/cli-module-migrate/catalog-info.yaml new file mode 100644 index 0000000000..5c62edf9ba --- /dev/null +++ b/packages/cli-module-migrate/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-migrate + title: '@backstage/cli-module-migrate' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-migrate/cli-report.md b/packages/cli-module-migrate/cli-report.md new file mode 100644 index 0000000000..f0c29e1edd --- /dev/null +++ b/packages/cli-module-migrate/cli-report.md @@ -0,0 +1,105 @@ +## CLI Report file for "@backstage/cli-module-migrate" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-migrate` + +``` +Usage: @backstage/cli-module-migrate [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + migrate [command] + versions:bump + versions:migrate +``` + +### `backstage-cli-module-migrate migrate` + +``` +Usage: @backstage/cli-module-migrate migrate [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + package-exports + package-lint-configs + package-roles + package-scripts + react-router-deps +``` + +### `backstage-cli-module-migrate migrate package-exports` + +``` +Usage: @backstage/cli-module-migrate migrate package-exports + +Options: + -h, --help +``` + +### `backstage-cli-module-migrate migrate package-lint-configs` + +``` +Usage: @backstage/cli-module-migrate migrate package-lint-configs + +Options: + -h, --help +``` + +### `backstage-cli-module-migrate migrate package-roles` + +``` +Usage: @backstage/cli-module-migrate migrate package-roles + +Options: + -h, --help +``` + +### `backstage-cli-module-migrate migrate package-scripts` + +``` +Usage: @backstage/cli-module-migrate migrate package-scripts + +Options: + -h, --help +``` + +### `backstage-cli-module-migrate migrate react-router-deps` + +``` +Usage: @backstage/cli-module-migrate migrate react-router-deps + +Options: + -h, --help +``` + +### `backstage-cli-module-migrate versions:bump` + +``` +Usage: @backstage/cli-module-migrate versions:bump + +Options: + --pattern + --release + --skip-install + --skip-migrate + -h, --help +``` + +### `backstage-cli-module-migrate versions:migrate` + +``` +Usage: @backstage/cli-module-migrate versions:migrate + +Options: + --pattern + --skip-code-changes + -h, --help +``` diff --git a/packages/cli-module-migrate/package.json b/packages/cli-module-migrate/package.json new file mode 100644 index 0000000000..dfc155fd11 --- /dev/null +++ b/packages/cli-module-migrate/package.json @@ -0,0 +1,57 @@ +{ + "name": "@backstage/cli-module-migrate", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-migrate" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/release-manifests": "workspace:^", + "@manypkg/get-packages": "^1.1.3", + "chalk": "^4.0.0", + "cleye": "^2.3.0", + "fs-extra": "^11.2.0", + "minimatch": "^10.2.1", + "ora": "^5.3.0", + "replace-in-file": "^7.1.0", + "semver": "^7.5.3" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@types/fs-extra": "^11.0.0", + "@types/semver": "^7", + "msw": "^1.0.0" + }, + "bin": "bin/backstage-cli-module-migrate" +} diff --git a/packages/cli-module-migrate/report.api.md b/packages/cli-module-migrate/report.api.md new file mode 100644 index 0000000000..09ed6c902a --- /dev/null +++ b/packages/cli-module-migrate/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-migrate" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/migrate/commands/packageExports.ts b/packages/cli-module-migrate/src/commands/packageExports.ts similarity index 86% rename from packages/cli/src/modules/migrate/commands/packageExports.ts rename to packages/cli-module-migrate/src/commands/packageExports.ts index 7fe6a49b35..8a4ad45f42 100644 --- a/packages/cli/src/modules/migrate/commands/packageExports.ts +++ b/packages/cli-module-migrate/src/commands/packageExports.ts @@ -15,9 +15,9 @@ */ import { cli } from 'cleye'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); throw new Error( 'The `migrate package-exports` command has been removed, use `repo fix` instead.', diff --git a/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts b/packages/cli-module-migrate/src/commands/packageLintConfigs.ts similarity index 95% rename from packages/cli/src/modules/migrate/commands/packageLintConfigs.ts rename to packages/cli-module-migrate/src/commands/packageLintConfigs.ts index 73cbe380ec..660ed35bb6 100644 --- a/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts +++ b/packages/cli-module-migrate/src/commands/packageLintConfigs.ts @@ -19,11 +19,11 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { PackageGraph } from '@backstage/cli-node'; import { runOutput } from '@backstage/cli-common'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; const PREFIX = `module.exports = require('@backstage/cli/config/eslint-factory')`; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); const packages = await PackageGraph.listTargetPackages(); diff --git a/packages/cli/src/modules/migrate/commands/packageRole.ts b/packages/cli-module-migrate/src/commands/packageRole.ts similarity index 94% rename from packages/cli/src/modules/migrate/commands/packageRole.ts rename to packages/cli-module-migrate/src/commands/packageRole.ts index cc909a351c..8758d48dd5 100644 --- a/packages/cli/src/modules/migrate/commands/packageRole.ts +++ b/packages/cli-module-migrate/src/commands/packageRole.ts @@ -20,9 +20,9 @@ import { resolve as resolvePath } from 'node:path'; import { getPackages } from '@manypkg/get-packages'; import { PackageRoles } from '@backstage/cli-node'; import { targetPaths } from '@backstage/cli-common'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); const { packages } = await getPackages(targetPaths.dir); diff --git a/packages/cli/src/modules/migrate/commands/packageScripts.ts b/packages/cli-module-migrate/src/commands/packageScripts.ts similarity index 94% rename from packages/cli/src/modules/migrate/commands/packageScripts.ts rename to packages/cli-module-migrate/src/commands/packageScripts.ts index 901cd8fc82..94a05f67f9 100644 --- a/packages/cli/src/modules/migrate/commands/packageScripts.ts +++ b/packages/cli-module-migrate/src/commands/packageScripts.ts @@ -18,13 +18,13 @@ import { cli } from 'cleye'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { PackageGraph, PackageRoles, PackageRole } from '@backstage/cli-node'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; const configArgPattern = /--config[=\s][^\s$]+/; -const noStartRoles: PackageRole[] = ['cli', 'common-library']; +const noStartRoles: PackageRole[] = ['cli', 'cli-module', 'common-library']; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); const packages = await PackageGraph.listTargetPackages(); diff --git a/packages/cli/src/modules/migrate/commands/reactRouterDeps.ts b/packages/cli-module-migrate/src/commands/reactRouterDeps.ts similarity index 94% rename from packages/cli/src/modules/migrate/commands/reactRouterDeps.ts rename to packages/cli-module-migrate/src/commands/reactRouterDeps.ts index b2208e2dc4..802d958e61 100644 --- a/packages/cli/src/modules/migrate/commands/reactRouterDeps.ts +++ b/packages/cli-module-migrate/src/commands/reactRouterDeps.ts @@ -18,12 +18,12 @@ import { cli } from 'cleye'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { PackageGraph, PackageRoles } from '@backstage/cli-node'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; const REACT_ROUTER_DEPS = ['react-router', 'react-router-dom']; const REACT_ROUTER_RANGE = '6.0.0-beta.0 || ^6.3.0'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); const packages = await PackageGraph.listTargetPackages(); diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts b/packages/cli-module-migrate/src/commands/versions/bump.test.ts similarity index 99% rename from packages/cli/src/modules/migrate/commands/versions/bump.test.ts rename to packages/cli-module-migrate/src/commands/versions/bump.test.ts index f119a0bedb..c7694f5e09 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts +++ b/packages/cli-module-migrate/src/commands/versions/bump.test.ts @@ -125,7 +125,7 @@ const expectLogsToMatch = ( expect(receivedLogs.filter(Boolean).sort()).toEqual(expected.sort()); }; -const info = { usage: 'backstage-cli versions:bump', description: '' }; +const info = { usage: 'backstage-cli versions:bump', name: 'versions:bump' }; describe('bump', () => { const mockDir = createMockDirectory(); diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli-module-migrate/src/commands/versions/bump.ts similarity index 99% rename from packages/cli/src/modules/migrate/commands/versions/bump.ts rename to packages/cli-module-migrate/src/commands/versions/bump.ts index 8048c21505..518126a3de 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli-module-migrate/src/commands/versions/bump.ts @@ -48,7 +48,7 @@ import { import { migrateMovedPackages } from './migrate'; import { runYarnInstall } from '../../lib/utils'; import { run } from '@backstage/cli-common'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; const DEP_TYPES = [ 'dependencies', @@ -74,7 +74,7 @@ function extendsDefaultPattern(pattern: string): boolean { return minimatch('@backstage/', pattern.slice(0, -1)); } -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { pattern: patternFlag, release, skipInstall, skipMigrate }, } = cli( diff --git a/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts b/packages/cli-module-migrate/src/commands/versions/migrate.test.ts similarity index 97% rename from packages/cli/src/modules/migrate/commands/versions/migrate.test.ts rename to packages/cli-module-migrate/src/commands/versions/migrate.test.ts index fc50a5c491..606fa528b1 100644 --- a/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts +++ b/packages/cli-module-migrate/src/commands/versions/migrate.test.ts @@ -123,7 +123,7 @@ describe('versions:migrate', () => { }); const { warn, log: logs } = await withLogCollector(async () => { - await migrate({ args: [], info: { usage: 'test', description: 'test' } }); + await migrate({ args: [], info: { usage: 'test', name: 'test' } }); }); expectLogsToMatch(logs, [ @@ -229,7 +229,7 @@ describe('versions:migrate', () => { }); await withLogCollector(async () => { - await migrate({ args: [], info: { usage: 'test', description: 'test' } }); + await migrate({ args: [], info: { usage: 'test', name: 'test' } }); }); expect(runObj.run).toHaveBeenCalledTimes(1); @@ -311,7 +311,7 @@ describe('versions:migrate', () => { }); await withLogCollector(async () => { - await migrate({ args: [], info: { usage: 'test', description: 'test' } }); + await migrate({ args: [], info: { usage: 'test', name: 'test' } }); }); expect(runObj.run).toHaveBeenCalledTimes(1); diff --git a/packages/cli/src/modules/migrate/commands/versions/migrate.ts b/packages/cli-module-migrate/src/commands/versions/migrate.ts similarity index 97% rename from packages/cli/src/modules/migrate/commands/versions/migrate.ts rename to packages/cli-module-migrate/src/commands/versions/migrate.ts index 1286af015e..a8ecb6cad4 100644 --- a/packages/cli/src/modules/migrate/commands/versions/migrate.ts +++ b/packages/cli-module-migrate/src/commands/versions/migrate.ts @@ -21,7 +21,7 @@ import { readJson, writeJson } from 'fs-extra'; import { minimatch } from 'minimatch'; import { runYarnInstall } from '../../lib/utils'; import replace from 'replace-in-file'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; declare module 'replace-in-file' { export default function (config: { @@ -39,7 +39,7 @@ declare module 'replace-in-file' { >; } -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { pattern, skipCodeChanges }, } = cli( diff --git a/packages/cli/src/modules/migrate/index.ts b/packages/cli-module-migrate/src/index.ts similarity index 94% rename from packages/cli/src/modules/migrate/index.ts rename to packages/cli-module-migrate/src/index.ts index c731479a51..e5d89383b8 100644 --- a/packages/cli/src/modules/migrate/index.ts +++ b/packages/cli-module-migrate/src/index.ts @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; -export default createCliPlugin({ - pluginId: 'migrate', +export default createCliModule({ + packageJson, init: async reg => { reg.addCommand({ path: ['versions:migrate'], diff --git a/packages/cli/src/modules/migrate/lib/utils.ts b/packages/cli-module-migrate/src/lib/utils.ts similarity index 100% rename from packages/cli/src/modules/migrate/lib/utils.ts rename to packages/cli-module-migrate/src/lib/utils.ts diff --git a/packages/cli/src/modules/migrate/lib/versioning/packages.test.ts b/packages/cli-module-migrate/src/lib/versioning/packages.test.ts similarity index 100% rename from packages/cli/src/modules/migrate/lib/versioning/packages.test.ts rename to packages/cli-module-migrate/src/lib/versioning/packages.test.ts diff --git a/packages/cli/src/modules/migrate/lib/versioning/packages.ts b/packages/cli-module-migrate/src/lib/versioning/packages.ts similarity index 100% rename from packages/cli/src/modules/migrate/lib/versioning/packages.ts rename to packages/cli-module-migrate/src/lib/versioning/packages.ts diff --git a/packages/cli/src/modules/migrate/lib/versioning/yarn.ts b/packages/cli-module-migrate/src/lib/versioning/yarn.ts similarity index 100% rename from packages/cli/src/modules/migrate/lib/versioning/yarn.ts rename to packages/cli-module-migrate/src/lib/versioning/yarn.ts diff --git a/packages/cli-module-new/.eslintrc.js b/packages/cli-module-new/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-new/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-new/README.md b/packages/cli-module-new/README.md new file mode 100644 index 0000000000..88855e856a --- /dev/null +++ b/packages/cli-module-new/README.md @@ -0,0 +1,14 @@ +# @backstage/cli-module-new + +CLI module that provides the `new` command for the Backstage CLI, offering an interactive guide to scaffold new plugins, packages, and modules in your Backstage app. + +## Commands + +| Command | Description | +| :------ | :-------------------------------------------------------------- | +| `new` | Open up an interactive guide to creating new things in your app | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-new/bin/backstage-cli-module-new b/packages/cli-module-new/bin/backstage-cli-module-new new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-new/bin/backstage-cli-module-new @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-new/catalog-info.yaml b/packages/cli-module-new/catalog-info.yaml new file mode 100644 index 0000000000..997f06d629 --- /dev/null +++ b/packages/cli-module-new/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-new + title: '@backstage/cli-module-new' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-new/cli-report.md b/packages/cli-module-new/cli-report.md new file mode 100644 index 0000000000..a2b4768148 --- /dev/null +++ b/packages/cli-module-new/cli-report.md @@ -0,0 +1,34 @@ +## CLI Report file for "@backstage/cli-module-new" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-new` + +``` +Usage: @backstage/cli-module-new [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + new +``` + +### `backstage-cli-module-new new` + +``` +Usage: @backstage/cli-module-new new + +Options: + --base-version + --license + --npm-registry + --option + --private + --scope + --select + --skip-install + -h, --help +``` diff --git a/packages/cli-module-new/package.json b/packages/cli-module-new/package.json new file mode 100644 index 0000000000..9ca1664f7a --- /dev/null +++ b/packages/cli-module-new/package.json @@ -0,0 +1,62 @@ +{ + "name": "@backstage/cli-module-new", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-new" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "@backstage/errors": "workspace:^", + "chalk": "^4.0.0", + "cleye": "^2.3.0", + "fs-extra": "^11.2.0", + "handlebars": "^4.7.3", + "inquirer": "^8.2.0", + "lodash": "^4.17.21", + "ora": "^5.3.0", + "recursive-readdir": "^2.2.2", + "semver": "^7.5.3", + "yaml": "^2.0.0", + "zod": "^3.25.76", + "zod-validation-error": "^4.0.2" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@types/fs-extra": "^11.0.0", + "@types/inquirer": "^8.1.3", + "@types/lodash": "^4.14.151", + "@types/recursive-readdir": "^2.2.0" + }, + "bin": "bin/backstage-cli-module-new" +} diff --git a/packages/cli-module-new/report.api.md b/packages/cli-module-new/report.api.md new file mode 100644 index 0000000000..0a9924e731 --- /dev/null +++ b/packages/cli-module-new/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-new" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/new/commands/new.test.ts b/packages/cli-module-new/src/commands/new.test.ts similarity index 91% rename from packages/cli/src/modules/new/commands/new.test.ts rename to packages/cli-module-new/src/commands/new.test.ts index 0da42e7019..54663ab60d 100644 --- a/packages/cli/src/modules/new/commands/new.test.ts +++ b/packages/cli-module-new/src/commands/new.test.ts @@ -16,7 +16,7 @@ import { createNewPackage } from '../lib/createNewPackage'; import { default as newCommand } from './new'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; jest.mock('../lib/createNewPackage'); @@ -39,9 +39,9 @@ describe.each([ if (scope) { args.push('--scope', scope); } - const context: CommandContext = { + const context: CliCommandContext = { args, - info: { usage: 'backstage-cli new', description: 'test' }, + info: { usage: 'backstage-cli new', name: 'new' }, }; await newCommand(context); expect(createNewPackage).toHaveBeenCalledWith( diff --git a/packages/cli/src/modules/new/commands/new.ts b/packages/cli-module-new/src/commands/new.ts similarity index 96% rename from packages/cli/src/modules/new/commands/new.ts rename to packages/cli-module-new/src/commands/new.ts index 3769e19269..3d7fcdf4e3 100644 --- a/packages/cli/src/modules/new/commands/new.ts +++ b/packages/cli-module-new/src/commands/new.ts @@ -16,9 +16,9 @@ import { cli } from 'cleye'; import { createNewPackage } from '../lib/createNewPackage'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { for (const flag of ['skipInstall', 'npmRegistry', 'baseVersion']) { if (args.some(a => a === `--${flag}` || a.startsWith(`--${flag}=`))) { process.stderr.write( diff --git a/packages/cli/src/modules/new/index.ts b/packages/cli-module-new/src/index.ts similarity index 91% rename from packages/cli/src/modules/new/index.ts rename to packages/cli-module-new/src/index.ts index 5b2a6562ea..c9bc322083 100644 --- a/packages/cli/src/modules/new/index.ts +++ b/packages/cli-module-new/src/index.ts @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliModule } from '@backstage/cli-node'; import { NotImplementedError } from '@backstage/errors'; +import packageJson from '../package.json'; -export default createCliPlugin({ - pluginId: 'new', +export default createCliModule({ + packageJson, init: async reg => { reg.addCommand({ path: ['new'], diff --git a/packages/cli/src/modules/new/lib/codeowners/codeowners.test.ts b/packages/cli-module-new/src/lib/codeowners/codeowners.test.ts similarity index 100% rename from packages/cli/src/modules/new/lib/codeowners/codeowners.test.ts rename to packages/cli-module-new/src/lib/codeowners/codeowners.test.ts diff --git a/packages/cli/src/modules/new/lib/codeowners/codeowners.ts b/packages/cli-module-new/src/lib/codeowners/codeowners.ts similarity index 100% rename from packages/cli/src/modules/new/lib/codeowners/codeowners.ts rename to packages/cli-module-new/src/lib/codeowners/codeowners.ts diff --git a/packages/cli/src/modules/new/lib/codeowners/index.ts b/packages/cli-module-new/src/lib/codeowners/index.ts similarity index 100% rename from packages/cli/src/modules/new/lib/codeowners/index.ts rename to packages/cli-module-new/src/lib/codeowners/index.ts diff --git a/packages/cli/src/modules/new/lib/createNewPackage.ts b/packages/cli-module-new/src/lib/createNewPackage.ts similarity index 100% rename from packages/cli/src/modules/new/lib/createNewPackage.ts rename to packages/cli-module-new/src/lib/createNewPackage.ts diff --git a/packages/cli/src/modules/new/lib/defaultTemplates.ts b/packages/cli-module-new/src/lib/defaultTemplates.ts similarity index 100% rename from packages/cli/src/modules/new/lib/defaultTemplates.ts rename to packages/cli-module-new/src/lib/defaultTemplates.ts diff --git a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts b/packages/cli-module-new/src/lib/execution/PortableTemplater.ts similarity index 100% rename from packages/cli/src/modules/new/lib/execution/PortableTemplater.ts rename to packages/cli-module-new/src/lib/execution/PortableTemplater.ts diff --git a/packages/cli/src/modules/new/lib/execution/executePortableTemplate.ts b/packages/cli-module-new/src/lib/execution/executePortableTemplate.ts similarity index 100% rename from packages/cli/src/modules/new/lib/execution/executePortableTemplate.ts rename to packages/cli-module-new/src/lib/execution/executePortableTemplate.ts diff --git a/packages/cli/src/modules/new/lib/execution/index.ts b/packages/cli-module-new/src/lib/execution/index.ts similarity index 100% rename from packages/cli/src/modules/new/lib/execution/index.ts rename to packages/cli-module-new/src/lib/execution/index.ts diff --git a/packages/cli/src/modules/new/lib/execution/installNewPackage.ts b/packages/cli-module-new/src/lib/execution/installNewPackage.ts similarity index 100% rename from packages/cli/src/modules/new/lib/execution/installNewPackage.ts rename to packages/cli-module-new/src/lib/execution/installNewPackage.ts diff --git a/packages/cli/src/modules/new/lib/execution/writeTemplateContents.test.ts b/packages/cli-module-new/src/lib/execution/writeTemplateContents.test.ts similarity index 100% rename from packages/cli/src/modules/new/lib/execution/writeTemplateContents.test.ts rename to packages/cli-module-new/src/lib/execution/writeTemplateContents.test.ts diff --git a/packages/cli/src/modules/new/lib/execution/writeTemplateContents.ts b/packages/cli-module-new/src/lib/execution/writeTemplateContents.ts similarity index 100% rename from packages/cli/src/modules/new/lib/execution/writeTemplateContents.ts rename to packages/cli-module-new/src/lib/execution/writeTemplateContents.ts diff --git a/packages/cli/src/modules/new/lib/preparation/collectPortableTemplateInput.test.ts b/packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.test.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/collectPortableTemplateInput.test.ts rename to packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.test.ts diff --git a/packages/cli/src/modules/new/lib/preparation/collectPortableTemplateInput.ts b/packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/collectPortableTemplateInput.ts rename to packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.ts diff --git a/packages/cli/src/modules/new/lib/preparation/index.ts b/packages/cli-module-new/src/lib/preparation/index.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/index.ts rename to packages/cli-module-new/src/lib/preparation/index.ts diff --git a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplate.test.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplate.test.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/loadPortableTemplate.test.ts rename to packages/cli-module-new/src/lib/preparation/loadPortableTemplate.test.ts diff --git a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplate.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplate.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/loadPortableTemplate.ts rename to packages/cli-module-new/src/lib/preparation/loadPortableTemplate.ts diff --git a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.test.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.test.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.test.ts rename to packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.test.ts diff --git a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.ts rename to packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts diff --git a/packages/cli/src/modules/new/lib/preparation/resolvePackageParams.test.ts b/packages/cli-module-new/src/lib/preparation/resolvePackageParams.test.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/resolvePackageParams.test.ts rename to packages/cli-module-new/src/lib/preparation/resolvePackageParams.test.ts diff --git a/packages/cli/src/modules/new/lib/preparation/resolvePackageParams.ts b/packages/cli-module-new/src/lib/preparation/resolvePackageParams.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/resolvePackageParams.ts rename to packages/cli-module-new/src/lib/preparation/resolvePackageParams.ts diff --git a/packages/cli/src/modules/new/lib/preparation/selectTemplateInteractively.test.ts b/packages/cli-module-new/src/lib/preparation/selectTemplateInteractively.test.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/selectTemplateInteractively.test.ts rename to packages/cli-module-new/src/lib/preparation/selectTemplateInteractively.test.ts diff --git a/packages/cli/src/modules/new/lib/preparation/selectTemplateInteractively.ts b/packages/cli-module-new/src/lib/preparation/selectTemplateInteractively.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/selectTemplateInteractively.ts rename to packages/cli-module-new/src/lib/preparation/selectTemplateInteractively.ts diff --git a/packages/cli/src/modules/new/lib/tasks.ts b/packages/cli-module-new/src/lib/tasks.ts similarity index 100% rename from packages/cli/src/modules/new/lib/tasks.ts rename to packages/cli-module-new/src/lib/tasks.ts diff --git a/packages/cli/src/modules/new/lib/types.ts b/packages/cli-module-new/src/lib/types.ts similarity index 100% rename from packages/cli/src/modules/new/lib/types.ts rename to packages/cli-module-new/src/lib/types.ts diff --git a/packages/cli/src/modules/new/lib/version.test.ts b/packages/cli-module-new/src/lib/version.test.ts similarity index 100% rename from packages/cli/src/modules/new/lib/version.test.ts rename to packages/cli-module-new/src/lib/version.test.ts diff --git a/packages/cli/src/modules/new/lib/version.ts b/packages/cli-module-new/src/lib/version.ts similarity index 65% rename from packages/cli/src/modules/new/lib/version.ts rename to packages/cli-module-new/src/lib/version.ts index 0849e8a9e8..fd65135f4c 100644 --- a/packages/cli/src/modules/new/lib/version.ts +++ b/packages/cli-module-new/src/lib/version.ts @@ -30,28 +30,29 @@ This does not create an actual dependency on these packages and does not bring i Rollup will extract the value of the version field in each package at build time without leaving any imports in place. */ -import { version as backendPluginApi } from '../../../../../../packages/backend-plugin-api/package.json'; -import { version as backendTestUtils } from '../../../../../../packages/backend-test-utils/package.json'; -import { version as catalogClient } from '../../../../../../packages/catalog-client/package.json'; -import { version as cli } from '../../../../../../packages/cli/package.json'; -import { version as config } from '../../../../../../packages/config/package.json'; -import { version as coreAppApi } from '../../../../../../packages/core-app-api/package.json'; -import { version as coreComponents } from '../../../../../../packages/core-components/package.json'; -import { version as corePluginApi } from '../../../../../../packages/core-plugin-api/package.json'; -import { version as devUtils } from '../../../../../../packages/dev-utils/package.json'; -import { version as errors } from '../../../../../../packages/errors/package.json'; -import { version as frontendDefaults } from '../../../../../../packages/frontend-defaults/package.json'; -import { version as frontendPluginApi } from '../../../../../../packages/frontend-plugin-api/package.json'; -import { version as frontendTestUtils } from '../../../../../../packages/frontend-test-utils/package.json'; -import { version as testUtils } from '../../../../../../packages/test-utils/package.json'; -import { version as scaffolderNode } from '../../../../../../plugins/scaffolder-node/package.json'; -import { version as scaffolderNodeTestUtils } from '../../../../../../plugins/scaffolder-node-test-utils/package.json'; -import { version as authBackend } from '../../../../../../plugins/auth-backend/package.json'; -import { version as authBackendModuleGuestProvider } from '../../../../../../plugins/auth-backend-module-guest-provider/package.json'; -import { version as catalogNode } from '../../../../../../plugins/catalog-node/package.json'; -import { version as theme } from '../../../../../../packages/theme/package.json'; -import { version as types } from '../../../../../../packages/types/package.json'; -import { version as backendDefaults } from '../../../../../../packages/backend-defaults/package.json'; + +import { version as backendDefaults } from '../../../backend-defaults/package.json'; +import { version as backendPluginApi } from '../../../backend-plugin-api/package.json'; +import { version as backendTestUtils } from '../../../backend-test-utils/package.json'; +import { version as catalogClient } from '../../../catalog-client/package.json'; +import { version as cli } from '../../../cli/package.json'; +import { version as config } from '../../../config/package.json'; +import { version as coreAppApi } from '../../../core-app-api/package.json'; +import { version as coreComponents } from '../../../core-components/package.json'; +import { version as corePluginApi } from '../../../core-plugin-api/package.json'; +import { version as devUtils } from '../../../dev-utils/package.json'; +import { version as errors } from '../../../errors/package.json'; +import { version as frontendDefaults } from '../../../frontend-defaults/package.json'; +import { version as frontendPluginApi } from '../../../frontend-plugin-api/package.json'; +import { version as frontendTestUtils } from '../../../frontend-test-utils/package.json'; +import { version as testUtils } from '../../../test-utils/package.json'; +import { version as theme } from '../../../theme/package.json'; +import { version as types } from '../../../types/package.json'; +import { version as authBackend } from '../../../../plugins/auth-backend/package.json'; +import { version as authBackendModuleGuestProvider } from '../../../../plugins/auth-backend-module-guest-provider/package.json'; +import { version as catalogNode } from '../../../../plugins/catalog-node/package.json'; +import { version as scaffolderNode } from '../../../../plugins/scaffolder-node/package.json'; +import { version as scaffolderNodeTestUtils } from '../../../../plugins/scaffolder-node-test-utils/package.json'; export const packageVersions: Record = { '@backstage/backend-defaults': backendDefaults, diff --git a/packages/cli-module-test-jest/.eslintrc.js b/packages/cli-module-test-jest/.eslintrc.js new file mode 100644 index 0000000000..6f313a7a5e --- /dev/null +++ b/packages/cli-module-test-jest/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + ignorePatterns: ['config/**'], +}); diff --git a/packages/cli-module-test-jest/README.md b/packages/cli-module-test-jest/README.md new file mode 100644 index 0000000000..e56ffea2a7 --- /dev/null +++ b/packages/cli-module-test-jest/README.md @@ -0,0 +1,15 @@ +# @backstage/cli-module-test-jest + +CLI module that provides Jest-based testing commands for the Backstage CLI. + +## Commands + +| Command | Description | +| :------------- | :---------------------------------------------------------------- | +| `package test` | Run tests, forwarding arguments to Jest, defaulting to watch mode | +| `repo test` | Run tests, forwarding arguments to Jest, defaulting to watch mode | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-test-jest/bin/backstage-cli-module-test-jest b/packages/cli-module-test-jest/bin/backstage-cli-module-test-jest new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-test-jest/bin/backstage-cli-module-test-jest @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-test-jest/catalog-info.yaml b/packages/cli-module-test-jest/catalog-info.yaml new file mode 100644 index 0000000000..c928c628f5 --- /dev/null +++ b/packages/cli-module-test-jest/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-test-jest + title: '@backstage/cli-module-test-jest' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-test-jest/cli-report.md b/packages/cli-module-test-jest/cli-report.md new file mode 100644 index 0000000000..00523eb8cf --- /dev/null +++ b/packages/cli-module-test-jest/cli-report.md @@ -0,0 +1,170 @@ +## CLI Report file for "@backstage/cli-module-test-jest" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-test-jest` + +``` +Usage: @backstage/cli-module-test-jest [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + package [command] + repo [command] +``` + +### `backstage-cli-module-test-jest package` + +``` +Usage: @backstage/cli-module-test-jest package [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + test +``` + +### `backstage-cli-module-test-jest package test` + +``` +Usage: backstage-cli-module-test-jest [--config=] + +Options: + --all + --automock + --cache + --cacheDirectory + --changedFilesWithAncestor + --changedSince + --ci + --clearCache + --clearMocks + --collectCoverage + --collectCoverageFrom + --color + --colors + --coverage + --coverageDirectory + --coveragePathIgnorePatterns + --coverageProvider + --coverageReporters + --coverageThreshold + --debug + --detectLeaks + --detectOpenHandles + --errorOnDeprecated + --filter + --findRelatedTests + --forceExit + --globalSetup + --globalTeardown + --globals + --haste + --ignoreProjects + --injectGlobals + --json + --lastCommit + --listTests + --logHeapUsage + --maxConcurrency + --moduleDirectories + --moduleFileExtensions + --moduleNameMapper + --modulePathIgnorePatterns + --modulePaths + --noStackTrace + --notify + --notifyMode + --openHandlesTimeout + --outputFile + --passWithNoTests + --preset + --prettierPath + --projects + --randomize + --reporters + --resetMocks + --resetModules + --resolver + --restoreMocks + --rootDir + --roots + --runTestsByPath + --runner + --seed + --selectProjects + --setupFiles + --setupFilesAfterEnv + --shard + --showConfig + --showSeed + --silent + --skipFilter + --snapshotSerializers + --testEnvironment, --env + --testEnvironmentOptions + --testFailureExitCode + --testLocationInResults + --testMatch + --testPathIgnorePatterns + --testPathPatterns + --testRegex + --testResultsProcessor + --testRunner + --testSequencer + --testTimeout + --transform + --transformIgnorePatterns + --unmockedModulePathPatterns + --useStderr + --verbose + --version + --waitForUnhandledRejections + --watch + --watchAll + --watchPathIgnorePatterns + --watchman + --workerThreads + -b, --bail + -c, --config + -e, --expand + -f, --onlyFailures + -h, --help + -i, --runInBand + -o, --onlyChanged + -t, --testNamePattern + -u, --updateSnapshot + -w, --maxWorkers +``` + +### `backstage-cli-module-test-jest repo` + +``` +Usage: @backstage/cli-module-test-jest repo [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + test +``` + +### `backstage-cli-module-test-jest repo test` + +``` +Usage: @backstage/cli-module-test-jest repo test + +Options: + --jest-help + --since + --success-cache + --success-cache-dir + -h, --help +``` diff --git a/packages/cli-module-test-jest/config/getJestEnvironment.js b/packages/cli-module-test-jest/config/getJestEnvironment.js new file mode 100644 index 0000000000..aac2f79021 --- /dev/null +++ b/packages/cli-module-test-jest/config/getJestEnvironment.js @@ -0,0 +1,49 @@ +/* + * 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. + */ + +function getJestMajorVersion() { + const jestVersion = require('jest/package.json').version; + const majorVersion = parseInt(jestVersion.split('.')[0], 10); + return majorVersion; +} + +function getJestEnvironment() { + const majorVersion = getJestMajorVersion(); + + if (majorVersion >= 30) { + try { + require.resolve('@jest/environment-jsdom-abstract'); + require.resolve('jsdom'); + } catch { + throw new Error( + 'Jest 30+ requires @jest/environment-jsdom-abstract and jsdom. ' + + 'Please install them as dev dependencies.', + ); + } + return require.resolve('./jest-environment-jsdom'); + } + try { + require.resolve('jest-environment-jsdom'); + } catch { + throw new Error( + 'Jest 29 requires jest-environment-jsdom. ' + + 'Please install it as a dev dependency.', + ); + } + return require.resolve('jest-environment-jsdom'); +} + +module.exports = { getJestMajorVersion, getJestEnvironment }; diff --git a/packages/cli-module-test-jest/config/jest-environment-jsdom/index.js b/packages/cli-module-test-jest/config/jest-environment-jsdom/index.js new file mode 100644 index 0000000000..9d8b76eaf5 --- /dev/null +++ b/packages/cli-module-test-jest/config/jest-environment-jsdom/index.js @@ -0,0 +1,61 @@ +/* + * 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. + */ + +const JSDOMEnvironment = require('@jest/environment-jsdom-abstract').default; +const jsdom = require('jsdom'); + +/** + * A custom JSDOM environment that extends the abstract base and applies + * fixes for Web API globals that are missing or incorrectly implemented + * in JSDOM. + * + * Based on https://github.com/mswjs/jest-fixed-jsdom + */ +class FixedJSDOMEnvironment extends JSDOMEnvironment { + constructor(config, context) { + super(config, context, jsdom); + + // Fix Web API globals that JSDOM doesn't properly expose + this.global.TextDecoder = TextDecoder; + this.global.TextEncoder = TextEncoder; + this.global.TextDecoderStream = TextDecoderStream; + this.global.TextEncoderStream = TextEncoderStream; + this.global.ReadableStream = ReadableStream; + + this.global.Blob = Blob; + this.global.Headers = Headers; + this.global.FormData = FormData; + this.global.Request = Request; + this.global.Response = Response; + this.global.fetch = fetch; + this.global.AbortController = AbortController; + this.global.AbortSignal = AbortSignal; + this.global.structuredClone = structuredClone; + this.global.URL = URL; + this.global.URLSearchParams = URLSearchParams; + + this.global.BroadcastChannel = BroadcastChannel; + this.global.TransformStream = TransformStream; + this.global.WritableStream = WritableStream; + + // Needed to ensure `e instanceof Error` works as expected with errors thrown from + // any of the native APIs above. Without this, the JSDOM `Error` is what the test + // code will use for comparison with `e`, which fails the instanceof check. + this.global.Error = Error; + } +} + +module.exports = FixedJSDOMEnvironment; diff --git a/packages/cli-module-test-jest/config/jest.js b/packages/cli-module-test-jest/config/jest.js new file mode 100644 index 0000000000..8a88483d42 --- /dev/null +++ b/packages/cli-module-test-jest/config/jest.js @@ -0,0 +1,422 @@ +/* + * 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. + */ + +const fs = require('fs-extra'); +const path = require('node:path'); +const crypto = require('node:crypto'); +const glob = require('node:util').promisify(require('glob')); +const { version } = require('../package.json'); +const paths = require('@backstage/cli-common').findPaths(process.cwd()); +const { + getJestEnvironment, + getJestMajorVersion, +} = require('./getJestEnvironment'); + +const SRC_EXTS = ['ts', 'js', 'tsx', 'jsx', 'mts', 'cts', 'mjs', 'cjs']; + +const FRONTEND_ROLES = [ + 'frontend', + 'web-library', + 'common-library', + 'frontend-plugin', + 'frontend-plugin-module', +]; + +const NODE_ROLES = [ + 'backend', + 'cli', + 'cli-module', + 'node-library', + 'backend-plugin', + 'backend-plugin-module', +]; + +const envOptions = { + oldTests: Boolean(process.env.BACKSTAGE_OLD_TESTS), +}; + +try { + require.resolve('react-dom/client', { + paths: [paths.targetRoot], + }); + process.env.HAS_REACT_DOM_CLIENT = true; +} catch { + /* ignored */ +} + +/** + * A list of config keys that are valid for project-level config. + * Jest will complain if we forward any other root configuration to the projects. + * + * @type {Array} + */ +const projectConfigKeys = [ + 'automock', + 'cache', + 'cacheDirectory', + 'clearMocks', + 'collectCoverageFrom', + 'coverageDirectory', + 'coveragePathIgnorePatterns', + 'cwd', + 'dependencyExtractor', + 'detectLeaks', + 'detectOpenHandles', + 'displayName', + 'errorOnDeprecated', + 'extensionsToTreatAsEsm', + 'fakeTimers', + 'filter', + 'forceCoverageMatch', + 'globalSetup', + 'globalTeardown', + 'globals', + 'haste', + 'id', + 'injectGlobals', + 'moduleDirectories', + 'moduleFileExtensions', + 'moduleNameMapper', + 'modulePathIgnorePatterns', + 'modulePaths', + 'openHandlesTimeout', + 'preset', + 'prettierPath', + 'resetMocks', + 'resetModules', + 'resolver', + 'restoreMocks', + 'rootDir', + 'roots', + 'runner', + 'runtime', + 'sandboxInjectedGlobals', + 'setupFiles', + 'setupFilesAfterEnv', + 'skipFilter', + 'skipNodeResolution', + 'slowTestThreshold', + 'snapshotResolver', + 'snapshotSerializers', + 'snapshotFormat', + 'testEnvironment', + 'testEnvironmentOptions', + 'testMatch', + 'testLocationInResults', + 'testPathIgnorePatterns', + 'testRegex', + 'testRunner', + 'transform', + 'transformIgnorePatterns', + 'watchPathIgnorePatterns', + 'unmockedModulePathPatterns', + 'workerIdleMemoryLimit', +]; + +const transformIgnorePattern = [ + '@material-ui', + 'ajv', + 'core-js', + 'jest-.*', + 'jsdom', + 'knex', + 'react', + 'react-dom', + 'highlight\\.js', + 'prismjs', + 'json-schema', + 'react-use/lib', + 'typescript', +].join('|'); + +// Provides additional config that's based on the role of the target package +function getRoleConfig(role, pkgJson) { + // Only Node.js package roles support native ESM modules, frontend and common + // packages are always transpiled to CommonJS. + const moduleOpts = NODE_ROLES.includes(role) + ? { + module: { + ignoreDynamic: true, + exportInteropAnnotation: true, + }, + } + : undefined; + + const transform = { + '\\.(mjs|cjs|js)$': [ + require.resolve('./jestSwcTransform'), + { + ...moduleOpts, + jsc: { + parser: { + syntax: 'ecmascript', + }, + }, + }, + ], + '\\.jsx$': [ + require.resolve('./jestSwcTransform'), + { + jsc: { + parser: { + syntax: 'ecmascript', + jsx: true, + }, + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }, + ], + '\\.(mts|cts|ts)$': [ + require.resolve('./jestSwcTransform'), + { + ...moduleOpts, + jsc: { + parser: { + syntax: 'typescript', + }, + }, + }, + ], + '\\.tsx$': [ + require.resolve('./jestSwcTransform'), + { + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + }, + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }, + ], + '\\.(bmp|gif|jpg|jpeg|png|ico|webp|frag|xml|svg|eot|woff|woff2|ttf)$': + require.resolve('./jestFileTransform.js'), + '\\.(yaml)$': require.resolve('./jestYamlTransform'), + }; + if (FRONTEND_ROLES.includes(role)) { + return { + testEnvironment: getJestEnvironment(), + // The caching module loader is only used to speed up frontend tests, + // as it breaks real dynamic imports of ESM modules. + runtime: envOptions.oldTests + ? undefined + : require.resolve('./jestCachingModuleLoader'), + transform, + }; + } + return { + testEnvironment: require.resolve('jest-environment-node'), + moduleFileExtensions: [...SRC_EXTS, 'json', 'node'], + // Jest doesn't let us dynamically detect type=module per transformed file, + // so we have to assume that if the entry point is ESM, all TS files are + // ESM. + // + // This means you can't switch a package to type=module until all of its + // monorepo dependencies are also type=module or does not contain any .ts + // files. + extensionsToTreatAsEsm: + pkgJson.type === 'module' ? ['.ts', '.mts'] : ['.mts'], + transform, + }; +} + +async function getProjectConfig(targetPath, extraConfig, extraOptions) { + const configJsPath = path.resolve(targetPath, 'jest.config.js'); + const configTsPath = path.resolve(targetPath, 'jest.config.ts'); + // If the package has it's own jest config, we use that instead. + if (await fs.pathExists(configJsPath)) { + return require(configJsPath); + } else if (await fs.pathExists(configTsPath)) { + return require(configTsPath); + } + + // Jest config can be defined both in the root package.json and within each package. The root config + // gets forwarded to us through the `extraConfig` parameter, while the package config is read here. + // If they happen to be the same the keys will simply override each other. + // The merging of the configs is shallow, meaning e.g. all transforms are replaced if new ones are defined. + const pkgJson = await fs.readJson(path.resolve(targetPath, 'package.json')); + + const options = { + ...extraConfig, + rootDir: path.resolve(targetPath, 'src'), + moduleNameMapper: { + '\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'), + }, + + // A bit more opinionated + testMatch: [`**/*.test.{${SRC_EXTS.join(',')}}`], + + transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`], + ...getRoleConfig(pkgJson.backstage?.role, pkgJson), + }; + + options.setupFilesAfterEnv = options.setupFilesAfterEnv || []; + + if ( + extraOptions.rejectFrontendNetworkRequests && + FRONTEND_ROLES.includes(pkgJson.backstage?.role) + ) { + // By adding this first we ensure that it's possible to for example override + // fetch with a mock in a custom setup file + options.setupFilesAfterEnv.unshift( + require.resolve('./jestRejectNetworkRequests.js'), + ); + } + + if ( + options.testEnvironment === getJestEnvironment() && + getJestMajorVersion() < 30 // Only needed when not running the custom env for Jest 30+ + ) { + // FIXME https://github.com/jsdom/jsdom/issues/1724 + options.setupFilesAfterEnv.unshift(require.resolve('cross-fetch/polyfill')); + } + + // Use src/setupTests.* as the default location for configuring test env + for (const ext of SRC_EXTS) { + if (fs.existsSync(path.resolve(targetPath, `src/setupTests.${ext}`))) { + options.setupFilesAfterEnv.push(`/setupTests.${ext}`); + break; + } + } + + const config = Object.assign(options, pkgJson.jest); + + // The config id is a cache key that lets us share the jest cache across projects. + // If no explicit id was configured, generated one based on the configuration. + if (!config.id) { + const configHash = crypto + .createHash('sha256') + .update(version) + .update(Buffer.alloc(1)) + .update(JSON.stringify(config.transform).replaceAll(paths.targetRoot, '')) + .digest('hex'); + config.id = `backstage_cli_${configHash}`; + } + + return config; +} + +// This loads the root jest config, which in turn will either refer to a single +// configuration for the current package, or a collection of configurations for +// the target workspace packages +async function getRootConfig() { + const rootPkgJson = await fs.readJson( + paths.resolveTargetRoot('package.json'), + ); + + const baseCoverageConfig = { + coverageDirectory: paths.resolveTarget('coverage'), + coverageProvider: envOptions.oldTests ? 'v8' : 'babel', + collectCoverageFrom: ['**/*.{js,jsx,ts,tsx,mjs,cjs}', '!**/*.d.ts'], + }; + + const { rejectFrontendNetworkRequests, ...rootOptions } = + rootPkgJson.jest ?? {}; + const extraRootOptions = { + rejectFrontendNetworkRequests, + }; + + const ws = rootPkgJson.workspaces; + const workspacePatterns = Array.isArray(ws) ? ws : ws?.packages; + + // Check if we're running within a specific monorepo package. In that case just get the single project config. + if (!workspacePatterns || paths.targetRoot !== paths.targetDir) { + return getProjectConfig( + paths.targetDir, + { + ...baseCoverageConfig, + ...rootOptions, + }, + extraRootOptions, + ); + } + + const globalRootConfig = { ...baseCoverageConfig }; + const globalProjectConfig = {}; + + for (const [key, value] of Object.entries(rootOptions)) { + if (projectConfigKeys.includes(key)) { + globalProjectConfig[key] = value; + } else { + globalRootConfig[key] = value; + } + } + + // If the target package is a workspace root, we find all packages in the + // workspace and load those in as separate jest projects instead. + const projectPaths = await Promise.all( + workspacePatterns.map(pattern => + glob(path.join(paths.targetRoot, pattern)), + ), + ).then(_ => _.flat()); + + let projects = await Promise.all( + projectPaths.flat().map(async projectPath => { + const packagePath = path.resolve(projectPath, 'package.json'); + if (!(await fs.pathExists(packagePath))) { + return undefined; + } + + // We check for the presence of "backstage-cli test" in the package test + // script to determine whether a given package should be tested + const packageData = await fs.readJson(packagePath); + const testScript = packageData.scripts && packageData.scripts.test; + const isSupportedTestScript = + testScript?.includes('backstage-cli test') || + testScript?.includes('backstage-cli package test'); + if (testScript && isSupportedTestScript) { + return await getProjectConfig( + projectPath, + { + ...globalProjectConfig, + displayName: packageData.name, + }, + extraRootOptions, + ); + } + + return undefined; + }), + ).then(cs => cs.filter(Boolean)); + + const cache = global.__backstageCli_jestSuccessCache; + if (cache) { + projects = await cache.filterConfigs(projects, globalRootConfig); + } + const watchProjectFilter = global.__backstageCli_watchProjectFilter; + if (watchProjectFilter) { + projects = await watchProjectFilter.filter(projects); + } + + return { + rootDir: paths.targetRoot, + projects, + testResultsProcessor: cache + ? require.resolve('./jestCacheResultProcessor.cjs') + : undefined, + ...globalRootConfig, + }; +} + +module.exports = getRootConfig(); diff --git a/packages/cli-module-test-jest/config/jestCacheResultProcessor.cjs b/packages/cli-module-test-jest/config/jestCacheResultProcessor.cjs new file mode 100644 index 0000000000..4af401d8b5 --- /dev/null +++ b/packages/cli-module-test-jest/config/jestCacheResultProcessor.cjs @@ -0,0 +1,23 @@ +/* + * 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. + */ + +module.exports = async results => { + const cache = global.__backstageCli_jestSuccessCache; + if (cache) { + await cache.reportResults(results); + } + return results; +}; diff --git a/packages/cli-module-test-jest/config/jestCachingModuleLoader.js b/packages/cli-module-test-jest/config/jestCachingModuleLoader.js new file mode 100644 index 0000000000..5652ff67e7 --- /dev/null +++ b/packages/cli-module-test-jest/config/jestCachingModuleLoader.js @@ -0,0 +1,35 @@ +/* + * 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. + */ + +// 'jest-runtime' is included with jest and should be kept in sync with the installed jest version +// eslint-disable-next-line @backstage/no-undeclared-imports +const { default: JestRuntime } = require('jest-runtime'); + +module.exports = class CachingJestRuntime extends JestRuntime { + constructor(config, ...restArgs) { + super(config, ...restArgs); + this.allowLoadAsEsm = config.extensionsToTreatAsEsm.includes('.mts'); + } + + // Unfortunately we need to use this unstable API to make sure that .js files + // are only loaded as modules where ESM is supported, i.e. Node.js packages. + unstable_shouldLoadAsEsm(path, ...restArgs) { + if (!this.allowLoadAsEsm) { + return false; + } + return super.unstable_shouldLoadAsEsm(path, ...restArgs); + } +}; diff --git a/packages/cli-module-test-jest/config/jestFileTransform.js b/packages/cli-module-test-jest/config/jestFileTransform.js new file mode 100644 index 0000000000..ad1c37a4f5 --- /dev/null +++ b/packages/cli-module-test-jest/config/jestFileTransform.js @@ -0,0 +1,44 @@ +/* + * 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. + */ + +const path = require('node:path'); + +module.exports = { + process(src, filename) { + const assetFilename = JSON.stringify(path.basename(filename)); + + if (filename.match(/\.icon\.svg$/)) { + return { + code: `const React = require('react'); + const SvgIcon = require('@material-ui/core/SvgIcon').default; + module.exports = { + __esModule: true, + default: props => React.createElement(SvgIcon, props, { + $$typeof: Symbol.for('react.element'), + type: 'svg', + ref: ref, + key: null, + props: Object.assign({}, props, { + children: ${assetFilename} + }) + }) + };`, + }; + } + + return { code: `module.exports = ${assetFilename};` }; + }, +}; diff --git a/packages/cli-module-test-jest/config/jestRejectNetworkRequests.js b/packages/cli-module-test-jest/config/jestRejectNetworkRequests.js new file mode 100644 index 0000000000..f6f189a60c --- /dev/null +++ b/packages/cli-module-test-jest/config/jestRejectNetworkRequests.js @@ -0,0 +1,70 @@ +/* + * 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. + */ + +const http = require('node:http'); +const https = require('node:https'); + +const errorMessage = 'Network requests are not allowed in tests'; + +const origHttpAgent = http.globalAgent; +const origHttpsAgent = https.globalAgent; +const origFetch = global.fetch; +const origXMLHttpRequest = global.XMLHttpRequest; + +http.globalAgent = new http.Agent({ + lookup() { + throw new Error(errorMessage); + }, +}); + +https.globalAgent = new https.Agent({ + lookup() { + throw new Error(errorMessage); + }, +}); + +const BLOCKING_FETCH_SYMBOL = Symbol.for( + 'backstage.jestRejectNetworkRequests.blockingFetch', +); + +if (global.fetch) { + const blockingFetch = async (input, init) => { + // If global.fetch still has our marker, block the request + if (global.fetch[BLOCKING_FETCH_SYMBOL]) { + throw new Error(errorMessage); + } + // MSW (or something else) wrapped us - pass through + return origFetch(input, init); + }; + blockingFetch[BLOCKING_FETCH_SYMBOL] = true; + global.fetch = blockingFetch; +} + +if (global.XMLHttpRequest) { + global.XMLHttpRequest = class { + constructor() { + throw new Error(errorMessage); + } + }; +} + +// Reset overrides after each suite to make sure we don't pollute the test environment +afterAll(() => { + http.globalAgent = origHttpAgent; + https.globalAgent = origHttpsAgent; + global.fetch = origFetch; + global.XMLHttpRequest = origXMLHttpRequest; +}); diff --git a/packages/cli-module-test-jest/config/jestSucraseTransform.js b/packages/cli-module-test-jest/config/jestSucraseTransform.js new file mode 100644 index 0000000000..621b9ee297 --- /dev/null +++ b/packages/cli-module-test-jest/config/jestSucraseTransform.js @@ -0,0 +1,87 @@ +/* + * 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. + */ + +const { createHash } = require('node:crypto'); +const { transform } = require('sucrase'); +const sucrasePkg = require('sucrase/package.json'); + +const ESM_REGEX = /\b(?:import|export)\b/; + +function createTransformer(config) { + const process = (source, filePath) => { + let transforms; + + if (filePath.endsWith('.esm.js')) { + transforms = ['imports']; + } else if (filePath.endsWith('.js')) { + // This is a very rough filter to avoid transforming things that we quickly + // can be sure are definitely not ESM modules. + if (ESM_REGEX.test(source)) { + transforms = ['imports', 'jsx']; // JSX within .js is currently allowed + } + } else if (filePath.endsWith('.jsx')) { + transforms = ['jsx', 'imports']; + } else if (filePath.endsWith('.ts')) { + transforms = ['typescript', 'imports']; + } else if (filePath.endsWith('.tsx')) { + transforms = ['typescript', 'jsx', 'imports']; + } + + // Only apply the jest transform to the test files themselves + if (transforms && filePath.includes('.test.')) { + transforms.push('jest'); + } + + if (transforms) { + const { code, sourceMap: map } = transform(source, { + transforms, + filePath, + disableESTransforms: true, + sourceMapOptions: { + compiledFilename: filePath, + }, + }); + if (config.enableSourceMaps) { + const b64 = Buffer.from(JSON.stringify(map), 'utf8').toString('base64'); + const suffix = `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${b64}`; + // Include both inline and object source maps, as inline source maps are + // needed for support of some editor integrations. + return { code: `${code}\n${suffix}`, map }; + } + // We only return the `map` result if source maps are enabled, as they + // have a negative impact on the coverage accuracy. + return { code }; + } + + return { code: source }; + }; + + const getCacheKey = sourceText => { + return createHash('sha256') + .update(sourceText) + .update(Buffer.alloc(1)) + .update(sucrasePkg.version) + .update(Buffer.alloc(1)) + .update(JSON.stringify(config)) + .update(Buffer.alloc(1)) + .update('1') // increment whenever the transform logic in this file changes + .digest('hex'); + }; + + return { process, getCacheKey }; +} + +module.exports = { createTransformer }; diff --git a/packages/cli-module-test-jest/config/jestSwcTransform.js b/packages/cli-module-test-jest/config/jestSwcTransform.js new file mode 100644 index 0000000000..4183349554 --- /dev/null +++ b/packages/cli-module-test-jest/config/jestSwcTransform.js @@ -0,0 +1,44 @@ +/* + * 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. + */ +const { createTransformer: createSwcTransformer } = require('@swc/jest'); + +const ESM_REGEX = /\b(?:import|export)\b/; + +function createTransformer(config) { + const swcTransformer = createSwcTransformer({ + inputSourceMap: false, + ...config, + }); + const process = (source, filePath, jestOptions) => { + // Skip transformation of .js files without ESM syntax, we never transform from CJS to ESM + if (filePath.endsWith('.js') && !ESM_REGEX.test(source)) { + return { code: source }; + } + + // Skip transformation of .mjs files, they should only be used if ESM support is available + if (jestOptions.supportsStaticESM && filePath.endsWith('.mjs')) { + return { code: source }; + } + + return swcTransformer.process(source, filePath, jestOptions); + }; + + const getCacheKey = swcTransformer.getCacheKey; + + return { process, getCacheKey }; +} + +module.exports = { createTransformer }; diff --git a/packages/cli-module-test-jest/config/jestYamlTransform.js b/packages/cli-module-test-jest/config/jestYamlTransform.js new file mode 100644 index 0000000000..7a05274add --- /dev/null +++ b/packages/cli-module-test-jest/config/jestYamlTransform.js @@ -0,0 +1,40 @@ +/* + * 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. + */ + +const yaml = require('yaml'); +const crypto = require('node:crypto'); + +function createTransformer(config) { + const process = source => { + const json = JSON.stringify(yaml.parse(source), null, 2); + return { code: `module.exports = ${json}`, map: null }; + }; + + const getCacheKey = sourceText => { + return crypto + .createHash('sha256') + .update(sourceText) + .update(Buffer.alloc(1)) + .update(JSON.stringify(config)) + .update(Buffer.alloc(1)) + .update('1') // increment whenever the transform logic in this file changes + .digest('hex'); + }; + + return { process, getCacheKey }; +} + +module.exports = { createTransformer }; diff --git a/packages/cli-module-test-jest/package.json b/packages/cli-module-test-jest/package.json new file mode 100644 index 0000000000..6d7811f484 --- /dev/null +++ b/packages/cli-module-test-jest/package.json @@ -0,0 +1,69 @@ +{ + "name": "@backstage/cli-module-test-jest", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-test-jest" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin", + "config" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "@swc/core": "^1.15.6", + "@swc/jest": "^0.2.39", + "cleye": "^2.3.0", + "cross-fetch": "^4.0.0", + "fs-extra": "^11.2.0", + "glob": "^7.1.7", + "jest-css-modules": "^2.1.0", + "sucrase": "^3.20.2", + "yargs": "^16.2.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "peerDependencies": { + "@jest/environment-jsdom-abstract": "^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-environment-jsdom": "*", + "jsdom": "^27.1.0" + }, + "peerDependenciesMeta": { + "@jest/environment-jsdom-abstract": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "jsdom": { + "optional": true + } + }, + "bin": "bin/backstage-cli-module-test-jest" +} diff --git a/packages/cli-module-test-jest/report.api.md b/packages/cli-module-test-jest/report.api.md new file mode 100644 index 0000000000..816df9dcf1 --- /dev/null +++ b/packages/cli-module-test-jest/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-test-jest" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/test/commands/package/test.ts b/packages/cli-module-test-jest/src/commands/package/test.ts similarity index 93% rename from packages/cli/src/modules/test/commands/package/test.ts rename to packages/cli-module-test-jest/src/commands/package/test.ts index 687b66dbfd..b260601f8e 100644 --- a/packages/cli/src/modules/test/commands/package/test.ts +++ b/packages/cli-module-test-jest/src/commands/package/test.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { runCheck, findOwnPaths } from '@backstage/cli-common'; -import type { CommandContext } from '../../../../wiring/types'; +import { findOwnPaths, runCheck } from '@backstage/cli-common'; +import type { CliCommandContext } from '@backstage/cli-node'; function includesAnyOf(hayStack: string[], ...needles: string[]) { for (const needle of needles) { @@ -26,7 +26,7 @@ function includesAnyOf(hayStack: string[], ...needles: string[]) { return false; } -export default async ({ args }: CommandContext) => { +export default async ({ args }: CliCommandContext) => { // Only include our config if caller isn't passing their own config if (!includesAnyOf(args, '-c', '--config')) { /* eslint-disable-next-line no-restricted-syntax */ @@ -96,5 +96,6 @@ export default async ({ args }: CommandContext) => { process.exit(1); } + // eslint-disable-next-line @backstage/no-undeclared-imports await require('jest').run(args); }; diff --git a/packages/cli/src/modules/test/commands/repo/test.test.ts b/packages/cli-module-test-jest/src/commands/repo/test.test.ts similarity index 100% rename from packages/cli/src/modules/test/commands/repo/test.test.ts rename to packages/cli-module-test-jest/src/commands/repo/test.test.ts diff --git a/packages/cli/src/modules/test/commands/repo/test.ts b/packages/cli-module-test-jest/src/commands/repo/test.ts similarity index 99% rename from packages/cli/src/modules/test/commands/repo/test.ts rename to packages/cli-module-test-jest/src/commands/repo/test.ts index 1c39415192..806d88965b 100644 --- a/packages/cli/src/modules/test/commands/repo/test.ts +++ b/packages/cli-module-test-jest/src/commands/repo/test.ts @@ -25,13 +25,13 @@ import { relative as relativePath } from 'node:path'; import { Lockfile, PackageGraph, SuccessCache } from '@backstage/cli-node'; import { + findOwnPaths, runCheck, runOutput, targetPaths, - findOwnPaths, isChildPath, } from '@backstage/cli-common'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; type JestProject = { displayName: string; @@ -131,7 +131,7 @@ export function createFlagFinder(args: string[]) { }; } -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const testGlobal = global as TestGlobal; for (const flag of ['successCache', 'successCacheDir', 'jestHelp']) { diff --git a/packages/cli/src/modules/test/index.ts b/packages/cli-module-test-jest/src/index.ts similarity index 88% rename from packages/cli/src/modules/test/index.ts rename to packages/cli-module-test-jest/src/index.ts index 627648332b..b2060fa89e 100644 --- a/packages/cli/src/modules/test/index.ts +++ b/packages/cli-module-test-jest/src/index.ts @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; -export default createCliPlugin({ - pluginId: 'test', +export default createCliModule({ + packageJson, init: async reg => { reg.addCommand({ path: ['repo', 'test'], diff --git a/packages/cli-module-translations/.eslintrc.js b/packages/cli-module-translations/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-translations/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-translations/README.md b/packages/cli-module-translations/README.md new file mode 100644 index 0000000000..1f415ca975 --- /dev/null +++ b/packages/cli-module-translations/README.md @@ -0,0 +1,15 @@ +# @backstage/cli-module-translations + +CLI module that provides translation management commands for the Backstage CLI. + +## Commands + +| Command | Description | +| :-------------------- | :------------------------------------------------------------------------------------ | +| `translations export` | Export translation messages from an app and all of its frontend plugins to JSON files | +| `translations import` | Generate translation resource wiring from translated JSON files | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-translations/bin/backstage-cli-module-translations b/packages/cli-module-translations/bin/backstage-cli-module-translations new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-translations/bin/backstage-cli-module-translations @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-translations/catalog-info.yaml b/packages/cli-module-translations/catalog-info.yaml new file mode 100644 index 0000000000..a2f21fc649 --- /dev/null +++ b/packages/cli-module-translations/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-translations + title: '@backstage/cli-module-translations' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-translations/cli-report.md b/packages/cli-module-translations/cli-report.md new file mode 100644 index 0000000000..bf2ff28001 --- /dev/null +++ b/packages/cli-module-translations/cli-report.md @@ -0,0 +1,53 @@ +## CLI Report file for "@backstage/cli-module-translations" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-translations` + +``` +Usage: @backstage/cli-module-translations [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + translations [command] +``` + +### `backstage-cli-module-translations translations` + +``` +Usage: @backstage/cli-module-translations translations [options] [command] [command] + +Options: + -h, --help + +Commands: + export + help [command] + import +``` + +### `backstage-cli-module-translations translations export` + +``` +Usage: @backstage/cli-module-translations translations export + +Options: + --output + --pattern + -h, --help +``` + +### `backstage-cli-module-translations translations import` + +``` +Usage: @backstage/cli-module-translations translations import + +Options: + --input + --output + -h, --help +``` diff --git a/packages/cli-module-translations/package.json b/packages/cli-module-translations/package.json new file mode 100644 index 0000000000..86ef2b7ca4 --- /dev/null +++ b/packages/cli-module-translations/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/cli-module-translations", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-translations" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "cleye": "^2.3.0", + "fs-extra": "^11.2.0", + "ts-morph": "^24.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^11.0.0" + }, + "bin": "bin/backstage-cli-module-translations" +} diff --git a/packages/cli-module-translations/report.api.md b/packages/cli-module-translations/report.api.md new file mode 100644 index 0000000000..5e5864bea5 --- /dev/null +++ b/packages/cli-module-translations/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-translations" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/translations/commands/export.ts b/packages/cli-module-translations/src/commands/export.ts similarity index 97% rename from packages/cli/src/modules/translations/commands/export.ts rename to packages/cli-module-translations/src/commands/export.ts index b6471f10f4..a1d2eb2dae 100644 --- a/packages/cli/src/modules/translations/commands/export.ts +++ b/packages/cli-module-translations/src/commands/export.ts @@ -33,9 +33,9 @@ import { formatMessagePath, validatePattern, } from '../lib/messageFilePath'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { output, pattern }, } = cli( diff --git a/packages/cli/src/modules/translations/commands/import.ts b/packages/cli-module-translations/src/commands/import.ts similarity index 98% rename from packages/cli/src/modules/translations/commands/import.ts rename to packages/cli-module-translations/src/commands/import.ts index 155724dd8c..35a071c4fb 100644 --- a/packages/cli/src/modules/translations/commands/import.ts +++ b/packages/cli-module-translations/src/commands/import.ts @@ -28,7 +28,7 @@ import { createMessagePathParser, formatMessagePath, } from '../lib/messageFilePath'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; interface ManifestRefEntry { package: string; @@ -41,7 +41,7 @@ interface Manifest { refs: Record; } -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { input, output }, } = cli( diff --git a/packages/cli/src/modules/translations/index.ts b/packages/cli-module-translations/src/index.ts similarity index 88% rename from packages/cli/src/modules/translations/index.ts rename to packages/cli-module-translations/src/index.ts index 43263831d5..e075dc61ae 100644 --- a/packages/cli/src/modules/translations/index.ts +++ b/packages/cli-module-translations/src/index.ts @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; -export default createCliPlugin({ - pluginId: 'translations', +export default createCliModule({ + packageJson, init: async reg => { reg.addCommand({ path: ['translations', 'export'], diff --git a/packages/cli/src/modules/translations/lib/discoverPackages.ts b/packages/cli-module-translations/src/lib/discoverPackages.ts similarity index 100% rename from packages/cli/src/modules/translations/lib/discoverPackages.ts rename to packages/cli-module-translations/src/lib/discoverPackages.ts diff --git a/packages/cli/src/modules/translations/lib/extractTranslations.test.ts b/packages/cli-module-translations/src/lib/extractTranslations.test.ts similarity index 88% rename from packages/cli/src/modules/translations/lib/extractTranslations.test.ts rename to packages/cli-module-translations/src/lib/extractTranslations.test.ts index 20ff05b69e..13a94f3a0a 100644 --- a/packages/cli/src/modules/translations/lib/extractTranslations.test.ts +++ b/packages/cli-module-translations/src/lib/extractTranslations.test.ts @@ -23,11 +23,11 @@ import { describe('extractTranslations', () => { it('extracts translation refs from the org plugin', () => { const project = createTranslationProject( - resolvePath(__dirname, '../../../../../../tsconfig.json'), + resolvePath(__dirname, '../../../../tsconfig.json'), ); const sourceFile = project.addSourceFileAtPath( - resolvePath(__dirname, '../../../../../..', 'plugins/org/src/alpha.tsx'), + resolvePath(__dirname, '../../../..', 'plugins/org/src/alpha.tsx'), ); const refs = extractTranslationRefsFromSourceFile( @@ -59,12 +59,12 @@ describe('extractTranslations', () => { it('ignores non-TranslationRef exports', () => { const project = createTranslationProject( - resolvePath(__dirname, '../../../../../../tsconfig.json'), + resolvePath(__dirname, '../../../../tsconfig.json'), ); // The main entry of org plugin exports components but no translation ref const sourceFile = project.addSourceFileAtPath( - resolvePath(__dirname, '../../../../../..', 'plugins/org/src/index.ts'), + resolvePath(__dirname, '../../../..', 'plugins/org/src/index.ts'), ); const refs = extractTranslationRefsFromSourceFile( @@ -78,13 +78,13 @@ describe('extractTranslations', () => { it('extracts from the test fixtures translation ref', () => { const project = createTranslationProject( - resolvePath(__dirname, '../../../../../../tsconfig.json'), + resolvePath(__dirname, '../../../../tsconfig.json'), ); const sourceFile = project.addSourceFileAtPath( resolvePath( __dirname, - '../../../../../..', + '../../../..', 'packages/frontend-plugin-api/src/translation/__fixtures__/refs.ts', ), ); diff --git a/packages/cli/src/modules/translations/lib/extractTranslations.ts b/packages/cli-module-translations/src/lib/extractTranslations.ts similarity index 100% rename from packages/cli/src/modules/translations/lib/extractTranslations.ts rename to packages/cli-module-translations/src/lib/extractTranslations.ts diff --git a/packages/cli/src/modules/translations/lib/messageFilePath.test.ts b/packages/cli-module-translations/src/lib/messageFilePath.test.ts similarity index 100% rename from packages/cli/src/modules/translations/lib/messageFilePath.test.ts rename to packages/cli-module-translations/src/lib/messageFilePath.test.ts diff --git a/packages/cli/src/modules/translations/lib/messageFilePath.ts b/packages/cli-module-translations/src/lib/messageFilePath.ts similarity index 100% rename from packages/cli/src/modules/translations/lib/messageFilePath.ts rename to packages/cli-module-translations/src/lib/messageFilePath.ts diff --git a/packages/cli-node/.eslintrc.js b/packages/cli-node/.eslintrc.js index e2a53a6ad2..7e7c302633 100644 --- a/packages/cli-node/.eslintrc.js +++ b/packages/cli-node/.eslintrc.js @@ -1 +1,19 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +/* + * Copyright 2026 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. + */ +module.exports = { + ...require('@backstage/cli/config/eslint-factory')(__dirname), + ignorePatterns: ['config/**'], +}; diff --git a/packages/cli-node/config/nodeTransform.cjs b/packages/cli-node/config/nodeTransform.cjs new file mode 100644 index 0000000000..15bcdba9bc --- /dev/null +++ b/packages/cli-node/config/nodeTransform.cjs @@ -0,0 +1,87 @@ +/* + * 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. + */ + +const { pathToFileURL } = require('node:url'); +const { transformSync } = require('@swc/core'); +const { addHook } = require('pirates'); +const { Module } = require('node:module'); + +// This hooks into module resolution and overrides imports of packages that +// exist in the linked workspace to instead be resolved from the linked workspace. +if (process.env.BACKSTAGE_CLI_LINKED_WORKSPACE) { + const { join: joinPath } = require('node:path'); + const { getPackagesSync } = require('@manypkg/get-packages'); + const { packages: linkedPackages, root: linkedRoot } = getPackagesSync( + process.env.BACKSTAGE_CLI_LINKED_WORKSPACE, + ); + + // Matches all packages in the linked workspaces, as well as sub-path exports from them + const replacementRegex = new RegExp( + `^(?:${linkedPackages + .map(pkg => pkg.packageJson.name) + .join('|')})(?:/.*)?$`, + ); + + const origLoad = Module._load; + Module._load = function requireHook(request, parent) { + if (!replacementRegex.test(request)) { + return origLoad.call(this, request, parent); + } + + // The package import that we're overriding will always existing in the root + // node_modules of the linked workspace, so it's enough to override the + // parent paths with that single entry + return origLoad.call(this, request, { + ...parent, + paths: [joinPath(linkedRoot.dir, 'node_modules')], + }); + }; +} + +addHook( + (code, filename) => { + const transformed = transformSync(code, { + filename, + sourceMaps: 'inline', + module: { + type: 'commonjs', + ignoreDynamic: true, + }, + jsc: { + target: 'es2023', + parser: { + syntax: 'typescript', + }, + }, + }); + process.send?.({ type: 'watch', path: filename }); + return transformed.code; + }, + { extensions: ['.ts', '.cts'], ignoreNodeModules: true }, +); + +addHook( + (code, filename) => { + process.send?.({ type: 'watch', path: filename }); + return code; + }, + { extensions: ['.js', '.cjs'], ignoreNodeModules: true }, +); + +// Register module hooks, used by "type": "module" in package.json, .mjs and +// .mts files, as well as dynamic import(...)s, although dynamic imports will be +// handled be the CommonJS hooks in this file if what it points to is CommonJS. +Module.register('./nodeTransformHooks.mjs', pathToFileURL(__filename)); diff --git a/packages/cli-node/config/nodeTransformHooks.mjs b/packages/cli-node/config/nodeTransformHooks.mjs new file mode 100644 index 0000000000..592867e57a --- /dev/null +++ b/packages/cli-node/config/nodeTransformHooks.mjs @@ -0,0 +1,294 @@ +/* + * 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 { dirname, extname, resolve as resolvePath } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { transformFile } from '@swc/core'; +import { isBuiltin } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; + +// @ts-check + +// No explicit file extension, no type in package.json +const DEFAULT_MODULE_FORMAT = 'commonjs'; + +// Source file extensions to look for when using bundle resolution strategy +const SRC_EXTS = ['.ts', '.js']; +const TS_EXTS = ['.ts', '.mts', '.cts']; +const moduleTypeTable = { + '.mjs': 'module', + '.mts': 'module', + '.cjs': 'commonjs', + '.cts': 'commonjs', + '.ts': undefined, + '.js': undefined, +}; + +/** @type {import('module').ResolveHook} */ +export async function resolve(specifier, context, nextResolve) { + // Built-in modules are handled by the default resolver + if (isBuiltin(specifier)) { + return nextResolve(specifier, context); + } + + const ext = extname(specifier); + + // Unless there's an explicit import attribute, JSON files are loaded with our custom loader that's defined below. + if (ext === '.json' && !context.importAttributes?.type) { + const jsonResult = await nextResolve(specifier, context); + return { + ...jsonResult, + format: 'commonjs', + importAttributes: { type: 'json' }, + }; + } + + // Anything else with an explicit extension is handled by the default + // resolver, except that we help determine the module type where needed. + if (ext !== '') { + return withDetectedModuleType(await nextResolve(specifier, context)); + } + + // Other external modules are handled by the default resolver, but again we + // help determine the module type where needed. + if (!specifier.startsWith('.')) { + return withDetectedModuleType(await nextResolve(specifier, context)); + } + + // The rest of this function handles the case of resolving imports that do not + // specify any extension and might point to a directory with an `index.*` + // file. We resolve those using the same logic as most JS bundlers would, with + // the addition of checking if there's an explicit module format listed in the + // closest `package.json` file. + // + // We use a bundle resolution strategy in order to keep code consistent across + // Backstage codebases that contains code both for Web and Node.js, and to + // support packages with common code that can be used in both environments. + try { + // This is expected to throw, but in the event that this module specifier is + // supported we prefer to use the default resolver. + return await nextResolve(specifier, context); + } catch (error) { + if (error.code === 'ERR_UNSUPPORTED_DIR_IMPORT') { + const spec = `${specifier}${specifier.endsWith('/') ? '' : '/'}index`; + const resolved = await resolveWithoutExt(spec, context, nextResolve); + if (resolved) { + return withDetectedModuleType(resolved); + } + } else if (error.code === 'ERR_MODULE_NOT_FOUND') { + const resolved = await resolveWithoutExt(specifier, context, nextResolve); + if (resolved) { + return withDetectedModuleType(resolved); + } + } + + // Unexpected error or no resolution found + throw error; + } +} + +/** + * Populates the `format` field in the resolved object based on the closest `package.json` file. + * + * @param {import('module').ResolveFnOutput} resolved + * @returns {Promise} + */ +async function withDetectedModuleType(resolved) { + // Already has an explicit format + if (resolved.format) { + return resolved; + } + // Happens in Node.js v22 when there's a package.json without an explicit "type" field. Use the default. + if (resolved.format === null) { + return { ...resolved, format: DEFAULT_MODULE_FORMAT }; + } + + const ext = extname(resolved.url); + + const explicitFormat = moduleTypeTable[ext]; + if (explicitFormat) { + return { + ...resolved, + format: explicitFormat, + }; + } + + // Under normal circumstances .js files should reliably have a format and so + // we should only reach this point for .ts files. However, if additional + // custom loaders are being used the format may not be detected for .js files + // either. As such we don't restrict the file format at this point. + + // TODO(Rugvip): Does this need caching? kept it simple for now but worth exploring + const packageJsonPath = await findPackageJSON(fileURLToPath(resolved.url)); + if (!packageJsonPath) { + return resolved; + } + + const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')); + return { + ...resolved, + format: packageJson.type ?? DEFAULT_MODULE_FORMAT, + }; +} + +/** + * Find the closest package.json file from the given path. + * + * TODO(Rugvip): This can be replaced with the Node.js built-in with the same name once it is stable. + * @param {string} startPath + * @returns {Promise} + */ +async function findPackageJSON(startPath) { + let path = startPath; + + // Some confidence check to avoid infinite loop + for (let i = 0; i < 1000; i++) { + const packagePath = resolvePath(path, 'package.json'); + if (existsSync(packagePath)) { + return packagePath; + } + + const newPath = dirname(path); + if (newPath === path) { + return undefined; + } + path = newPath; + } + + throw new Error( + `Iteration limit reached when searching for package.json at ${startPath}`, + ); +} + +/** @type {import('module').ResolveHook} */ +async function resolveWithoutExt(specifier, context, nextResolve) { + for (const tryExt of SRC_EXTS) { + try { + const resolved = await nextResolve(specifier + tryExt, { + ...context, + format: 'commonjs', + }); + return { + ...resolved, + format: moduleTypeTable[tryExt] ?? resolved.format, + }; + } catch { + /* ignore */ + } + } + return undefined; +} + +/** @type {import('module').LoadHook} */ +export async function load(url, context, nextLoad) { + // Non-file URLs are handled by the default loader + if (!url.startsWith('file://')) { + return nextLoad(url, context); + } + + // JSON files loaded as CommonJS are handled by this custom loader, because + // the default one doesn't work. For JSON loading to work we'd need the + // synchronous hooks that aren't supported yet, or avoid using the CommonJS + // compatibility. + if ( + context.format === 'commonjs' && + context.importAttributes?.type === 'json' + ) { + try { + // TODO(Rugvip): Make sure this is valid JSON + const content = await readFile(fileURLToPath(url), 'utf8'); + return { + source: `module.exports = (${content})`, + format: 'commonjs', + shortCircuit: true, + }; + } catch { + // Let the default loader generate the error + return nextLoad(url, context); + } + } + + const ext = extname(url); + + // Non-TS files are handled by the default loader + if (!TS_EXTS.includes(ext)) { + return nextLoad(url, context); + } + + const format = context.format ?? DEFAULT_MODULE_FORMAT; + + // We have two choices at this point, we can either transform CommonJS files + // and return the transformed source code, or let the default loader handle + // them. If we transform them ourselves we will enter CommonJS compatibility + // mode in the new module system in Node.js, this effectively means all + // CommonJS loaded via `require` calls from this point will all be treated as + // if it was loaded via `import` calls from modules. + // + // The CommonJS compatibility layer will try to identify named exports and + // make them available directly, which is convenient as it avoids things like + // `import(...).then(m => m.default.foo)`, allowing you to instead write + // `import(...).then(m => m.foo)`. The compatibility layer doesn't always work + // all that well though, and can lead to module loading issues in many cases, + // especially for older code. + + // This `if` block opts-out of using CommonJS compatibility mode by default, + // and instead leaves it to our existing loader to transform CommonJS. We do + // however use compatibility mode for the more explicit .cts file extension, + // allows for a way to opt-in to the new behavior. + // + // TODO(Rugvip): Once the synchronous hooks API is available for us to use, we might be able to adopt that instead + if (format === 'commonjs' && ext !== '.cts') { + return nextLoad(url, { ...context, format }); + } + + // If the Node.js version we're running supports TypeScript, i.e. type + // stripping, we hand over to the default loader. This is done for all cases + // except if we're loading a .ts file that's been resolved to CommonJS format. + // This is because these files aren't actually CommonJS in the Backstage build + // system, and need to be transformed to CommonJS. + if ( + format === 'module-typescript' || + (format === 'module-commonjs' && ext !== '.ts') + ) { + return nextLoad(url, { ...context, format }); + } + + const transformed = await transformFile(fileURLToPath(url), { + sourceMaps: 'inline', + module: { + type: format === 'module' ? 'es6' : 'commonjs', + ignoreDynamic: true, + + // This helps the Node.js CommonJS compat layer identify named exports. + exportInteropAnnotation: true, + }, + jsc: { + target: 'es2023', + parser: { + syntax: 'typescript', + }, + }, + }); + + return { + ...context, + shortCircuit: true, + source: transformed.code, + format, + responseURL: url, + }; +} diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index 4fa5af208c..483b0eccff 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -20,7 +20,8 @@ "main": "src/index.ts", "types": "src/index.ts", "files": [ - "dist" + "dist", + "config" ], "scripts": { "build": "backstage-cli package build", @@ -37,7 +38,10 @@ "@manypkg/get-packages": "^1.1.3", "@yarnpkg/lockfile": "^1.1.0", "@yarnpkg/parsers": "^3.0.0", + "chalk": "^4.0.0", + "commander": "^12.0.0", "fs-extra": "^11.2.0", + "pirates": "^4.0.6", "semver": "^7.5.3", "yaml": "^2.0.0", "zod": "^3.25.76" @@ -47,5 +51,13 @@ "@backstage/cli": "workspace:^", "@backstage/test-utils": "workspace:^", "@types/yarnpkg__lockfile": "^1.1.4" + }, + "peerDependencies": { + "@swc/core": "^1.15.6" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } } } diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index 9d4887ca59..2fac82080b 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -86,6 +86,36 @@ export interface BackstagePackageJson { version: string; } +// @public +export interface CliCommand { + deprecated?: boolean; + description: string; + execute: + | ((context: CliCommandContext) => Promise) + | { + loader: () => Promise<{ + default: (context: CliCommandContext) => Promise; + }>; + }; + experimental?: boolean; + path: string[]; +} + +// @public +export interface CliCommandContext { + args: string[]; + info: { + usage: string; + name: string; + }; +} + +// @public +export interface CliModule { + // (undocumented) + readonly $$type: '@backstage/CliModule'; +} + // @public export type ConcurrentTasksOptions = { concurrencyFactor?: number; @@ -93,6 +123,16 @@ export type ConcurrentTasksOptions = { worker: (item: TItem) => Promise; }; +// @public +export function createCliModule(options: { + packageJson: { + name: string; + }; + init: (registry: { + addCommand: (command: CliCommand) => void; + }) => Promise; +}): CliModule; + // @public export class GitUtils { static listChangedFiles(ref: string): Promise; @@ -187,6 +227,7 @@ export type PackageRole = | 'frontend' | 'backend' | 'cli' + | 'cli-module' | 'web-library' | 'node-library' | 'common-library' @@ -212,6 +253,13 @@ export class PackageRoles { static getRoleInfo(role: string): PackageRoleInfo; } +// @public +export function runCliModule(options: { + module: CliModule; + name: string; + version?: string; +}): Promise; + // @public export function runConcurrentTasks( options: ConcurrentTasksOptions, diff --git a/packages/cli-node/src/cli-module/createCliModule.ts b/packages/cli-node/src/cli-module/createCliModule.ts new file mode 100644 index 0000000000..b2facc3a67 --- /dev/null +++ b/packages/cli-node/src/cli-module/createCliModule.ts @@ -0,0 +1,73 @@ +/* + * 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 { OpaqueCliModule } from '@internal/cli'; +import { CliCommand, CliModule } from './types'; + +/** + * Creates a new CLI plugin that provides commands to the Backstage CLI. + * + * The `init` callback is invoked immediately at creation time and is used + * to register commands via the provided registry. The commands are then + * made available to the CLI host once the returned promise resolves. + * + * @example + * ``` + * import { createCliModule } from '@backstage/cli-node'; + * import packageJson from '../package.json'; + * + * export default createCliModule({ + * packageJson, + * init: async reg => { + * reg.addCommand({ + * path: ['repo', 'test'], + * description: 'Run tests across the repository', + * execute: { loader: () => import('./commands/test') }, + * }); + * }, + * }); + * ``` + * + * @public + */ +export function createCliModule(options: { + /** The `package.json` contents of the plugin package, used to identify the plugin. */ + packageJson: { name: string }; + /** + * An initialization callback that registers commands with the CLI. + * Called immediately when the plugin is created. + */ + init: (registry: { + /** Registers a new command with the CLI. */ + addCommand: (command: CliCommand) => void; + }) => Promise; +}): CliModule { + if (!options.packageJson.name) { + throw new Error( + 'The packageJson provided to createCliModule must have a name', + ); + } + + const commands: CliCommand[] = []; + const commandsPromise = Promise.resolve() + .then(() => options.init({ addCommand: command => commands.push(command) })) + .then(() => commands); + + return OpaqueCliModule.createInstance('v1', { + packageName: options.packageJson.name, + commands: commandsPromise, + }); +} diff --git a/packages/cli-node/src/cli-module/index.ts b/packages/cli-node/src/cli-module/index.ts new file mode 100644 index 0000000000..56324c3423 --- /dev/null +++ b/packages/cli-node/src/cli-module/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { createCliModule } from './createCliModule'; +export { runCliModule } from './runCliModule'; +export type { CliCommand, CliCommandContext, CliModule } from './types'; diff --git a/packages/cli-node/src/cli-module/runCliModule.ts b/packages/cli-node/src/cli-module/runCliModule.ts new file mode 100644 index 0000000000..0dc40e4c33 --- /dev/null +++ b/packages/cli-node/src/cli-module/runCliModule.ts @@ -0,0 +1,222 @@ +/* + * 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 { + OpaqueCliModule, + OpaqueCommandTreeNode, + OpaqueCommandLeafNode, + isCommandNodeHidden, +} from '@internal/cli'; +import type { CommandNode } from '@internal/cli'; +import { Command } from 'commander'; +import chalk from 'chalk'; +import { isError, stringifyError } from '@backstage/errors'; +import type { CliModule, CliCommand } from './types'; + +function buildCommandGraph(commands: ReadonlyArray): CommandNode[] { + const graph: CommandNode[] = []; + + for (const command of commands) { + const { path } = command; + let current = graph; + + for (let i = 0; i < path.length - 1; i++) { + const name = path[i]; + let next = current.find( + n => + OpaqueCommandTreeNode.isType(n) && + OpaqueCommandTreeNode.toInternal(n).name === name, + ); + if (!next) { + next = OpaqueCommandTreeNode.createInstance('v1', { + name, + children: [], + }); + current.push(next); + } + current = OpaqueCommandTreeNode.toInternal(next).children; + } + + current.push( + OpaqueCommandLeafNode.createInstance('v1', { + name: path[path.length - 1], + command, + }), + ); + } + + return graph; +} + +function exitWithError(error: unknown): never { + process.stderr.write(`\n${chalk.red(stringifyError(error))}\n\n`); + process.exit( + isError(error) && 'code' in error && typeof error.code === 'number' + ? error.code + : 1, + ); +} + +function registerCommands( + graph: CommandNode[], + program: Command, + programName: string, +): void { + const queue = graph.map(node => ({ node, argParser: program })); + + while (queue.length) { + const { node, argParser } = queue.shift()!; + + if (OpaqueCommandTreeNode.isType(node)) { + const internal = OpaqueCommandTreeNode.toInternal(node); + const treeParser = argParser + .command(`${internal.name} [command]`, { + hidden: isCommandNodeHidden(node), + }) + .description(internal.name); + + queue.push( + ...internal.children.map(child => ({ + node: child, + argParser: treeParser, + })), + ); + } else { + const internal = OpaqueCommandLeafNode.toInternal(node); + argParser + .command(internal.name, { + hidden: + !!internal.command.deprecated || !!internal.command.experimental, + }) + .description(internal.command.description) + .helpOption(false) + .allowUnknownOption(true) + .allowExcessArguments(true) + .action(async () => { + try { + const args = program.parseOptions(process.argv); + + const nonProcessArgs = args.operands.slice(2); + const positionalArgs = []; + let index = 0; + for ( + let argIndex = 0; + argIndex < nonProcessArgs.length; + argIndex++ + ) { + if ( + argIndex === index && + internal.command.path[argIndex] === nonProcessArgs[argIndex] + ) { + index += 1; + continue; + } + positionalArgs.push(nonProcessArgs[argIndex]); + } + const context = { + args: [...positionalArgs, ...args.unknown], + info: { + usage: [programName, ...internal.command.path].join(' '), + name: internal.command.path.join(' '), + }, + }; + + if (typeof internal.command.execute === 'function') { + await internal.command.execute(context); + } else { + const mod = await internal.command.execute.loader(); + const fn = + typeof mod.default === 'function' + ? mod.default + : (mod.default as any).default; + await fn(context); + } + process.exit(0); + } catch (error: unknown) { + exitWithError(error); + } + }); + } + } +} + +/** + * Runs a CLI module as a standalone program. + * + * This helper extracts the commands from a {@link CliModule} and exposes + * them as a fully functional CLI with help output and argument parsing. + * It is intended to be called from a module package's `bin` entry point + * so that the module can be executed directly without being wired into + * a larger CLI host. + * + * @example + * ```ts + * #!/usr/bin/env node + * import { runCliModule } from '@backstage/cli-node'; + * import cliModule from './index'; + * + * runCliModule({ + * module: cliModule, + * name: 'backstage-auth', + * version: require('../package.json').version, + * }); + * ``` + * + * @public + */ +export async function runCliModule(options: { + /** The CLI module to run. */ + module: CliModule; + /** The program name shown in help output and usage strings. */ + name: string; + /** The version string shown when `--version` is passed. */ + version?: string; +}): Promise { + const { module: cliModule, name, version } = options; + + if (!OpaqueCliModule.isType(cliModule)) { + throw new Error( + `Invalid CLI module: expected a module created with createCliModule`, + ); + } + + const internal = OpaqueCliModule.toInternal(cliModule); + const commands = await internal.commands; + const graph = buildCommandGraph(commands); + + const program = new Command(); + program.name(name).allowUnknownOption(true).allowExcessArguments(true); + + if (version) { + program.version(version); + } + + registerCommands(graph, program, name); + + program.on('command:*', () => { + console.log(); + console.log(chalk.red(`Invalid command: ${program.args.join(' ')}`)); + console.log(); + program.outputHelp(); + process.exit(1); + }); + + process.on('unhandledRejection', rejection => { + exitWithError(rejection); + }); + + await program.parseAsync(process.argv); +} diff --git a/packages/cli-node/src/cli-module/types.ts b/packages/cli-node/src/cli-module/types.ts new file mode 100644 index 0000000000..3db290f89f --- /dev/null +++ b/packages/cli-node/src/cli-module/types.ts @@ -0,0 +1,118 @@ +/* + * 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. + */ + +/** + * The context provided to a CLI command at the time of execution. + * + * Contains the parsed arguments and metadata about the command being run. + * + * @public + */ +export interface CliCommandContext { + /** + * The remaining arguments passed to the command after the command path + * has been resolved. This includes both positional arguments and flags. + * + * For example, running `backstage-cli repo test --verbose src/` would + * result in `args` being `['--verbose', 'src/']`. + */ + args: string[]; + /** + * Metadata about the command being executed. + */ + info: { + /** + * The full usage string of the command including the program name, + * for example `"backstage-cli repo test"`. + */ + usage: string; + /** + * The name of the command as defined by its path, + * for example `"repo test"`. + */ + name: string; + }; +} + +/** + * A command definition for a Backstage CLI plugin. + * + * Each command is identified by a `path` that determines its position in + * the command tree. For example, a path of `['repo', 'test']` registers + * the command as `backstage-cli repo test`. + * + * Commands can either provide an `execute` function directly, or use a + * `loader` for deferred loading of the implementation. The loader pattern + * is recommended for commands with heavy dependencies, as it avoids + * loading the implementation until the command is actually invoked. + * + * @public + */ +export interface CliCommand { + /** + * The path segments that define the command's position in the CLI tree. + * For example, `['repo', 'test']` maps to `backstage-cli repo test`. + */ + path: string[]; + /** + * A short description of the command, displayed in help output. + */ + description: string; + /** + * If `true`, the command is deprecated and will be hidden from help output + * but can still be invoked. + */ + deprecated?: boolean; + /** + * If `true`, the command is experimental and will be hidden from help + * output but can still be invoked. + */ + experimental?: boolean; + /** + * The command implementation, either as a direct function or as a loader + * that returns the implementation as a default export. The loader form + * is useful for deferring heavy imports until the command is invoked. + * + * @example + * Direct execution: + * ``` + * execute: async ({ args }) => { ... } + * ``` + * + * @example + * Deferred loading: + * ``` + * execute: { loader: () => import('./my-command') } + * ``` + */ + execute: + | ((context: CliCommandContext) => Promise) + | { + loader: () => Promise<{ + default: (context: CliCommandContext) => Promise; + }>; + }; +} + +/** + * An opaque representation of a Backstage CLI plugin, created + * using {@link createCliModule}. + * + * @public + */ +export interface CliModule { + readonly $$type: '@backstage/CliModule'; +} diff --git a/packages/cli-node/src/index.ts b/packages/cli-node/src/index.ts index 35074507c4..8831753a82 100644 --- a/packages/cli-node/src/index.ts +++ b/packages/cli-node/src/index.ts @@ -21,6 +21,7 @@ */ export * from './cache'; +export * from './cli-module'; export * from './concurrency'; export * from './git'; export * from './monorepo'; diff --git a/packages/cli-node/src/roles/PackageRoles.ts b/packages/cli-node/src/roles/PackageRoles.ts index 70b62e2479..7761b0d4cc 100644 --- a/packages/cli-node/src/roles/PackageRoles.ts +++ b/packages/cli-node/src/roles/PackageRoles.ts @@ -33,6 +33,11 @@ const packageRoleInfos: PackageRoleInfo[] = [ platform: 'node', output: ['cjs'], }, + { + role: 'cli-module', + platform: 'node', + output: ['types', 'cjs'], + }, { role: 'web-library', platform: 'web', diff --git a/packages/cli-node/src/roles/types.ts b/packages/cli-node/src/roles/types.ts index d6eff04541..d0eaedbd02 100644 --- a/packages/cli-node/src/roles/types.ts +++ b/packages/cli-node/src/roles/types.ts @@ -23,6 +23,7 @@ export type PackageRole = | 'frontend' | 'backend' | 'cli' + | 'cli-module' | 'web-library' | 'node-library' | 'common-library' diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js index dbb40537ab..4e51803802 100644 --- a/packages/cli/config/eslint-factory.js +++ b/packages/cli/config/eslint-factory.js @@ -269,6 +269,7 @@ function createConfigForRole(dir, role, extraConfig = {}) { }); case 'cli': + case 'cli-module': case 'node-library': case 'backend': case 'backend-plugin': diff --git a/packages/cli/config/getJestEnvironment.js b/packages/cli/config/getJestEnvironment.js index aac2f79021..e6fb66f407 100644 --- a/packages/cli/config/getJestEnvironment.js +++ b/packages/cli/config/getJestEnvironment.js @@ -14,36 +14,14 @@ * limitations under the License. */ -function getJestMajorVersion() { - const jestVersion = require('jest/package.json').version; - const majorVersion = parseInt(jestVersion.split('.')[0], 10); - return majorVersion; -} - -function getJestEnvironment() { - const majorVersion = getJestMajorVersion(); - - if (majorVersion >= 30) { - try { - require.resolve('@jest/environment-jsdom-abstract'); - require.resolve('jsdom'); - } catch { - throw new Error( - 'Jest 30+ requires @jest/environment-jsdom-abstract and jsdom. ' + - 'Please install them as dev dependencies.', - ); - } - return require.resolve('./jest-environment-jsdom'); - } - try { - require.resolve('jest-environment-jsdom'); - } catch { +try { + module.exports = require('@backstage/cli-module-test-jest/config/getJestEnvironment'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { throw new Error( - 'Jest 29 requires jest-environment-jsdom. ' + - 'Please install it as a dev dependency.', + '@backstage/cli-module-test-jest is required to use the jest environment configuration. ' + + 'Please install it as a dependency.', ); } - return require.resolve('jest-environment-jsdom'); + throw e; } - -module.exports = { getJestMajorVersion, getJestEnvironment }; diff --git a/packages/cli/config/jest-environment-jsdom/index.js b/packages/cli/config/jest-environment-jsdom/index.js index 9d8b76eaf5..f9be215d76 100644 --- a/packages/cli/config/jest-environment-jsdom/index.js +++ b/packages/cli/config/jest-environment-jsdom/index.js @@ -14,48 +14,14 @@ * limitations under the License. */ -const JSDOMEnvironment = require('@jest/environment-jsdom-abstract').default; -const jsdom = require('jsdom'); - -/** - * A custom JSDOM environment that extends the abstract base and applies - * fixes for Web API globals that are missing or incorrectly implemented - * in JSDOM. - * - * Based on https://github.com/mswjs/jest-fixed-jsdom - */ -class FixedJSDOMEnvironment extends JSDOMEnvironment { - constructor(config, context) { - super(config, context, jsdom); - - // Fix Web API globals that JSDOM doesn't properly expose - this.global.TextDecoder = TextDecoder; - this.global.TextEncoder = TextEncoder; - this.global.TextDecoderStream = TextDecoderStream; - this.global.TextEncoderStream = TextEncoderStream; - this.global.ReadableStream = ReadableStream; - - this.global.Blob = Blob; - this.global.Headers = Headers; - this.global.FormData = FormData; - this.global.Request = Request; - this.global.Response = Response; - this.global.fetch = fetch; - this.global.AbortController = AbortController; - this.global.AbortSignal = AbortSignal; - this.global.structuredClone = structuredClone; - this.global.URL = URL; - this.global.URLSearchParams = URLSearchParams; - - this.global.BroadcastChannel = BroadcastChannel; - this.global.TransformStream = TransformStream; - this.global.WritableStream = WritableStream; - - // Needed to ensure `e instanceof Error` works as expected with errors thrown from - // any of the native APIs above. Without this, the JSDOM `Error` is what the test - // code will use for comparison with `e`, which fails the instanceof check. - this.global.Error = Error; +try { + module.exports = require('@backstage/cli-module-test-jest/config/jest-environment-jsdom'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest JSDOM environment. ' + + 'Please install it as a dependency.', + ); } + throw e; } - -module.exports = FixedJSDOMEnvironment; diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index c670f0fe8b..e06d0e319e 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -14,408 +14,14 @@ * limitations under the License. */ -const fs = require('fs-extra'); -const path = require('node:path'); -const crypto = require('node:crypto'); -const glob = require('node:util').promisify(require('glob')); -const { version } = require('../package.json'); -const paths = require('@backstage/cli-common').findPaths(process.cwd()); -const { - getJestEnvironment, - getJestMajorVersion, -} = require('./getJestEnvironment'); - -const SRC_EXTS = ['ts', 'js', 'tsx', 'jsx', 'mts', 'cts', 'mjs', 'cjs']; - -const FRONTEND_ROLES = [ - 'frontend', - 'web-library', - 'common-library', - 'frontend-plugin', - 'frontend-plugin-module', -]; - -const NODE_ROLES = [ - 'backend', - 'cli', - 'node-library', - 'backend-plugin', - 'backend-plugin-module', -]; - -const envOptions = { - oldTests: Boolean(process.env.BACKSTAGE_OLD_TESTS), -}; - try { - require.resolve('react-dom/client', { - paths: [paths.targetRoot], - }); - process.env.HAS_REACT_DOM_CLIENT = true; -} catch { - /* ignored */ -} - -/** - * A list of config keys that are valid for project-level config. - * Jest will complain if we forward any other root configuration to the projects. - * - * @type {Array} - */ -const projectConfigKeys = [ - 'automock', - 'cache', - 'cacheDirectory', - 'clearMocks', - 'collectCoverageFrom', - 'coverageDirectory', - 'coveragePathIgnorePatterns', - 'cwd', - 'dependencyExtractor', - 'detectLeaks', - 'detectOpenHandles', - 'displayName', - 'errorOnDeprecated', - 'extensionsToTreatAsEsm', - 'fakeTimers', - 'filter', - 'forceCoverageMatch', - 'globalSetup', - 'globalTeardown', - 'globals', - 'haste', - 'id', - 'injectGlobals', - 'moduleDirectories', - 'moduleFileExtensions', - 'moduleNameMapper', - 'modulePathIgnorePatterns', - 'modulePaths', - 'openHandlesTimeout', - 'preset', - 'prettierPath', - 'resetMocks', - 'resetModules', - 'resolver', - 'restoreMocks', - 'rootDir', - 'roots', - 'runner', - 'runtime', - 'sandboxInjectedGlobals', - 'setupFiles', - 'setupFilesAfterEnv', - 'skipFilter', - 'skipNodeResolution', - 'slowTestThreshold', - 'snapshotResolver', - 'snapshotSerializers', - 'snapshotFormat', - 'testEnvironment', - 'testEnvironmentOptions', - 'testMatch', - 'testLocationInResults', - 'testPathIgnorePatterns', - 'testRegex', - 'testRunner', - 'transform', - 'transformIgnorePatterns', - 'watchPathIgnorePatterns', - 'unmockedModulePathPatterns', - 'workerIdleMemoryLimit', -]; - -const transformIgnorePattern = [ - '@material-ui', - 'ajv', - 'core-js', - 'jest-.*', - 'jsdom', - 'knex', - 'react', - 'react-dom', - 'highlight\\.js', - 'prismjs', - 'json-schema', - 'react-use/lib', - 'typescript', -].join('|'); - -// Provides additional config that's based on the role of the target package -function getRoleConfig(role, pkgJson) { - // Only Node.js package roles support native ESM modules, frontend and common - // packages are always transpiled to CommonJS. - const moduleOpts = NODE_ROLES.includes(role) - ? { - module: { - ignoreDynamic: true, - exportInteropAnnotation: true, - }, - } - : undefined; - - const transform = { - '\\.(mjs|cjs|js)$': [ - require.resolve('./jestSwcTransform'), - { - ...moduleOpts, - jsc: { - parser: { - syntax: 'ecmascript', - }, - }, - }, - ], - '\\.jsx$': [ - require.resolve('./jestSwcTransform'), - { - jsc: { - parser: { - syntax: 'ecmascript', - jsx: true, - }, - transform: { - react: { - runtime: 'automatic', - }, - }, - }, - }, - ], - '\\.(mts|cts|ts)$': [ - require.resolve('./jestSwcTransform'), - { - ...moduleOpts, - jsc: { - parser: { - syntax: 'typescript', - }, - }, - }, - ], - '\\.tsx$': [ - require.resolve('./jestSwcTransform'), - { - jsc: { - parser: { - syntax: 'typescript', - tsx: true, - }, - transform: { - react: { - runtime: 'automatic', - }, - }, - }, - }, - ], - '\\.(bmp|gif|jpg|jpeg|png|ico|webp|frag|xml|svg|eot|woff|woff2|ttf)$': - require.resolve('./jestFileTransform.js'), - '\\.(yaml)$': require.resolve('./jestYamlTransform'), - }; - if (FRONTEND_ROLES.includes(role)) { - return { - testEnvironment: getJestEnvironment(), - // The caching module loader is only used to speed up frontend tests, - // as it breaks real dynamic imports of ESM modules. - runtime: envOptions.oldTests - ? undefined - : require.resolve('./jestCachingModuleLoader'), - transform, - }; - } - return { - testEnvironment: require.resolve('jest-environment-node'), - moduleFileExtensions: [...SRC_EXTS, 'json', 'node'], - // Jest doesn't let us dynamically detect type=module per transformed file, - // so we have to assume that if the entry point is ESM, all TS files are - // ESM. - // - // This means you can't switch a package to type=module until all of its - // monorepo dependencies are also type=module or does not contain any .ts - // files. - extensionsToTreatAsEsm: - pkgJson.type === 'module' ? ['.ts', '.mts'] : ['.mts'], - transform, - }; -} - -async function getProjectConfig(targetPath, extraConfig, extraOptions) { - const configJsPath = path.resolve(targetPath, 'jest.config.js'); - const configTsPath = path.resolve(targetPath, 'jest.config.ts'); - // If the package has it's own jest config, we use that instead. - if (await fs.pathExists(configJsPath)) { - return require(configJsPath); - } else if (await fs.pathExists(configTsPath)) { - return require(configTsPath); - } - - // Jest config can be defined both in the root package.json and within each package. The root config - // gets forwarded to us through the `extraConfig` parameter, while the package config is read here. - // If they happen to be the same the keys will simply override each other. - // The merging of the configs is shallow, meaning e.g. all transforms are replaced if new ones are defined. - const pkgJson = await fs.readJson(path.resolve(targetPath, 'package.json')); - - const options = { - ...extraConfig, - rootDir: path.resolve(targetPath, 'src'), - moduleNameMapper: { - '\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'), - }, - - // A bit more opinionated - testMatch: [`**/*.test.{${SRC_EXTS.join(',')}}`], - - transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`], - ...getRoleConfig(pkgJson.backstage?.role, pkgJson), - }; - - options.setupFilesAfterEnv = options.setupFilesAfterEnv || []; - - if ( - extraOptions.rejectFrontendNetworkRequests && - FRONTEND_ROLES.includes(pkgJson.backstage?.role) - ) { - // By adding this first we ensure that it's possible to for example override - // fetch with a mock in a custom setup file - options.setupFilesAfterEnv.unshift( - require.resolve('./jestRejectNetworkRequests.js'), + module.exports = require('@backstage/cli-module-test-jest/config/jest'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use this jest configuration. ' + + 'Please install it as a dependency.', ); } - - if ( - options.testEnvironment === getJestEnvironment() && - getJestMajorVersion() < 30 // Only needed when not running the custom env for Jest 30+ - ) { - // FIXME https://github.com/jsdom/jsdom/issues/1724 - options.setupFilesAfterEnv.unshift(require.resolve('cross-fetch/polyfill')); - } - - // Use src/setupTests.* as the default location for configuring test env - for (const ext of SRC_EXTS) { - if (fs.existsSync(path.resolve(targetPath, `src/setupTests.${ext}`))) { - options.setupFilesAfterEnv.push(`/setupTests.${ext}`); - break; - } - } - - const config = Object.assign(options, pkgJson.jest); - - // The config id is a cache key that lets us share the jest cache across projects. - // If no explicit id was configured, generated one based on the configuration. - if (!config.id) { - const configHash = crypto - .createHash('sha256') - .update(version) - .update(Buffer.alloc(1)) - .update(JSON.stringify(config.transform).replaceAll(paths.targetRoot, '')) - .digest('hex'); - config.id = `backstage_cli_${configHash}`; - } - - return config; + throw e; } - -// This loads the root jest config, which in turn will either refer to a single -// configuration for the current package, or a collection of configurations for -// the target workspace packages -async function getRootConfig() { - const rootPkgJson = await fs.readJson( - paths.resolveTargetRoot('package.json'), - ); - - const baseCoverageConfig = { - coverageDirectory: paths.resolveTarget('coverage'), - coverageProvider: envOptions.oldTests ? 'v8' : 'babel', - collectCoverageFrom: ['**/*.{js,jsx,ts,tsx,mjs,cjs}', '!**/*.d.ts'], - }; - - const { rejectFrontendNetworkRequests, ...rootOptions } = - rootPkgJson.jest ?? {}; - const extraRootOptions = { - rejectFrontendNetworkRequests, - }; - - const ws = rootPkgJson.workspaces; - const workspacePatterns = Array.isArray(ws) ? ws : ws?.packages; - - // Check if we're running within a specific monorepo package. In that case just get the single project config. - if (!workspacePatterns || paths.targetRoot !== paths.targetDir) { - return getProjectConfig( - paths.targetDir, - { - ...baseCoverageConfig, - ...rootOptions, - }, - extraRootOptions, - ); - } - - const globalRootConfig = { ...baseCoverageConfig }; - const globalProjectConfig = {}; - - for (const [key, value] of Object.entries(rootOptions)) { - if (projectConfigKeys.includes(key)) { - globalProjectConfig[key] = value; - } else { - globalRootConfig[key] = value; - } - } - - // If the target package is a workspace root, we find all packages in the - // workspace and load those in as separate jest projects instead. - const projectPaths = await Promise.all( - workspacePatterns.map(pattern => - glob(path.join(paths.targetRoot, pattern)), - ), - ).then(_ => _.flat()); - - let projects = await Promise.all( - projectPaths.flat().map(async projectPath => { - const packagePath = path.resolve(projectPath, 'package.json'); - if (!(await fs.pathExists(packagePath))) { - return undefined; - } - - // We check for the presence of "backstage-cli test" in the package test - // script to determine whether a given package should be tested - const packageData = await fs.readJson(packagePath); - const testScript = packageData.scripts && packageData.scripts.test; - const isSupportedTestScript = - testScript?.includes('backstage-cli test') || - testScript?.includes('backstage-cli package test'); - if (testScript && isSupportedTestScript) { - return await getProjectConfig( - projectPath, - { - ...globalProjectConfig, - displayName: packageData.name, - }, - extraRootOptions, - ); - } - - return undefined; - }), - ).then(cs => cs.filter(Boolean)); - - const cache = global.__backstageCli_jestSuccessCache; - if (cache) { - projects = await cache.filterConfigs(projects, globalRootConfig); - } - const watchProjectFilter = global.__backstageCli_watchProjectFilter; - if (watchProjectFilter) { - projects = await watchProjectFilter.filter(projects); - } - - return { - rootDir: paths.targetRoot, - projects, - testResultsProcessor: cache - ? require.resolve('./jestCacheResultProcessor.cjs') - : undefined, - ...globalRootConfig, - }; -} - -module.exports = getRootConfig(); diff --git a/packages/cli/config/jestCacheResultProcessor.cjs b/packages/cli/config/jestCacheResultProcessor.cjs index 4af401d8b5..58217428cc 100644 --- a/packages/cli/config/jestCacheResultProcessor.cjs +++ b/packages/cli/config/jestCacheResultProcessor.cjs @@ -14,10 +14,14 @@ * limitations under the License. */ -module.exports = async results => { - const cache = global.__backstageCli_jestSuccessCache; - if (cache) { - await cache.reportResults(results); +try { + module.exports = require('@backstage/cli-module-test-jest/config/jestCacheResultProcessor.cjs'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest cache result processor. ' + + 'Please install it as a dependency.', + ); } - return results; -}; + throw e; +} diff --git a/packages/cli/config/jestCachingModuleLoader.js b/packages/cli/config/jestCachingModuleLoader.js index 5652ff67e7..2b6da11007 100644 --- a/packages/cli/config/jestCachingModuleLoader.js +++ b/packages/cli/config/jestCachingModuleLoader.js @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * Copyright 2023 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,22 +14,14 @@ * limitations under the License. */ -// 'jest-runtime' is included with jest and should be kept in sync with the installed jest version -// eslint-disable-next-line @backstage/no-undeclared-imports -const { default: JestRuntime } = require('jest-runtime'); - -module.exports = class CachingJestRuntime extends JestRuntime { - constructor(config, ...restArgs) { - super(config, ...restArgs); - this.allowLoadAsEsm = config.extensionsToTreatAsEsm.includes('.mts'); +try { + module.exports = require('@backstage/cli-module-test-jest/config/jestCachingModuleLoader'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest caching module loader. ' + + 'Please install it as a dependency.', + ); } - - // Unfortunately we need to use this unstable API to make sure that .js files - // are only loaded as modules where ESM is supported, i.e. Node.js packages. - unstable_shouldLoadAsEsm(path, ...restArgs) { - if (!this.allowLoadAsEsm) { - return false; - } - return super.unstable_shouldLoadAsEsm(path, ...restArgs); - } -}; + throw e; +} diff --git a/packages/cli/config/jestFileTransform.js b/packages/cli/config/jestFileTransform.js index ad1c37a4f5..00e1c22815 100644 --- a/packages/cli/config/jestFileTransform.js +++ b/packages/cli/config/jestFileTransform.js @@ -14,31 +14,14 @@ * limitations under the License. */ -const path = require('node:path'); - -module.exports = { - process(src, filename) { - const assetFilename = JSON.stringify(path.basename(filename)); - - if (filename.match(/\.icon\.svg$/)) { - return { - code: `const React = require('react'); - const SvgIcon = require('@material-ui/core/SvgIcon').default; - module.exports = { - __esModule: true, - default: props => React.createElement(SvgIcon, props, { - $$typeof: Symbol.for('react.element'), - type: 'svg', - ref: ref, - key: null, - props: Object.assign({}, props, { - children: ${assetFilename} - }) - }) - };`, - }; - } - - return { code: `module.exports = ${assetFilename};` }; - }, -}; +try { + module.exports = require('@backstage/cli-module-test-jest/config/jestFileTransform'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest file transform. ' + + 'Please install it as a dependency.', + ); + } + throw e; +} diff --git a/packages/cli/config/jestRejectNetworkRequests.js b/packages/cli/config/jestRejectNetworkRequests.js index f6f189a60c..c975257b8c 100644 --- a/packages/cli/config/jestRejectNetworkRequests.js +++ b/packages/cli/config/jestRejectNetworkRequests.js @@ -14,57 +14,14 @@ * limitations under the License. */ -const http = require('node:http'); -const https = require('node:https'); - -const errorMessage = 'Network requests are not allowed in tests'; - -const origHttpAgent = http.globalAgent; -const origHttpsAgent = https.globalAgent; -const origFetch = global.fetch; -const origXMLHttpRequest = global.XMLHttpRequest; - -http.globalAgent = new http.Agent({ - lookup() { - throw new Error(errorMessage); - }, -}); - -https.globalAgent = new https.Agent({ - lookup() { - throw new Error(errorMessage); - }, -}); - -const BLOCKING_FETCH_SYMBOL = Symbol.for( - 'backstage.jestRejectNetworkRequests.blockingFetch', -); - -if (global.fetch) { - const blockingFetch = async (input, init) => { - // If global.fetch still has our marker, block the request - if (global.fetch[BLOCKING_FETCH_SYMBOL]) { - throw new Error(errorMessage); - } - // MSW (or something else) wrapped us - pass through - return origFetch(input, init); - }; - blockingFetch[BLOCKING_FETCH_SYMBOL] = true; - global.fetch = blockingFetch; +try { + module.exports = require('@backstage/cli-module-test-jest/config/jestRejectNetworkRequests'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest network request rejection. ' + + 'Please install it as a dependency.', + ); + } + throw e; } - -if (global.XMLHttpRequest) { - global.XMLHttpRequest = class { - constructor() { - throw new Error(errorMessage); - } - }; -} - -// Reset overrides after each suite to make sure we don't pollute the test environment -afterAll(() => { - http.globalAgent = origHttpAgent; - https.globalAgent = origHttpsAgent; - global.fetch = origFetch; - global.XMLHttpRequest = origXMLHttpRequest; -}); diff --git a/packages/cli/config/jestSucraseTransform.js b/packages/cli/config/jestSucraseTransform.js index 621b9ee297..fe20351c29 100644 --- a/packages/cli/config/jestSucraseTransform.js +++ b/packages/cli/config/jestSucraseTransform.js @@ -14,74 +14,14 @@ * limitations under the License. */ -const { createHash } = require('node:crypto'); -const { transform } = require('sucrase'); -const sucrasePkg = require('sucrase/package.json'); - -const ESM_REGEX = /\b(?:import|export)\b/; - -function createTransformer(config) { - const process = (source, filePath) => { - let transforms; - - if (filePath.endsWith('.esm.js')) { - transforms = ['imports']; - } else if (filePath.endsWith('.js')) { - // This is a very rough filter to avoid transforming things that we quickly - // can be sure are definitely not ESM modules. - if (ESM_REGEX.test(source)) { - transforms = ['imports', 'jsx']; // JSX within .js is currently allowed - } - } else if (filePath.endsWith('.jsx')) { - transforms = ['jsx', 'imports']; - } else if (filePath.endsWith('.ts')) { - transforms = ['typescript', 'imports']; - } else if (filePath.endsWith('.tsx')) { - transforms = ['typescript', 'jsx', 'imports']; - } - - // Only apply the jest transform to the test files themselves - if (transforms && filePath.includes('.test.')) { - transforms.push('jest'); - } - - if (transforms) { - const { code, sourceMap: map } = transform(source, { - transforms, - filePath, - disableESTransforms: true, - sourceMapOptions: { - compiledFilename: filePath, - }, - }); - if (config.enableSourceMaps) { - const b64 = Buffer.from(JSON.stringify(map), 'utf8').toString('base64'); - const suffix = `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${b64}`; - // Include both inline and object source maps, as inline source maps are - // needed for support of some editor integrations. - return { code: `${code}\n${suffix}`, map }; - } - // We only return the `map` result if source maps are enabled, as they - // have a negative impact on the coverage accuracy. - return { code }; - } - - return { code: source }; - }; - - const getCacheKey = sourceText => { - return createHash('sha256') - .update(sourceText) - .update(Buffer.alloc(1)) - .update(sucrasePkg.version) - .update(Buffer.alloc(1)) - .update(JSON.stringify(config)) - .update(Buffer.alloc(1)) - .update('1') // increment whenever the transform logic in this file changes - .digest('hex'); - }; - - return { process, getCacheKey }; +try { + module.exports = require('@backstage/cli-module-test-jest/config/jestSucraseTransform'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest Sucrase transform. ' + + 'Please install it as a dependency.', + ); + } + throw e; } - -module.exports = { createTransformer }; diff --git a/packages/cli/config/jestSwcTransform.js b/packages/cli/config/jestSwcTransform.js index 4183349554..4aa6776c22 100644 --- a/packages/cli/config/jestSwcTransform.js +++ b/packages/cli/config/jestSwcTransform.js @@ -13,32 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -const { createTransformer: createSwcTransformer } = require('@swc/jest'); -const ESM_REGEX = /\b(?:import|export)\b/; - -function createTransformer(config) { - const swcTransformer = createSwcTransformer({ - inputSourceMap: false, - ...config, - }); - const process = (source, filePath, jestOptions) => { - // Skip transformation of .js files without ESM syntax, we never transform from CJS to ESM - if (filePath.endsWith('.js') && !ESM_REGEX.test(source)) { - return { code: source }; - } - - // Skip transformation of .mjs files, they should only be used if ESM support is available - if (jestOptions.supportsStaticESM && filePath.endsWith('.mjs')) { - return { code: source }; - } - - return swcTransformer.process(source, filePath, jestOptions); - }; - - const getCacheKey = swcTransformer.getCacheKey; - - return { process, getCacheKey }; +try { + module.exports = require('@backstage/cli-module-test-jest/config/jestSwcTransform'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest SWC transform. ' + + 'Please install it as a dependency.', + ); + } + throw e; } - -module.exports = { createTransformer }; diff --git a/packages/cli/config/jestYamlTransform.js b/packages/cli/config/jestYamlTransform.js index 7a05274add..85b889498f 100644 --- a/packages/cli/config/jestYamlTransform.js +++ b/packages/cli/config/jestYamlTransform.js @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * 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. @@ -14,27 +14,14 @@ * limitations under the License. */ -const yaml = require('yaml'); -const crypto = require('node:crypto'); - -function createTransformer(config) { - const process = source => { - const json = JSON.stringify(yaml.parse(source), null, 2); - return { code: `module.exports = ${json}`, map: null }; - }; - - const getCacheKey = sourceText => { - return crypto - .createHash('sha256') - .update(sourceText) - .update(Buffer.alloc(1)) - .update(JSON.stringify(config)) - .update(Buffer.alloc(1)) - .update('1') // increment whenever the transform logic in this file changes - .digest('hex'); - }; - - return { process, getCacheKey }; +try { + module.exports = require('@backstage/cli-module-test-jest/config/jestYamlTransform'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest YAML transform. ' + + 'Please install it as a dependency.', + ); + } + throw e; } - -module.exports = { createTransformer }; diff --git a/packages/cli/config/nodeTransform.cjs b/packages/cli/config/nodeTransform.cjs index 15bcdba9bc..c6866b2964 100644 --- a/packages/cli/config/nodeTransform.cjs +++ b/packages/cli/config/nodeTransform.cjs @@ -14,74 +14,14 @@ * limitations under the License. */ -const { pathToFileURL } = require('node:url'); -const { transformSync } = require('@swc/core'); -const { addHook } = require('pirates'); -const { Module } = require('node:module'); - -// This hooks into module resolution and overrides imports of packages that -// exist in the linked workspace to instead be resolved from the linked workspace. -if (process.env.BACKSTAGE_CLI_LINKED_WORKSPACE) { - const { join: joinPath } = require('node:path'); - const { getPackagesSync } = require('@manypkg/get-packages'); - const { packages: linkedPackages, root: linkedRoot } = getPackagesSync( - process.env.BACKSTAGE_CLI_LINKED_WORKSPACE, - ); - - // Matches all packages in the linked workspaces, as well as sub-path exports from them - const replacementRegex = new RegExp( - `^(?:${linkedPackages - .map(pkg => pkg.packageJson.name) - .join('|')})(?:/.*)?$`, - ); - - const origLoad = Module._load; - Module._load = function requireHook(request, parent) { - if (!replacementRegex.test(request)) { - return origLoad.call(this, request, parent); - } - - // The package import that we're overriding will always existing in the root - // node_modules of the linked workspace, so it's enough to override the - // parent paths with that single entry - return origLoad.call(this, request, { - ...parent, - paths: [joinPath(linkedRoot.dir, 'node_modules')], - }); - }; +try { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-node is required to use the node transform. ' + + 'Please install it as a dependency.', + ); + } + throw e; } - -addHook( - (code, filename) => { - const transformed = transformSync(code, { - filename, - sourceMaps: 'inline', - module: { - type: 'commonjs', - ignoreDynamic: true, - }, - jsc: { - target: 'es2023', - parser: { - syntax: 'typescript', - }, - }, - }); - process.send?.({ type: 'watch', path: filename }); - return transformed.code; - }, - { extensions: ['.ts', '.cts'], ignoreNodeModules: true }, -); - -addHook( - (code, filename) => { - process.send?.({ type: 'watch', path: filename }); - return code; - }, - { extensions: ['.js', '.cjs'], ignoreNodeModules: true }, -); - -// Register module hooks, used by "type": "module" in package.json, .mjs and -// .mts files, as well as dynamic import(...)s, although dynamic imports will be -// handled be the CommonJS hooks in this file if what it points to is CommonJS. -Module.register('./nodeTransformHooks.mjs', pathToFileURL(__filename)); diff --git a/packages/cli/config/nodeTransformHooks.mjs b/packages/cli/config/nodeTransformHooks.mjs index 592867e57a..274e24ed39 100644 --- a/packages/cli/config/nodeTransformHooks.mjs +++ b/packages/cli/config/nodeTransformHooks.mjs @@ -14,281 +14,7 @@ * limitations under the License. */ -import { dirname, extname, resolve as resolvePath } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { transformFile } from '@swc/core'; -import { isBuiltin } from 'node:module'; -import { readFile } from 'node:fs/promises'; -import { existsSync } from 'node:fs'; - -// @ts-check - -// No explicit file extension, no type in package.json -const DEFAULT_MODULE_FORMAT = 'commonjs'; - -// Source file extensions to look for when using bundle resolution strategy -const SRC_EXTS = ['.ts', '.js']; -const TS_EXTS = ['.ts', '.mts', '.cts']; -const moduleTypeTable = { - '.mjs': 'module', - '.mts': 'module', - '.cjs': 'commonjs', - '.cts': 'commonjs', - '.ts': undefined, - '.js': undefined, -}; - -/** @type {import('module').ResolveHook} */ -export async function resolve(specifier, context, nextResolve) { - // Built-in modules are handled by the default resolver - if (isBuiltin(specifier)) { - return nextResolve(specifier, context); - } - - const ext = extname(specifier); - - // Unless there's an explicit import attribute, JSON files are loaded with our custom loader that's defined below. - if (ext === '.json' && !context.importAttributes?.type) { - const jsonResult = await nextResolve(specifier, context); - return { - ...jsonResult, - format: 'commonjs', - importAttributes: { type: 'json' }, - }; - } - - // Anything else with an explicit extension is handled by the default - // resolver, except that we help determine the module type where needed. - if (ext !== '') { - return withDetectedModuleType(await nextResolve(specifier, context)); - } - - // Other external modules are handled by the default resolver, but again we - // help determine the module type where needed. - if (!specifier.startsWith('.')) { - return withDetectedModuleType(await nextResolve(specifier, context)); - } - - // The rest of this function handles the case of resolving imports that do not - // specify any extension and might point to a directory with an `index.*` - // file. We resolve those using the same logic as most JS bundlers would, with - // the addition of checking if there's an explicit module format listed in the - // closest `package.json` file. - // - // We use a bundle resolution strategy in order to keep code consistent across - // Backstage codebases that contains code both for Web and Node.js, and to - // support packages with common code that can be used in both environments. - try { - // This is expected to throw, but in the event that this module specifier is - // supported we prefer to use the default resolver. - return await nextResolve(specifier, context); - } catch (error) { - if (error.code === 'ERR_UNSUPPORTED_DIR_IMPORT') { - const spec = `${specifier}${specifier.endsWith('/') ? '' : '/'}index`; - const resolved = await resolveWithoutExt(spec, context, nextResolve); - if (resolved) { - return withDetectedModuleType(resolved); - } - } else if (error.code === 'ERR_MODULE_NOT_FOUND') { - const resolved = await resolveWithoutExt(specifier, context, nextResolve); - if (resolved) { - return withDetectedModuleType(resolved); - } - } - - // Unexpected error or no resolution found - throw error; - } -} - -/** - * Populates the `format` field in the resolved object based on the closest `package.json` file. - * - * @param {import('module').ResolveFnOutput} resolved - * @returns {Promise} - */ -async function withDetectedModuleType(resolved) { - // Already has an explicit format - if (resolved.format) { - return resolved; - } - // Happens in Node.js v22 when there's a package.json without an explicit "type" field. Use the default. - if (resolved.format === null) { - return { ...resolved, format: DEFAULT_MODULE_FORMAT }; - } - - const ext = extname(resolved.url); - - const explicitFormat = moduleTypeTable[ext]; - if (explicitFormat) { - return { - ...resolved, - format: explicitFormat, - }; - } - - // Under normal circumstances .js files should reliably have a format and so - // we should only reach this point for .ts files. However, if additional - // custom loaders are being used the format may not be detected for .js files - // either. As such we don't restrict the file format at this point. - - // TODO(Rugvip): Does this need caching? kept it simple for now but worth exploring - const packageJsonPath = await findPackageJSON(fileURLToPath(resolved.url)); - if (!packageJsonPath) { - return resolved; - } - - const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')); - return { - ...resolved, - format: packageJson.type ?? DEFAULT_MODULE_FORMAT, - }; -} - -/** - * Find the closest package.json file from the given path. - * - * TODO(Rugvip): This can be replaced with the Node.js built-in with the same name once it is stable. - * @param {string} startPath - * @returns {Promise} - */ -async function findPackageJSON(startPath) { - let path = startPath; - - // Some confidence check to avoid infinite loop - for (let i = 0; i < 1000; i++) { - const packagePath = resolvePath(path, 'package.json'); - if (existsSync(packagePath)) { - return packagePath; - } - - const newPath = dirname(path); - if (newPath === path) { - return undefined; - } - path = newPath; - } - - throw new Error( - `Iteration limit reached when searching for package.json at ${startPath}`, - ); -} - -/** @type {import('module').ResolveHook} */ -async function resolveWithoutExt(specifier, context, nextResolve) { - for (const tryExt of SRC_EXTS) { - try { - const resolved = await nextResolve(specifier + tryExt, { - ...context, - format: 'commonjs', - }); - return { - ...resolved, - format: moduleTypeTable[tryExt] ?? resolved.format, - }; - } catch { - /* ignore */ - } - } - return undefined; -} - -/** @type {import('module').LoadHook} */ -export async function load(url, context, nextLoad) { - // Non-file URLs are handled by the default loader - if (!url.startsWith('file://')) { - return nextLoad(url, context); - } - - // JSON files loaded as CommonJS are handled by this custom loader, because - // the default one doesn't work. For JSON loading to work we'd need the - // synchronous hooks that aren't supported yet, or avoid using the CommonJS - // compatibility. - if ( - context.format === 'commonjs' && - context.importAttributes?.type === 'json' - ) { - try { - // TODO(Rugvip): Make sure this is valid JSON - const content = await readFile(fileURLToPath(url), 'utf8'); - return { - source: `module.exports = (${content})`, - format: 'commonjs', - shortCircuit: true, - }; - } catch { - // Let the default loader generate the error - return nextLoad(url, context); - } - } - - const ext = extname(url); - - // Non-TS files are handled by the default loader - if (!TS_EXTS.includes(ext)) { - return nextLoad(url, context); - } - - const format = context.format ?? DEFAULT_MODULE_FORMAT; - - // We have two choices at this point, we can either transform CommonJS files - // and return the transformed source code, or let the default loader handle - // them. If we transform them ourselves we will enter CommonJS compatibility - // mode in the new module system in Node.js, this effectively means all - // CommonJS loaded via `require` calls from this point will all be treated as - // if it was loaded via `import` calls from modules. - // - // The CommonJS compatibility layer will try to identify named exports and - // make them available directly, which is convenient as it avoids things like - // `import(...).then(m => m.default.foo)`, allowing you to instead write - // `import(...).then(m => m.foo)`. The compatibility layer doesn't always work - // all that well though, and can lead to module loading issues in many cases, - // especially for older code. - - // This `if` block opts-out of using CommonJS compatibility mode by default, - // and instead leaves it to our existing loader to transform CommonJS. We do - // however use compatibility mode for the more explicit .cts file extension, - // allows for a way to opt-in to the new behavior. - // - // TODO(Rugvip): Once the synchronous hooks API is available for us to use, we might be able to adopt that instead - if (format === 'commonjs' && ext !== '.cts') { - return nextLoad(url, { ...context, format }); - } - - // If the Node.js version we're running supports TypeScript, i.e. type - // stripping, we hand over to the default loader. This is done for all cases - // except if we're loading a .ts file that's been resolved to CommonJS format. - // This is because these files aren't actually CommonJS in the Backstage build - // system, and need to be transformed to CommonJS. - if ( - format === 'module-typescript' || - (format === 'module-commonjs' && ext !== '.ts') - ) { - return nextLoad(url, { ...context, format }); - } - - const transformed = await transformFile(fileURLToPath(url), { - sourceMaps: 'inline', - module: { - type: format === 'module' ? 'es6' : 'commonjs', - ignoreDynamic: true, - - // This helps the Node.js CommonJS compat layer identify named exports. - exportInteropAnnotation: true, - }, - jsc: { - target: 'es2023', - parser: { - syntax: 'typescript', - }, - }, - }); - - return { - ...context, - shortCircuit: true, - source: transformed.code, - format, - responseURL: url, - }; -} +export { + resolve, + load, +} from '@backstage/cli-node/config/nodeTransformHooks.mjs'; diff --git a/packages/cli/config/webpack-public-path.js b/packages/cli/config/webpack-public-path.js index 3e14e96eb9..d1c36222c9 100644 --- a/packages/cli/config/webpack-public-path.js +++ b/packages/cli/config/webpack-public-path.js @@ -14,18 +14,14 @@ * limitations under the License. */ -// This script is used to pick up and set the public path of the Webpack bundle -// at runtime. The meta tag is injected by the app build, but only present in -// the `index.html.tmpl` file. The runtime value of the meta tag is populated by -// the app backend, when it templates the final `index.html` file. -// -// This is needed for additional chunks to use the correct public path, and it -// is not possible to set the `__webpack_public_path__` variable outside of the -// build itself. The Webpack output also does not read any tags or -// similar, this seems to be the only way to dynamically configure the public -// path at runtime. -const el = document.querySelector('meta[name="backstage-public-path"]'); -const path = el?.getAttribute('content'); -if (path) { - __webpack_public_path__ = path; +try { + require('@backstage/cli-module-build/config/webpack-public-path'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-build is required to use the webpack public path configuration. ' + + 'Please install it as a dependency.', + ); + } + throw e; } diff --git a/packages/cli/package.json b/packages/cli/package.json index 8e03c60d0f..4ba287a9ef 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -47,50 +47,27 @@ ] }, "dependencies": { - "@backstage/catalog-model": "workspace:^", "@backstage/cli-common": "workspace:^", + "@backstage/cli-defaults": "workspace:^", + "@backstage/cli-module-build": "workspace:^", + "@backstage/cli-module-test-jest": "workspace:^", "@backstage/cli-node": "workspace:^", - "@backstage/config": "workspace:^", - "@backstage/config-loader": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/eslint-plugin": "workspace:^", - "@backstage/integration": "workspace:^", - "@backstage/module-federation-common": "workspace:^", - "@backstage/release-manifests": "workspace:^", - "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", - "@module-federation/enhanced": "^0.21.6", - "@octokit/request": "^8.0.0", - "@rollup/plugin-commonjs": "^26.0.0", - "@rollup/plugin-json": "^6.0.0", - "@rollup/plugin-node-resolve": "^15.0.0", - "@rollup/plugin-yaml": "^4.0.0", - "@rspack/core": "^1.4.11", - "@rspack/dev-server": "^1.1.4", - "@rspack/plugin-react-refresh": "^1.4.3", "@spotify/eslint-config-base": "^15.0.0", "@spotify/eslint-config-react": "^15.0.0", "@spotify/eslint-config-typescript": "^15.0.0", "@swc/core": "^1.15.6", - "@swc/helpers": "^0.5.17", "@swc/jest": "^0.2.39", "@types/webpack-env": "^1.15.2", "@typescript-eslint/eslint-plugin": "^8.17.0", "@typescript-eslint/parser": "^8.16.0", - "bfj": "^9.0.2", - "buffer": "^6.0.3", "chalk": "^4.0.0", - "chokidar": "^3.3.1", - "cleye": "^2.3.0", "commander": "^14.0.3", "cross-fetch": "^4.0.0", - "cross-spawn": "^7.0.3", - "css-loader": "^6.5.1", - "ctrlc-windows": "^2.1.0", - "esbuild": "^0.27.0", "eslint": "^8.6.0", "eslint-config-prettier": "^9.0.0", - "eslint-formatter-friendly": "^7.0.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "^28.9.0", @@ -98,55 +75,13 @@ "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-unused-imports": "^4.1.4", - "eslint-rspack-plugin": "^4.2.1", - "express": "^4.22.0", "fs-extra": "^11.2.0", - "git-url-parse": "^15.0.0", "glob": "^7.1.7", - "global-agent": "^3.0.0", - "globby": "^11.1.0", - "handlebars": "^4.7.3", - "html-webpack-plugin": "^5.6.3", - "inquirer": "^8.2.0", "jest-css-modules": "^2.1.0", - "json-schema": "^0.4.0", - "lodash": "^4.17.21", - "minimatch": "^10.2.1", - "node-stdlib-browser": "^1.3.1", - "npm-packlist": "^5.0.0", - "ora": "^5.3.0", - "p-queue": "^6.6.2", "pirates": "^4.0.6", "postcss": "^8.1.0", - "postcss-import": "^16.1.0", - "process": "^0.11.10", - "proper-lockfile": "^4.1.2", - "raw-loader": "^4.0.2", - "react-dev-utils": "^12.0.0-next.60", - "react-refresh": "^0.18.0", - "recursive-readdir": "^2.2.2", - "replace-in-file": "^7.1.0", - "rollup": "^4.27.3", - "rollup-plugin-dts": "^6.1.0", - "rollup-plugin-esbuild": "^6.1.1", - "rollup-plugin-postcss": "^4.0.0", - "rollup-pluginutils": "^2.8.2", - "semver": "^7.5.3", - "shell-quote": "^1.8.1", - "style-loader": "^3.3.1", "sucrase": "^3.20.2", - "swc-loader": "^0.2.3", - "tar": "^7.5.6", - "ts-checker-rspack-plugin": "^1.1.5", - "ts-morph": "^24.0.0", - "undici": "^7.2.3", - "util": "^0.12.3", - "yaml": "^2.0.0", - "yargs": "^16.2.0", - "yml-loader": "^2.1.0", - "yn": "^4.0.0", - "zod": "^3.25.76", - "zod-validation-error": "^4.0.2" + "yaml": "^2.0.0" }, "devDependencies": { "@backstage/backend-plugin-api": "workspace:^", @@ -166,91 +101,28 @@ "@backstage/test-utils": "workspace:^", "@backstage/theme": "workspace:^", "@jest/environment-jsdom-abstract": "^30.0.0", - "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0", - "@types/cross-spawn": "^6.0.2", - "@types/ejs": "^3.1.3", - "@types/express": "^4.17.6", "@types/fs-extra": "^11.0.0", - "@types/http-proxy": "^1.17.4", - "@types/inquirer": "^8.1.3", "@types/jest": "^30.0.0", "@types/node": "^22.13.14", - "@types/npm-packlist": "^3.0.0", - "@types/proper-lockfile": "^4", - "@types/recursive-readdir": "^2.2.0", - "@types/rollup-plugin-peer-deps-external": "^2.2.0", - "@types/rollup-plugin-postcss": "^3.1.4", - "@types/shell-quote": "^1.7.5", - "@types/svgo": "^2.6.2", - "@types/terser-webpack-plugin": "^5.0.4", - "@types/webpack-sources": "^3.2.3", - "del": "^8.0.0", - "esbuild-loader": "^4.0.0", - "eslint-webpack-plugin": "^4.2.0", - "fork-ts-checker-webpack-plugin": "^9.0.0", "jest": "^30.2.0", "jsdom": "^27.1.0", - "mini-css-extract-plugin": "^2.4.2", - "msw": "^1.0.0", - "nodemon": "^3.0.1", - "terser-webpack-plugin": "^5.1.3", - "webpack": "~5.105.0", - "webpack-dev-server": "^5.0.0" - }, - "optionalDependencies": { - "keytar": "^7.9.0" + "nodemon": "^3.0.1" }, "peerDependencies": { "@jest/environment-jsdom-abstract": "^30.0.0", - "@module-federation/enhanced": "^0.21.6", - "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0", - "esbuild-loader": "^4.0.0", - "eslint-webpack-plugin": "^4.2.0", - "fork-ts-checker-webpack-plugin": "^9.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-environment-jsdom": "*", - "jsdom": "^27.1.0", - "mini-css-extract-plugin": "^2.4.2", - "terser-webpack-plugin": "^5.1.3", - "webpack": "~5.105.0", - "webpack-dev-server": "^5.0.0" + "jsdom": "^27.1.0" }, "peerDependenciesMeta": { "@jest/environment-jsdom-abstract": { "optional": true }, - "@module-federation/enhanced": { - "optional": true - }, - "@pmmmwh/react-refresh-webpack-plugin": { - "optional": true - }, - "esbuild-loader": { - "optional": true - }, - "eslint-webpack-plugin": { - "optional": true - }, - "fork-ts-checker-webpack-plugin": { - "optional": true - }, "jest-environment-jsdom": { "optional": true }, "jsdom": { "optional": true - }, - "mini-css-extract-plugin": { - "optional": true - }, - "terser-webpack-plugin": { - "optional": true - }, - "webpack": { - "optional": true - }, - "webpack-dev-server": { - "optional": true } }, "configSchema": { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 14da5e791e..483717fc64 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -14,20 +14,35 @@ * limitations under the License. */ +import chalk from 'chalk'; import { CliInitializer } from './wiring/CliInitializer'; +import { discoverCliModules } from './wiring/discoverCliModules'; (async () => { const initializer = new CliInitializer(); - initializer.add(import('./modules/build')); - initializer.add(import('./modules/config')); - initializer.add(import('./modules/create-github-app')); - initializer.add(import('./modules/info')); - initializer.add(import('./modules/lint')); - initializer.add(import('./modules/maintenance')); - initializer.add(import('./modules/migrate')); - initializer.add(import('./modules/new')); - initializer.add(import('./modules/test')); - initializer.add(import('./modules/translations')); - initializer.add(import('./modules/auth')); + + const discoveredModules = discoverCliModules(); + + if (discoveredModules.length > 0) { + for (const resolvedPath of discoveredModules) { + initializer.add(import(resolvedPath)); + } + } else { + // No CLI modules found in the project root; fall back to the built-in + // set while printing a deprecation warning. + console.error( + chalk.yellow( + `No CLI modules found in the project root dependencies. ` + + `Falling back to the built-in set of modules.\n` + + `This fallback will be removed in a future release. ` + + `Please add @backstage/cli-defaults as a devDependency ` + + `in your root package.json, or install individual ` + + `@backstage/cli-module-* packages for fine-grained control.\n`, + ), + ); + + initializer.add(import('@backstage/cli-defaults')); + } + await initializer.run(); })(); diff --git a/packages/cli/src/wiring/CliInitializer.test.ts b/packages/cli/src/wiring/CliInitializer.test.ts index 10de6d8e09..703ec972fc 100644 --- a/packages/cli/src/wiring/CliInitializer.test.ts +++ b/packages/cli/src/wiring/CliInitializer.test.ts @@ -15,10 +15,28 @@ */ import { CliInitializer } from './CliInitializer'; -import { createCliPlugin } from './factory'; +import { createCliModule } from './factory'; process.exit = jest.fn() as any; +describe('createCliModule', () => { + it('should throw if packageJson has no name', () => { + expect(() => + createCliModule({ + packageJson: { name: '' }, + init: async () => {}, + }), + ).toThrow('The packageJson provided to createCliModule must have a name'); + + expect(() => + createCliModule({ + packageJson: {} as any, + init: async () => {}, + }), + ).toThrow('The packageJson provided to createCliModule must have a name'); + }); +}); + describe('CliInitializer', () => { beforeEach(() => { jest.resetAllMocks(); @@ -28,8 +46,8 @@ describe('CliInitializer', () => { process.argv = ['node', 'cli', 'test']; const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ - pluginId: 'test', + createCliModule({ + packageJson: { name: '@backstage/test' }, init: async reg => reg.addCommand({ path: ['test'], @@ -50,8 +68,8 @@ describe('CliInitializer', () => { process.argv = ['node', 'cli', 'test', '[positional]', '']; const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ - pluginId: 'test', + createCliModule({ + packageJson: { name: '@backstage/test' }, init: async reg => reg.addCommand({ path: ['test'], @@ -72,8 +90,8 @@ describe('CliInitializer', () => { process.argv = ['node', 'cli', 'test', '--verbose']; const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ - pluginId: 'test', + createCliModule({ + packageJson: { name: '@backstage/test' }, init: async reg => reg.addCommand({ path: ['test'], @@ -97,8 +115,8 @@ describe('CliInitializer', () => { process.argv = ['node', 'cli', 'secret']; const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ - pluginId: 'test', + createCliModule({ + packageJson: { name: '@backstage/test' }, init: async reg => { reg.addCommand({ path: ['visible'], @@ -124,8 +142,8 @@ describe('CliInitializer', () => { const writeSpy = jest.spyOn(process.stdout, 'write'); const initializer2 = new CliInitializer(); initializer2.add( - createCliPlugin({ - pluginId: 'test', + createCliModule({ + packageJson: { name: '@backstage/test' }, init: async reg => { reg.addCommand({ path: ['visible'], @@ -152,8 +170,8 @@ describe('CliInitializer', () => { const writeSpy = jest.spyOn(process.stdout, 'write'); const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ - pluginId: 'test', + createCliModule({ + packageJson: { name: '@backstage/test' }, init: async reg => { reg.addCommand({ path: ['visible'], @@ -187,8 +205,8 @@ describe('CliInitializer', () => { const writeSpy = jest.spyOn(process.stdout, 'write'); const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ - pluginId: 'test', + createCliModule({ + packageJson: { name: '@backstage/test' }, init: async reg => { reg.addCommand({ path: ['group', 'alpha'], @@ -223,8 +241,8 @@ describe('CliInitializer', () => { ]; const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ - pluginId: 'test', + createCliModule({ + packageJson: { name: '@backstage/test' }, init: async reg => reg.addCommand({ path: ['test', 'nested', 'command'], @@ -239,4 +257,120 @@ describe('CliInitializer', () => { await initializer.run(); expect(process.exit).toHaveBeenCalledWith(0); }); + + it('should silently override array-sourced module with conflicting individual module while keeping siblings', async () => { + expect.assertions(3); + process.argv = ['node', 'cli', 'test']; + const initializer = new CliInitializer(); + + const individualModule = createCliModule({ + packageJson: { name: '@backstage/individual' }, + init: async reg => + reg.addCommand({ + path: ['test'], + description: 'individual test', + execute: ({ args }) => { + expect(args).toEqual([]); + return Promise.resolve(); + }, + }), + }); + + const conflictingArrayModule = createCliModule({ + packageJson: { name: '@backstage/array-conflict' }, + init: async reg => + reg.addCommand({ + path: ['test'], + description: 'array test (should be skipped)', + execute: () => Promise.resolve(), + }), + }); + + const nonConflictingArrayModule = createCliModule({ + packageJson: { name: '@backstage/array-sibling' }, + init: async reg => + reg.addCommand({ + path: ['other'], + description: 'other command', + execute: () => Promise.resolve(), + }), + }); + + initializer.add(individualModule); + initializer.add([conflictingArrayModule, nonConflictingArrayModule]); + + await initializer.run(); + expect(process.exit).toHaveBeenCalledWith(0); + + // Verify the sibling command is available by running it + process.argv = ['node', 'cli', 'other']; + const initializer2 = new CliInitializer(); + initializer2.add(individualModule); + initializer2.add([conflictingArrayModule, nonConflictingArrayModule]); + await initializer2.run(); + expect(process.exit).toHaveBeenCalledTimes(2); + }); + + it('should error with package names when two individual modules conflict', async () => { + const initializer = new CliInitializer(); + + initializer.add( + createCliModule({ + packageJson: { name: '@backstage/module-a' }, + init: async reg => + reg.addCommand({ + path: ['conflicting'], + description: 'from module A', + execute: () => Promise.resolve(), + }), + }), + ); + + initializer.add( + createCliModule({ + packageJson: { name: '@backstage/module-b' }, + init: async reg => + reg.addCommand({ + path: ['conflicting'], + description: 'from module B', + execute: () => Promise.resolve(), + }), + }), + ); + + await expect(initializer.run()).rejects.toThrow( + 'Command "conflicting" from "@backstage/module-b" conflicts with an existing command from "@backstage/module-a"', + ); + }); + + it('should error with package names when two array-sourced modules conflict', async () => { + const initializer = new CliInitializer(); + + const moduleA = createCliModule({ + packageJson: { name: '@backstage/array-a' }, + init: async reg => + reg.addCommand({ + path: ['shared'], + description: 'from array A', + execute: () => Promise.resolve(), + }), + }); + + const moduleB = createCliModule({ + packageJson: { name: '@backstage/array-b' }, + init: async reg => + reg.addCommand({ + path: ['shared'], + description: 'from array B', + execute: () => Promise.resolve(), + }), + }); + + initializer.add([moduleA]); + initializer.add([moduleB]); + + await expect(initializer.run()).rejects.toThrow( + 'Command "shared" from "@backstage/array-b" conflicts with an existing command from "@backstage/array-a"', + ); + }); }); diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index d9af1e8a4d..adbdfbe517 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -15,8 +15,13 @@ */ import { CommandGraph } from './CommandGraph'; -import { BackstageCommand, CliFeature, OpaqueCliPlugin } from './types'; -import { CommandRegistry } from './CommandRegistry'; +import { + OpaqueCliModule, + OpaqueCommandTreeNode, + OpaqueCommandLeafNode, + isCommandNodeHidden, +} from '@internal/cli'; +import type { CliModule } from '@backstage/cli-node'; import { Command } from 'commander'; import { version } from './version'; import chalk from 'chalk'; @@ -24,46 +29,81 @@ import { exitWithError } from './errors'; import { ForwardedError } from '@backstage/errors'; import { isPromise } from 'node:util/types'; -function isNodeHidden( - node: - | { $$type: '@tree/leaf'; command: BackstageCommand } - | { $$type: '@tree/root'; children: unknown[] }, -): boolean { - if (node.$$type === '@tree/leaf') { - return !!node.command.deprecated || !!node.command.experimental; - } - return node.children.every(child => isNodeHidden(child as any)); -} +type UninitializedFeature = + | CliModule + | CliModule[] + | Promise<{ default: CliModule | CliModule[] }>; -type UninitializedFeature = CliFeature | Promise<{ default: CliFeature }>; +interface TaggedFeature { + feature: CliModule; + /** + * Whether this module was sourced from an array (e.g. cli-defaults). + * Array-sourced modules are silently skipped when any of their commands + * overlap with an individually-added module, allowing explicit module + * additions to take precedence without causing conflicts. + */ + fromArray: boolean; +} export class CliInitializer { private graph = new CommandGraph(); - private commandRegistry = new CommandRegistry(this.graph); - #uninitiazedFeatures: Promise[] = []; + #uninitiazedFeatures: Promise[] = []; add(feature: UninitializedFeature) { if (isPromise(feature)) { this.#uninitiazedFeatures.push( - feature.then(f => unwrapFeature(f.default)), + feature.then(f => { + const unwrapped = unwrapFeature(f.default); + if (Array.isArray(unwrapped)) { + return unwrapped.map(m => ({ feature: m, fromArray: true })); + } + return [{ feature: unwrapped, fromArray: false }]; + }), + ); + } else if (Array.isArray(feature)) { + this.#uninitiazedFeatures.push( + Promise.resolve(feature.map(m => ({ feature: m, fromArray: true }))), ); } else { - this.#uninitiazedFeatures.push(Promise.resolve(feature)); + this.#uninitiazedFeatures.push( + Promise.resolve([{ feature, fromArray: false }]), + ); } } - async #register(feature: CliFeature) { - if (OpaqueCliPlugin.isType(feature)) { - const internal = OpaqueCliPlugin.toInternal(feature); - await internal.init(this.commandRegistry); + async #register(feature: CliModule) { + if (OpaqueCliModule.isType(feature)) { + for (const command of await OpaqueCliModule.toInternal(feature) + .commands) { + this.graph.add(command, feature); + } } else { throw new Error(`Unsupported feature type: ${(feature as any).$$type}`); } } async #doInit() { - const features = await Promise.all(this.#uninitiazedFeatures); - for (const feature of features) { + const resolvedGroups = await Promise.all(this.#uninitiazedFeatures); + const allFeatures = resolvedGroups.flat(); + + // Collect command paths from individually-added modules + const individualPaths = new Set(); + for (const { feature, fromArray } of allFeatures) { + if (!fromArray && OpaqueCliModule.isType(feature)) { + const cmds = await OpaqueCliModule.toInternal(feature).commands; + for (const cmd of cmds) { + individualPaths.add(cmd.path.join(' ')); + } + } + } + + for (const { feature, fromArray } of allFeatures) { + if (fromArray && OpaqueCliModule.isType(feature)) { + const cmds = await OpaqueCliModule.toInternal(feature).commands; + if (cmds.some(cmd => individualPaths.has(cmd.path.join(' ')))) { + continue; + } + } await this.#register(feature); } } @@ -89,25 +129,28 @@ export class CliInitializer { })); while (queue.length) { const { node, argParser } = queue.shift()!; - if (node.$$type === '@tree/root') { + if (OpaqueCommandTreeNode.isType(node)) { + const internal = OpaqueCommandTreeNode.toInternal(node); const treeParser = argParser - .command(`${node.name} [command]`, { - hidden: isNodeHidden(node), + .command(`${internal.name} [command]`, { + hidden: isCommandNodeHidden(node), }) - .description(node.name); + .description(internal.name); queue.push( - ...node.children.map(child => ({ + ...internal.children.map(child => ({ node: child, argParser: treeParser, })), ); } else { + const internal = OpaqueCommandLeafNode.toInternal(node); argParser - .command(node.name, { - hidden: !!node.command.deprecated || !!node.command.experimental, + .command(internal.name, { + hidden: + !!internal.command.deprecated || !!internal.command.experimental, }) - .description(node.command.description) + .description(internal.command.description) .helpOption(false) .allowUnknownOption(true) .allowExcessArguments(true) @@ -126,7 +169,7 @@ export class CliInitializer { // Skip the command name if ( argIndex === index && - node.command.path[argIndex] === nonProcessArgs[argIndex] + internal.command.path[argIndex] === nonProcessArgs[argIndex] ) { index += 1; continue; @@ -136,15 +179,15 @@ export class CliInitializer { const context = { args: [...positionalArgs, ...args.unknown], info: { - usage: [programName, ...node.command.path].join(' '), - description: node.command.description, + usage: [programName, ...internal.command.path].join(' '), + name: internal.command.path.join(' '), }, }; - if (typeof node.command.execute === 'function') { - await node.command.execute(context); + if (typeof internal.command.execute === 'function') { + await internal.command.execute(context); } else { - const mod = await node.command.execute.loader(); + const mod = await internal.command.execute.loader(); // Handle CJS double-wrapping of default exports const fn = typeof mod.default === 'function' @@ -177,8 +220,12 @@ export class CliInitializer { /** @internal */ export function unwrapFeature( - feature: CliFeature | { default: CliFeature }, -): CliFeature { + feature: CliModule | CliModule[] | { default: CliModule | CliModule[] }, +): CliModule | CliModule[] { + if (Array.isArray(feature)) { + return feature; + } + if ('$$type' in feature) { return feature; } diff --git a/packages/cli/src/wiring/CommandGraph.ts b/packages/cli/src/wiring/CommandGraph.ts index a0a2de3305..b201a3fba3 100644 --- a/packages/cli/src/wiring/CommandGraph.ts +++ b/packages/cli/src/wiring/CommandGraph.ts @@ -13,91 +13,146 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { BackstageCommand } from './types'; - -type Node = TreeNode | LeafNode; - -interface TreeNode { - $$type: '@tree/root'; - name: string; - children: TreeNode[]; -} - -interface LeafNode { - $$type: '@tree/leaf'; - name: string; - command: BackstageCommand; -} +import { + CommandNode, + OpaqueCommandTreeNode, + OpaqueCommandLeafNode, + OpaqueCliModule, +} from '@internal/cli'; +import { CliCommand, CliModule } from './types'; /** * A sparse graph of commands. */ export class CommandGraph { - private graph: Node[] = []; + private graph: CommandNode[] = []; /** * Adds a command to the graph. The graph is sparse, so we use the path to determine the nodes * to traverse. Only leaf nodes should have a command/action. */ - add(command: BackstageCommand) { + add(command: CliCommand, module?: CliModule) { const path = command.path; let current = this.graph; for (let i = 0; i < path.length - 1; i++) { const name = path[i]; - let next = current.find(n => n.name === name); + let next = current.find( + n => + (OpaqueCommandTreeNode.isType(n) && + OpaqueCommandTreeNode.toInternal(n).name === name) || + (OpaqueCommandLeafNode.isType(n) && + OpaqueCommandLeafNode.toInternal(n).name === name), + ); if (!next) { - next = { $$type: '@tree/root', name, children: [] }; + next = OpaqueCommandTreeNode.createInstance('v1', { + name, + children: [], + }); current.push(next); - } else if (next.$$type === '@tree/leaf') { + } else if (OpaqueCommandLeafNode.isType(next)) { throw new Error( - `Command already exists at path: "${path.slice(0, i).join(' ')}"`, + formatConflictError( + path, + module, + OpaqueCommandLeafNode.toInternal(next).module, + ), ); } - current = next.children; + current = OpaqueCommandTreeNode.toInternal(next).children; } - const last = current.find(n => n.name === path[path.length - 1]); - if (last && last.$$type === '@tree/leaf') { + const lastName = path[path.length - 1]; + const last = current.find(n => { + if (OpaqueCommandTreeNode.isType(n)) { + return OpaqueCommandTreeNode.toInternal(n).name === lastName; + } + return OpaqueCommandLeafNode.toInternal(n).name === lastName; + }); + if (last && OpaqueCommandLeafNode.isType(last)) { throw new Error( - `Command already exists at path: "${path.slice(0, -1).join(' ')}"`, + formatConflictError( + path, + module, + OpaqueCommandLeafNode.toInternal(last).module, + ), ); } else { - current.push({ - $$type: '@tree/leaf', - name: path[path.length - 1], - command, - }); + current.push( + OpaqueCommandLeafNode.createInstance('v1', { + name: lastName, + command, + module, + }), + ); } } /** * Given a path, try to find a command that matches it. */ - find(path: string[]): BackstageCommand | undefined { + find(path: string[]): CliCommand | undefined { let current = this.graph; for (let i = 0; i < path.length - 1; i++) { const name = path[i]; - const next = current.find(n => n.name === name); - if (!next) { - return undefined; - } else if (next.$$type === '@tree/leaf') { + const next = current.find(n => { + if (OpaqueCommandTreeNode.isType(n)) { + return OpaqueCommandTreeNode.toInternal(n).name === name; + } + return OpaqueCommandLeafNode.toInternal(n).name === name; + }); + if (!next || OpaqueCommandLeafNode.isType(next)) { return undefined; } - current = next.children; + current = OpaqueCommandTreeNode.toInternal(next).children; } - const last = current.find(n => n.name === path[path.length - 1]); - if (!last || last.$$type === '@tree/root') { + const lastName = path[path.length - 1]; + const last = current.find(n => { + if (OpaqueCommandTreeNode.isType(n)) { + return OpaqueCommandTreeNode.toInternal(n).name === lastName; + } + return OpaqueCommandLeafNode.toInternal(n).name === lastName; + }); + if (!last || OpaqueCommandTreeNode.isType(last)) { return undefined; } - return last?.command; + return OpaqueCommandLeafNode.toInternal(last).command; } - atDepth(depth: number): Node[] { + atDepth(depth: number): CommandNode[] { let current = this.graph; for (let i = 0; i < depth; i++) { current = current.flatMap(n => - n.$$type === '@tree/root' ? n.children : [], + OpaqueCommandTreeNode.isType(n) + ? OpaqueCommandTreeNode.toInternal(n).children + : [], ); } return current; } } + +function getModuleName(module?: CliModule): string | undefined { + if (module && OpaqueCliModule.isType(module)) { + return OpaqueCliModule.toInternal(module).packageName; + } + return undefined; +} + +function formatConflictError( + path: string[], + newModule?: CliModule, + existingModule?: CliModule, +): string { + const cmd = path.join(' '); + const newPkg = getModuleName(newModule); + const existingPkg = getModuleName(existingModule); + if (newPkg && existingPkg) { + return `Command "${cmd}" from "${newPkg}" conflicts with an existing command from "${existingPkg}"`; + } + if (newPkg) { + return `Command "${cmd}" from "${newPkg}" conflicts with an existing command`; + } + if (existingPkg) { + return `Command "${cmd}" conflicts with an existing command from "${existingPkg}"`; + } + return `Command "${cmd}" conflicts with an existing command`; +} diff --git a/packages/cli/src/wiring/CommandRegistry.ts b/packages/cli/src/wiring/CommandRegistry.ts index 9e8ff9646c..c3c1f1956d 100644 --- a/packages/cli/src/wiring/CommandRegistry.ts +++ b/packages/cli/src/wiring/CommandRegistry.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { CommandGraph } from './CommandGraph'; -import { BackstageCommand } from './types'; +import { CliCommand } from './types'; export class CommandRegistry { private graph: CommandGraph; @@ -22,7 +22,7 @@ export class CommandRegistry { this.graph = graph; } - addCommand(command: BackstageCommand) { + addCommand(command: CliCommand) { this.graph.add(command); } } diff --git a/packages/cli/src/wiring/discoverCliModules.ts b/packages/cli/src/wiring/discoverCliModules.ts new file mode 100644 index 0000000000..e2945d2631 --- /dev/null +++ b/packages/cli/src/wiring/discoverCliModules.ts @@ -0,0 +1,69 @@ +/* + * 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 { targetPaths } from '@backstage/cli-common'; +import { PackageRoles } from '@backstage/cli-node'; +import fs from 'node:fs'; +import { resolve as resolvePath } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +/** + * Scans the target project root's package.json for dependencies that are CLI + * modules (packages with `backstage.role === 'cli-module'`). + * + * Returns the resolved entry point paths of discovered CLI module packages, + * or an empty array if none are found or the project root cannot be read. + * The paths are resolved relative to the project root to ensure they can be + * imported regardless of where the CLI code itself is located. + */ +export function discoverCliModules(): string[] { + const rootDir = targetPaths.rootDir; + const pkgJsonPath = resolvePath(rootDir, 'package.json'); + + let projectPkg: { + dependencies?: Record; + devDependencies?: Record; + }; + try { + projectPkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); + } catch { + return []; + } + + const allDeps = { + ...projectPkg.dependencies, + ...projectPkg.devDependencies, + }; + + const modules: string[] = []; + + for (const depName of Object.keys(allDeps)) { + try { + const depPkgPath = require.resolve(`${depName}/package.json`, { + paths: [rootDir], + }); + const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf8')); + if (PackageRoles.getRoleFromPackage(depPkg) === 'cli-module') { + const resolvedPath = require.resolve(depName, { paths: [rootDir] }); + modules.push(pathToFileURL(resolvedPath).href); + } + } catch { + // Skip packages that can't be resolved or read + } + } + + return modules; +} diff --git a/packages/cli/src/wiring/factory.ts b/packages/cli/src/wiring/factory.ts index 6374f37c14..f8f04715c3 100644 --- a/packages/cli/src/wiring/factory.ts +++ b/packages/cli/src/wiring/factory.ts @@ -14,18 +14,4 @@ * limitations under the License. */ -import { describeParentCallSite } from './describeParentCallSite'; -import { BackstageCommand, CliPlugin, OpaqueCliPlugin } from './types'; - -export function createCliPlugin(options: { - pluginId: string; - init: (registry: { - addCommand: (command: BackstageCommand) => void; - }) => Promise; -}): CliPlugin { - return OpaqueCliPlugin.createInstance('v1', { - pluginId: options.pluginId, - init: options.init, - description: `created at '${describeParentCallSite()}'`, - }); -} +export { createCliModule } from '@backstage/cli-node'; diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts index fa3d2341f0..7a8a009a60 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -13,53 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { OpaqueType } from '@internal/opaque'; -export interface CommandContext { - args: string[]; - info: { - /** - * The usage string of the current command, for example: "backstage-cli repo test" - */ - usage: string; - /** - * The description provided for the command - */ - description: string; - }; -} - -export type CommandExecuteFn = (context: CommandContext) => Promise; - -export interface BackstageCommand { - path: string[]; - description: string; - deprecated?: boolean; - experimental?: boolean; - execute: - | CommandExecuteFn - | { - loader: () => Promise<{ default: CommandExecuteFn }>; - }; -} - -export type CliFeature = CliPlugin; - -export interface CliPlugin { - readonly pluginId: string; - readonly $$type: '@backstage/CliPlugin'; -} - -export const OpaqueCliPlugin = OpaqueType.create<{ - public: CliPlugin; - versions: { - readonly version: 'v1'; - readonly description: string; - init: (registry: { - addCommand: (command: BackstageCommand) => void; - }) => Promise; - }; -}>({ - type: '@backstage/CliPlugin', - versions: ['v1'], -}); +export type { + CliCommandContext, + CliCommand, + CliModule, +} from '@backstage/cli-node'; diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index be6852e3ab..6416681b27 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -50,6 +50,7 @@ jest.mock('./versions', () => ({ packageVersions: { root: '1.2.3', '@backstage/cli': '1.0.0', + '@backstage/cli-defaults': '1.0.0', '@backstage/backend-defaults': '1.0.0', '@backstage/backend-tasks': '1.0.0', '@backstage/catalog-model': '1.0.0', diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index db865a2db5..006dd6d7b8 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -38,6 +38,7 @@ import { version as backendDefaults } from '../../../backend-defaults/package.js import { version as catalogClient } from '../../../catalog-client/package.json'; import { version as catalogModel } from '../../../catalog-model/package.json'; import { version as cli } from '../../../cli/package.json'; +import { version as cliDefaults } from '../../../cli-defaults/package.json'; import { version as config } from '../../../config/package.json'; import { version as coreAppApi } from '../../../core-app-api/package.json'; import { version as coreCompatApi } from '../../../core-compat-api/package.json'; @@ -107,6 +108,7 @@ export const packageVersions = { '@backstage/catalog-client': catalogClient, '@backstage/catalog-model': catalogModel, '@backstage/cli': cli, + '@backstage/cli-defaults': cliDefaults, '@backstage/config': config, '@backstage/core-app-api': coreAppApi, '@backstage/core-compat-api': coreCompatApi, diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 229ed5ffa0..4262e0fc82 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -28,6 +28,7 @@ ], "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", + "@backstage/cli-defaults": "^{{version '@backstage/cli-defaults'}}", "@backstage/e2e-test-utils": "^{{version '@backstage/e2e-test-utils'}}", "@jest/environment-jsdom-abstract": "^30.0.0", "@playwright/test": "^1.32.3", diff --git a/packages/create-app/templates/next-app/package.json.hbs b/packages/create-app/templates/next-app/package.json.hbs index 27d5880c11..49436f5fcf 100644 --- a/packages/create-app/templates/next-app/package.json.hbs +++ b/packages/create-app/templates/next-app/package.json.hbs @@ -50,6 +50,7 @@ ], "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", + "@backstage/cli-defaults": "^{{version '@backstage/cli-defaults'}}", "@backstage/e2e-test-utils": "^{{version '@backstage/e2e-test-utils'}}", "@jest/environment-jsdom-abstract": "^30.0.0", "@playwright/test": "^1.32.3", diff --git a/packages/e2e-test/src/commands/runCommand.ts b/packages/e2e-test/src/commands/runCommand.ts index da39351400..a86e7f303d 100644 --- a/packages/e2e-test/src/commands/runCommand.ts +++ b/packages/e2e-test/src/commands/runCommand.ts @@ -383,9 +383,13 @@ async function createPlugin(options: { try { let stdout = ''; + let stderr = ''; child.stdout?.on('data', (data: Buffer) => { stdout = stdout + data.toString('utf8'); }); + child.stderr?.on('data', (data: Buffer) => { + stderr = stderr + data.toString('utf8'); + }); print('Waiting for plugin create script to be done'); await child.waitForExit(); diff --git a/packages/repo-tools/src/commands/api-reports/api-reports/runApiExtraction.ts b/packages/repo-tools/src/commands/api-reports/api-reports/runApiExtraction.ts index 787a2cb3f4..6091237fae 100644 --- a/packages/repo-tools/src/commands/api-reports/api-reports/runApiExtraction.ts +++ b/packages/repo-tools/src/commands/api-reports/api-reports/runApiExtraction.ts @@ -174,6 +174,7 @@ export async function runApiExtraction({ filename => // https://regex101.com/r/QDZIV0/2 filename !== 'knip-report.md' && + !filename.startsWith('cli-report') && !filename.endsWith('.sql.md') && // this has to temporarily match all old api report formats filename.match(/^.*?(api-)?report(-[^.-]+)?(.*?)\.md$/), diff --git a/packages/repo-tools/src/commands/api-reports/categorizePackageDirs.ts b/packages/repo-tools/src/commands/api-reports/categorizePackageDirs.ts index 973c7b8acb..6fdb890100 100644 --- a/packages/repo-tools/src/commands/api-reports/categorizePackageDirs.ts +++ b/packages/repo-tools/src/commands/api-reports/categorizePackageDirs.ts @@ -54,9 +54,10 @@ export async function categorizePackageDirs(packageDirs: string[]) { if (pkgJson?.backstage?.inline) { return; } - if (role === 'cli') { + if (role === 'cli' || role === 'cli-module') { cliPackageDirs.push(dir); - } else if (role !== 'frontend' && role !== 'backend') { + } + if (role !== 'cli' && role !== 'frontend' && role !== 'backend') { tsPackageDirs.push(dir); } } diff --git a/packages/repo-tools/src/commands/api-reports/cli-reports/runCliExtraction.ts b/packages/repo-tools/src/commands/api-reports/cli-reports/runCliExtraction.ts index ae49c95f39..ce207489a7 100644 --- a/packages/repo-tools/src/commands/api-reports/cli-reports/runCliExtraction.ts +++ b/packages/repo-tools/src/commands/api-reports/cli-reports/runCliExtraction.ts @@ -132,7 +132,12 @@ export async function runCliExtraction({ const pkgJson = await fs.readJson(resolvePath(fullDir, 'package.json')); if (!pkgJson.bin) { - throw new Error(`CLI Package in ${packageDir} has no bin field`); + if (pkgJson.backstage?.role === 'cli') { + throw new Error( + `CLI package ${pkgJson.name} is missing a "bin" field in its package.json`, + ); + } + continue; } const models = new Array(); diff --git a/yarn.lock b/yarn.lock index 32b09cc5e3..90ac7fa1c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2801,6 +2801,332 @@ __metadata: languageName: unknown linkType: soft +"@backstage/cli-defaults@workspace:*, @backstage/cli-defaults@workspace:^, @backstage/cli-defaults@workspace:packages/cli-defaults": + version: 0.0.0-use.local + resolution: "@backstage/cli-defaults@workspace:packages/cli-defaults" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-module-auth": "workspace:^" + "@backstage/cli-module-build": "workspace:^" + "@backstage/cli-module-config": "workspace:^" + "@backstage/cli-module-github": "workspace:^" + "@backstage/cli-module-info": "workspace:^" + "@backstage/cli-module-lint": "workspace:^" + "@backstage/cli-module-maintenance": "workspace:^" + "@backstage/cli-module-migrate": "workspace:^" + "@backstage/cli-module-new": "workspace:^" + "@backstage/cli-module-test-jest": "workspace:^" + "@backstage/cli-module-translations": "workspace:^" + languageName: unknown + linkType: soft + +"@backstage/cli-module-auth@workspace:^, @backstage/cli-module-auth@workspace:packages/cli-module-auth": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-auth@workspace:packages/cli-module-auth" + dependencies: + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@backstage/errors": "workspace:^" + "@types/fs-extra": "npm:^11.0.0" + "@types/proper-lockfile": "npm:^4" + cleye: "npm:^2.3.0" + cross-fetch: "npm:^4.0.0" + fs-extra: "npm:^11.2.0" + glob: "npm:^7.1.7" + inquirer: "npm:^8.2.0" + keytar: "npm:^7.9.0" + proper-lockfile: "npm:^4.1.2" + yaml: "npm:^2.0.0" + zod: "npm:^3.25.76" + dependenciesMeta: + keytar: + optional: true + bin: + cli-module-auth: bin/backstage-cli-module-auth + languageName: unknown + linkType: soft + +"@backstage/cli-module-build@workspace:^, @backstage/cli-module-build@workspace:packages/cli-module-build": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-build@workspace:packages/cli-module-build" + dependencies: + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/config-loader": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/module-federation-common": "workspace:^" + "@manypkg/get-packages": "npm:^1.1.3" + "@module-federation/enhanced": "npm:^0.21.6" + "@pmmmwh/react-refresh-webpack-plugin": "npm:^0.6.0" + "@rollup/plugin-commonjs": "npm:^26.0.0" + "@rollup/plugin-json": "npm:^6.0.0" + "@rollup/plugin-node-resolve": "npm:^15.0.0" + "@rollup/plugin-yaml": "npm:^4.0.0" + "@rspack/core": "npm:^1.4.11" + "@rspack/dev-server": "npm:^1.1.4" + "@rspack/plugin-react-refresh": "npm:^1.4.3" + "@swc/core": "npm:^1.15.6" + "@types/fs-extra": "npm:^11.0.0" + "@types/lodash": "npm:^4.14.151" + "@types/npm-packlist": "npm:^3.0.0" + "@types/shell-quote": "npm:^1.7.5" + bfj: "npm:^9.0.2" + buffer: "npm:^6.0.3" + chalk: "npm:^4.0.0" + chokidar: "npm:^3.3.1" + cleye: "npm:^2.3.0" + cross-spawn: "npm:^7.0.3" + css-loader: "npm:^6.5.1" + ctrlc-windows: "npm:^2.1.0" + esbuild-loader: "npm:^4.0.0" + eslint-rspack-plugin: "npm:^4.2.1" + eslint-webpack-plugin: "npm:^4.2.0" + fork-ts-checker-webpack-plugin: "npm:^9.0.0" + fs-extra: "npm:^11.2.0" + glob: "npm:^7.1.7" + html-webpack-plugin: "npm:^5.6.3" + lodash: "npm:^4.17.21" + mini-css-extract-plugin: "npm:^2.4.2" + node-stdlib-browser: "npm:^1.3.1" + npm-packlist: "npm:^5.0.0" + p-queue: "npm:^6.6.2" + postcss: "npm:^8.1.0" + postcss-import: "npm:^16.1.0" + process: "npm:^0.11.10" + raw-loader: "npm:^4.0.2" + react-dev-utils: "npm:^12.0.0-next.60" + react-refresh: "npm:^0.18.0" + rollup: "npm:^4.27.3" + rollup-plugin-dts: "npm:^6.1.0" + rollup-plugin-esbuild: "npm:^6.1.1" + rollup-plugin-postcss: "npm:^4.0.0" + rollup-pluginutils: "npm:^2.8.2" + shell-quote: "npm:^1.8.1" + style-loader: "npm:^3.3.1" + swc-loader: "npm:^0.2.3" + tar: "npm:^7.5.6" + ts-checker-rspack-plugin: "npm:^1.1.5" + ts-morph: "npm:^24.0.0" + util: "npm:^0.12.3" + webpack: "npm:~5.105.0" + webpack-dev-server: "npm:^5.0.0" + yml-loader: "npm:^2.1.0" + yn: "npm:^4.0.0" + bin: + cli-module-build: bin/backstage-cli-module-build + languageName: unknown + linkType: soft + +"@backstage/cli-module-config@workspace:^, @backstage/cli-module-config@workspace:packages/cli-module-config": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-config@workspace:packages/cli-module-config" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/config-loader": "workspace:^" + "@backstage/types": "workspace:^" + "@manypkg/get-packages": "npm:^1.1.3" + "@types/json-schema": "npm:^7.0.6" + chalk: "npm:^4.0.0" + cleye: "npm:^2.3.0" + json-schema: "npm:^0.4.0" + react-dev-utils: "npm:^12.0.0-next.60" + yaml: "npm:^2.0.0" + bin: + cli-module-config: bin/backstage-cli-module-config + languageName: unknown + linkType: soft + +"@backstage/cli-module-github@workspace:^, @backstage/cli-module-github@workspace:packages/cli-module-github": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-github@workspace:packages/cli-module-github" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@octokit/request": "npm:^8.0.0" + "@types/express": "npm:^4.17.6" + "@types/fs-extra": "npm:^11.0.0" + chalk: "npm:^4.0.0" + cleye: "npm:^2.3.0" + express: "npm:^4.22.0" + fs-extra: "npm:^11.2.0" + inquirer: "npm:^8.2.0" + react-dev-utils: "npm:^12.0.0-next.60" + yaml: "npm:^2.0.0" + bin: + cli-module-github: bin/backstage-cli-module-github + languageName: unknown + linkType: soft + +"@backstage/cli-module-info@workspace:^, @backstage/cli-module-info@workspace:packages/cli-module-info": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-info@workspace:packages/cli-module-info" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@types/fs-extra": "npm:^11.0.0" + cleye: "npm:^2.3.0" + fs-extra: "npm:^11.2.0" + minimatch: "npm:^10.2.1" + bin: + cli-module-info: bin/backstage-cli-module-info + languageName: unknown + linkType: soft + +"@backstage/cli-module-lint@workspace:^, @backstage/cli-module-lint@workspace:packages/cli-module-lint": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-lint@workspace:packages/cli-module-lint" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@types/fs-extra": "npm:^11.0.0" + "@types/shell-quote": "npm:^1.7.5" + chalk: "npm:^4.0.0" + cleye: "npm:^2.3.0" + eslint: "npm:^8.6.0" + eslint-formatter-friendly: "npm:^7.0.0" + fs-extra: "npm:^11.2.0" + globby: "npm:^11.1.0" + shell-quote: "npm:^1.8.1" + bin: + cli-module-lint: bin/backstage-cli-module-lint + languageName: unknown + linkType: soft + +"@backstage/cli-module-maintenance@workspace:^, @backstage/cli-module-maintenance@workspace:packages/cli-module-maintenance": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-maintenance@workspace:packages/cli-module-maintenance" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@types/fs-extra": "npm:^11.0.0" + chalk: "npm:^4.0.0" + cleye: "npm:^2.3.0" + eslint: "npm:^8.6.0" + fs-extra: "npm:^11.2.0" + bin: + cli-module-maintenance: bin/backstage-cli-module-maintenance + languageName: unknown + linkType: soft + +"@backstage/cli-module-migrate@workspace:^, @backstage/cli-module-migrate@workspace:packages/cli-module-migrate": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-migrate@workspace:packages/cli-module-migrate" + dependencies: + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/release-manifests": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@manypkg/get-packages": "npm:^1.1.3" + "@types/fs-extra": "npm:^11.0.0" + "@types/semver": "npm:^7" + chalk: "npm:^4.0.0" + cleye: "npm:^2.3.0" + fs-extra: "npm:^11.2.0" + minimatch: "npm:^10.2.1" + msw: "npm:^1.0.0" + ora: "npm:^5.3.0" + replace-in-file: "npm:^7.1.0" + semver: "npm:^7.5.3" + bin: + cli-module-migrate: bin/backstage-cli-module-migrate + languageName: unknown + linkType: soft + +"@backstage/cli-module-new@workspace:^, @backstage/cli-module-new@workspace:packages/cli-module-new": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-new@workspace:packages/cli-module-new" + dependencies: + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@types/fs-extra": "npm:^11.0.0" + "@types/inquirer": "npm:^8.1.3" + "@types/lodash": "npm:^4.14.151" + "@types/recursive-readdir": "npm:^2.2.0" + chalk: "npm:^4.0.0" + cleye: "npm:^2.3.0" + fs-extra: "npm:^11.2.0" + handlebars: "npm:^4.7.3" + inquirer: "npm:^8.2.0" + lodash: "npm:^4.17.21" + ora: "npm:^5.3.0" + recursive-readdir: "npm:^2.2.2" + semver: "npm:^7.5.3" + yaml: "npm:^2.0.0" + zod: "npm:^3.25.76" + zod-validation-error: "npm:^4.0.2" + bin: + cli-module-new: bin/backstage-cli-module-new + languageName: unknown + linkType: soft + +"@backstage/cli-module-test-jest@workspace:^, @backstage/cli-module-test-jest@workspace:packages/cli-module-test-jest": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-test-jest@workspace:packages/cli-module-test-jest" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@swc/core": "npm:^1.15.6" + "@swc/jest": "npm:^0.2.39" + cleye: "npm:^2.3.0" + cross-fetch: "npm:^4.0.0" + fs-extra: "npm:^11.2.0" + glob: "npm:^7.1.7" + jest-css-modules: "npm:^2.1.0" + sucrase: "npm:^3.20.2" + yargs: "npm:^16.2.0" + peerDependencies: + "@jest/environment-jsdom-abstract": ^30.0.0 + jest: ^29.0.0 || ^30.0.0 + jest-environment-jsdom: "*" + jsdom: ^27.1.0 + peerDependenciesMeta: + "@jest/environment-jsdom-abstract": + optional: true + jest-environment-jsdom: + optional: true + jsdom: + optional: true + bin: + cli-module-test-jest: bin/backstage-cli-module-test-jest + languageName: unknown + linkType: soft + +"@backstage/cli-module-translations@workspace:^, @backstage/cli-module-translations@workspace:packages/cli-module-translations": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-translations@workspace:packages/cli-module-translations" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@types/fs-extra": "npm:^11.0.0" + cleye: "npm:^2.3.0" + fs-extra: "npm:^11.2.0" + ts-morph: "npm:^24.0.0" + bin: + cli-module-translations: bin/backstage-cli-module-translations + languageName: unknown + linkType: soft + "@backstage/cli-node@workspace:^, @backstage/cli-node@workspace:packages/cli-node": version: 0.0.0-use.local resolution: "@backstage/cli-node@workspace:packages/cli-node" @@ -2815,10 +3141,18 @@ __metadata: "@types/yarnpkg__lockfile": "npm:^1.1.4" "@yarnpkg/lockfile": "npm:^1.1.0" "@yarnpkg/parsers": "npm:^3.0.0" + chalk: "npm:^4.0.0" + commander: "npm:^12.0.0" fs-extra: "npm:^11.2.0" + pirates: "npm:^4.0.6" semver: "npm:^7.5.3" yaml: "npm:^2.0.0" zod: "npm:^3.25.76" + peerDependencies: + "@swc/core": ^1.15.6 + peerDependenciesMeta: + "@swc/core": + optional: true languageName: unknown linkType: soft @@ -2829,82 +3163,43 @@ __metadata: "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" - "@backstage/catalog-model": "workspace:^" "@backstage/cli-common": "workspace:^" + "@backstage/cli-defaults": "workspace:^" + "@backstage/cli-module-build": "workspace:^" + "@backstage/cli-module-test-jest": "workspace:^" "@backstage/cli-node": "workspace:^" "@backstage/config": "workspace:^" - "@backstage/config-loader": "workspace:^" "@backstage/core-app-api": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/eslint-plugin": "workspace:^" - "@backstage/integration": "workspace:^" - "@backstage/module-federation-common": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" - "@backstage/release-manifests": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" - "@backstage/types": "workspace:^" "@jest/environment-jsdom-abstract": "npm:^30.0.0" "@manypkg/get-packages": "npm:^1.1.3" - "@module-federation/enhanced": "npm:^0.21.6" - "@octokit/request": "npm:^8.0.0" - "@pmmmwh/react-refresh-webpack-plugin": "npm:^0.6.0" - "@rollup/plugin-commonjs": "npm:^26.0.0" - "@rollup/plugin-json": "npm:^6.0.0" - "@rollup/plugin-node-resolve": "npm:^15.0.0" - "@rollup/plugin-yaml": "npm:^4.0.0" - "@rspack/core": "npm:^1.4.11" - "@rspack/dev-server": "npm:^1.1.4" - "@rspack/plugin-react-refresh": "npm:^1.4.3" "@spotify/eslint-config-base": "npm:^15.0.0" "@spotify/eslint-config-react": "npm:^15.0.0" "@spotify/eslint-config-typescript": "npm:^15.0.0" "@swc/core": "npm:^1.15.6" - "@swc/helpers": "npm:^0.5.17" "@swc/jest": "npm:^0.2.39" - "@types/cross-spawn": "npm:^6.0.2" - "@types/ejs": "npm:^3.1.3" - "@types/express": "npm:^4.17.6" "@types/fs-extra": "npm:^11.0.0" - "@types/http-proxy": "npm:^1.17.4" - "@types/inquirer": "npm:^8.1.3" "@types/jest": "npm:^30.0.0" "@types/node": "npm:^22.13.14" - "@types/npm-packlist": "npm:^3.0.0" - "@types/proper-lockfile": "npm:^4" - "@types/recursive-readdir": "npm:^2.2.0" - "@types/rollup-plugin-peer-deps-external": "npm:^2.2.0" - "@types/rollup-plugin-postcss": "npm:^3.1.4" - "@types/shell-quote": "npm:^1.7.5" - "@types/svgo": "npm:^2.6.2" - "@types/terser-webpack-plugin": "npm:^5.0.4" "@types/webpack-env": "npm:^1.15.2" - "@types/webpack-sources": "npm:^3.2.3" "@typescript-eslint/eslint-plugin": "npm:^8.17.0" "@typescript-eslint/parser": "npm:^8.16.0" - bfj: "npm:^9.0.2" - buffer: "npm:^6.0.3" chalk: "npm:^4.0.0" - chokidar: "npm:^3.3.1" - cleye: "npm:^2.3.0" commander: "npm:^14.0.3" cross-fetch: "npm:^4.0.0" - cross-spawn: "npm:^7.0.3" - css-loader: "npm:^6.5.1" - ctrlc-windows: "npm:^2.1.0" - del: "npm:^8.0.0" - esbuild: "npm:^0.27.0" - esbuild-loader: "npm:^4.0.0" eslint: "npm:^8.6.0" eslint-config-prettier: "npm:^9.0.0" - eslint-formatter-friendly: "npm:^7.0.0" eslint-plugin-deprecation: "npm:^3.0.0" eslint-plugin-import: "npm:^2.31.0" eslint-plugin-jest: "npm:^28.9.0" @@ -2912,108 +3207,28 @@ __metadata: eslint-plugin-react: "npm:^7.37.2" eslint-plugin-react-hooks: "npm:^5.0.0" eslint-plugin-unused-imports: "npm:^4.1.4" - eslint-rspack-plugin: "npm:^4.2.1" - eslint-webpack-plugin: "npm:^4.2.0" - express: "npm:^4.22.0" - fork-ts-checker-webpack-plugin: "npm:^9.0.0" fs-extra: "npm:^11.2.0" - git-url-parse: "npm:^15.0.0" glob: "npm:^7.1.7" - global-agent: "npm:^3.0.0" - globby: "npm:^11.1.0" - handlebars: "npm:^4.7.3" - html-webpack-plugin: "npm:^5.6.3" - inquirer: "npm:^8.2.0" jest: "npm:^30.2.0" jest-css-modules: "npm:^2.1.0" jsdom: "npm:^27.1.0" - json-schema: "npm:^0.4.0" - keytar: "npm:^7.9.0" - lodash: "npm:^4.17.21" - mini-css-extract-plugin: "npm:^2.4.2" - minimatch: "npm:^10.2.1" - msw: "npm:^1.0.0" - node-stdlib-browser: "npm:^1.3.1" nodemon: "npm:^3.0.1" - npm-packlist: "npm:^5.0.0" - ora: "npm:^5.3.0" - p-queue: "npm:^6.6.2" pirates: "npm:^4.0.6" postcss: "npm:^8.1.0" - postcss-import: "npm:^16.1.0" - process: "npm:^0.11.10" - proper-lockfile: "npm:^4.1.2" - raw-loader: "npm:^4.0.2" - react-dev-utils: "npm:^12.0.0-next.60" - react-refresh: "npm:^0.18.0" - recursive-readdir: "npm:^2.2.2" - replace-in-file: "npm:^7.1.0" - rollup: "npm:^4.27.3" - rollup-plugin-dts: "npm:^6.1.0" - rollup-plugin-esbuild: "npm:^6.1.1" - rollup-plugin-postcss: "npm:^4.0.0" - rollup-pluginutils: "npm:^2.8.2" - semver: "npm:^7.5.3" - shell-quote: "npm:^1.8.1" - style-loader: "npm:^3.3.1" sucrase: "npm:^3.20.2" - swc-loader: "npm:^0.2.3" - tar: "npm:^7.5.6" - terser-webpack-plugin: "npm:^5.1.3" - ts-checker-rspack-plugin: "npm:^1.1.5" - ts-morph: "npm:^24.0.0" - undici: "npm:^7.2.3" - util: "npm:^0.12.3" - webpack: "npm:~5.105.0" - webpack-dev-server: "npm:^5.0.0" yaml: "npm:^2.0.0" - yargs: "npm:^16.2.0" - yml-loader: "npm:^2.1.0" - yn: "npm:^4.0.0" - zod: "npm:^3.25.76" - zod-validation-error: "npm:^4.0.2" peerDependencies: "@jest/environment-jsdom-abstract": ^30.0.0 - "@module-federation/enhanced": ^0.21.6 - "@pmmmwh/react-refresh-webpack-plugin": ^0.6.0 - esbuild-loader: ^4.0.0 - eslint-webpack-plugin: ^4.2.0 - fork-ts-checker-webpack-plugin: ^9.0.0 jest: ^29.0.0 || ^30.0.0 jest-environment-jsdom: "*" jsdom: ^27.1.0 - mini-css-extract-plugin: ^2.4.2 - terser-webpack-plugin: ^5.1.3 - webpack: ~5.105.0 - webpack-dev-server: ^5.0.0 - dependenciesMeta: - keytar: - optional: true peerDependenciesMeta: "@jest/environment-jsdom-abstract": optional: true - "@module-federation/enhanced": - optional: true - "@pmmmwh/react-refresh-webpack-plugin": - optional: true - esbuild-loader: - optional: true - eslint-webpack-plugin: - optional: true - fork-ts-checker-webpack-plugin: - optional: true jest-environment-jsdom: optional: true jsdom: optional: true - mini-css-extract-plugin: - optional: true - terser-webpack-plugin: - optional: true - webpack: - optional: true - webpack-dev-server: - optional: true bin: backstage-cli: bin/backstage-cli languageName: unknown @@ -9670,6 +9885,15 @@ __metadata: languageName: node linkType: hard +"@internal/cli@workspace:packages/cli-internal": + version: 0.0.0-use.local + resolution: "@internal/cli@workspace:packages/cli-internal" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-node": "workspace:^" + languageName: unknown + linkType: soft + "@internal/frontend@workspace:packages/frontend-internal": version: 0.0.0-use.local resolution: "@internal/frontend@workspace:packages/frontend-internal" @@ -18036,13 +18260,6 @@ __metadata: languageName: node linkType: hard -"@sindresorhus/merge-streams@npm:^2.1.0": - version: 2.3.0 - resolution: "@sindresorhus/merge-streams@npm:2.3.0" - checksum: 10/798bcb53cd1ace9df84fcdd1ba86afdc9e0cd84f5758d26ae9b1eefd8e8887e5fc30051132b9e74daf01bb41fa5a2faf1369361f83d76a3b3d7ee938058fd71c - languageName: node - linkType: hard - "@sinonjs/commons@npm:^2.0.0": version: 2.0.0 resolution: "@sinonjs/commons@npm:2.0.0" @@ -20339,7 +20556,7 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:^0.5.0, @swc/helpers@npm:^0.5.17, @swc/helpers@npm:^0.5.8": +"@swc/helpers@npm:^0.5.0, @swc/helpers@npm:^0.5.8": version: 0.5.19 resolution: "@swc/helpers@npm:0.5.19" dependencies: @@ -21143,13 +21360,6 @@ __metadata: languageName: node linkType: hard -"@types/ejs@npm:^3.1.3": - version: 3.1.5 - resolution: "@types/ejs@npm:3.1.5" - checksum: 10/918898fd279108087722c1713e2ddb0c152ab839397946d164db8a18b5bbd732af9746373882a9bcf4843d35c6b191a8f569a7a4e51e90726d24501b39f40367 - languageName: node - linkType: hard - "@types/emscripten@npm:^1.39.6": version: 1.39.10 resolution: "@types/emscripten@npm:1.39.10" @@ -22222,24 +22432,6 @@ __metadata: languageName: node linkType: hard -"@types/rollup-plugin-peer-deps-external@npm:^2.2.0": - version: 2.2.6 - resolution: "@types/rollup-plugin-peer-deps-external@npm:2.2.6" - peerDependencies: - rollup: "*" - checksum: 10/9fcee30d60d9d8de0f4bbf7694c254a18798b0ff415563a60fb2ed8c6c46423c64e1a977e0344b8393c26221dbd6e74d9e872c2694b518fcf149ba85d94e7eac - languageName: node - linkType: hard - -"@types/rollup-plugin-postcss@npm:^3.1.4": - version: 3.1.4 - resolution: "@types/rollup-plugin-postcss@npm:3.1.4" - dependencies: - rollup-plugin-postcss: "npm:*" - checksum: 10/a94119c43db77ad10a96f34a4190b678e87fe63bdedfb2b989b64a9c0e15680c034d728daa4ade9cfd31d2fdc48827c79ff69ad2599d0b323c2c321896866cf5 - languageName: node - linkType: hard - "@types/sarif@npm:^2.1.4": version: 2.1.5 resolution: "@types/sarif@npm:2.1.5" @@ -22254,10 +22446,10 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7.1.0": - version: 7.7.0 - resolution: "@types/semver@npm:7.7.0" - checksum: 10/ee4514c6c852b1c38f951239db02f9edeea39f5310fad9396a00b51efa2a2d96b3dfca1ae84c88181ea5b7157c57d32d7ef94edacee36fbf975546396b85ba5b +"@types/semver@npm:^7, @types/semver@npm:^7.1.0": + version: 7.7.1 + resolution: "@types/semver@npm:7.7.1" + checksum: 10/8f09e7e6ca3ded67d78ba7a8f7535c8d9cf8ced83c52e7f3ac3c281fe8c689c3fe475d199d94390dc04fc681d51f2358b430bb7b2e21c62de24f2bee2c719068 languageName: node linkType: hard @@ -22341,13 +22533,6 @@ __metadata: languageName: node linkType: hard -"@types/source-list-map@npm:*": - version: 0.1.6 - resolution: "@types/source-list-map@npm:0.1.6" - checksum: 10/9cd294c121f1562062de5d241fe4d10780b1131b01c57434845fe50968e9dcf67ede444591c2b1ad6d3f9b6bc646ac02cc8f51a3577c795f9c64cf4573dcc6b1 - languageName: node - linkType: hard - "@types/ssh2-streams@npm:*": version: 0.1.8 resolution: "@types/ssh2-streams@npm:0.1.8" @@ -22418,15 +22603,6 @@ __metadata: languageName: node linkType: hard -"@types/svgo@npm:^2.6.2": - version: 2.6.4 - resolution: "@types/svgo@npm:2.6.4" - dependencies: - "@types/node": "npm:*" - checksum: 10/9632b350949677fa68d6f13b4d45495a4af3108bb5f020a7257ae5a883e20b9efc0fada3bc3e012f215be312fabe5a28485fffaff5afd6da4daa8cb4fe5b04a2 - languageName: node - linkType: hard - "@types/swagger-ui-react@npm:^5.0.0": version: 5.18.0 resolution: "@types/swagger-ui-react@npm:5.18.0" @@ -22454,15 +22630,6 @@ __metadata: languageName: node linkType: hard -"@types/terser-webpack-plugin@npm:^5.0.4": - version: 5.2.0 - resolution: "@types/terser-webpack-plugin@npm:5.2.0" - dependencies: - terser-webpack-plugin: "npm:*" - checksum: 10/475b0f160c9f83641255f6516b69d7908b9e3e8e8ab653f3a257690249f9614f9386c0897eba6e4e35182ec0d83f57454d62fb94b38ae7c171891641350072ee - languageName: node - linkType: hard - "@types/through@npm:*": version: 0.0.30 resolution: "@types/through@npm:0.0.30" @@ -22545,17 +22712,6 @@ __metadata: languageName: node linkType: hard -"@types/webpack-sources@npm:^3.2.3": - version: 3.2.3 - resolution: "@types/webpack-sources@npm:3.2.3" - dependencies: - "@types/node": "npm:*" - "@types/source-list-map": "npm:*" - source-map: "npm:^0.7.3" - checksum: 10/7b557f242efaa10e4e3e18cc4171a0c98e22898570caefdd4f7b076fe8534b5abfac92c953c6604658dcb7218507f970230352511840fe9fdea31a9af3b9a906 - languageName: node - linkType: hard - "@types/webpack@npm:^5.28.0": version: 5.28.5 resolution: "@types/webpack@npm:5.28.5" @@ -27262,7 +27418,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^12.1.0": +"commander@npm:^12.0.0, commander@npm:^12.1.0": version: 12.1.0 resolution: "commander@npm:12.1.0" checksum: 10/cdaeb672d979816853a4eed7f1310a9319e8b976172485c2a6b437ed0db0a389a44cfb222bfbde772781efa9f215bdd1b936f80d6b249485b465c6cb906e1f93 @@ -28743,21 +28899,6 @@ __metadata: languageName: node linkType: hard -"del@npm:^8.0.0": - version: 8.0.1 - resolution: "del@npm:8.0.1" - dependencies: - globby: "npm:^14.0.2" - is-glob: "npm:^4.0.3" - is-path-cwd: "npm:^3.0.0" - is-path-inside: "npm:^4.0.0" - p-map: "npm:^7.0.2" - presentable-error: "npm:^0.0.1" - slash: "npm:^5.1.0" - checksum: 10/53ed4a379a68c90e7d6d3bcce09c49229e77de9a946d0a5fc25f45b16c950cb8665986b7d0d0423416c03bfd43e0f31e528c5a19c558fe47449be9d6fae7f846 - languageName: node - linkType: hard - "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -32706,20 +32847,6 @@ __metadata: languageName: node linkType: hard -"globby@npm:^14.0.2": - version: 14.0.2 - resolution: "globby@npm:14.0.2" - dependencies: - "@sindresorhus/merge-streams": "npm:^2.1.0" - fast-glob: "npm:^3.3.2" - ignore: "npm:^5.2.4" - path-type: "npm:^5.0.0" - slash: "npm:^5.1.0" - unicorn-magic: "npm:^0.1.0" - checksum: 10/67660da70fc1223f7170c1a62ba6c373385e9e39765d952b6518606dec15ed8c7958e9dae6ba5752a31dbc1e9126f146938b830ad680fe794141734ffc3fbb75 - languageName: node - linkType: hard - "globrex@npm:^0.1.2": version: 0.1.2 resolution: "globrex@npm:0.1.2" @@ -33981,7 +34108,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.1.4, ignore@npm:^5.2.0, ignore@npm:^5.2.4": +"ignore@npm:^5.1.4, ignore@npm:^5.2.0": version: 5.3.2 resolution: "ignore@npm:5.3.2" checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98 @@ -34790,13 +34917,6 @@ __metadata: languageName: node linkType: hard -"is-path-cwd@npm:^3.0.0": - version: 3.0.0 - resolution: "is-path-cwd@npm:3.0.0" - checksum: 10/bc34d13b6a03dfca4a3ab6a8a5ba78ae4b24f4f1db4b2b031d2760c60d0913bd16a4b980dcb4e590adfc906649d5f5132684079a3972bd219da49deebb9adea8 - languageName: node - linkType: hard - "is-path-inside@npm:^3.0.2, is-path-inside@npm:^3.0.3": version: 3.0.3 resolution: "is-path-inside@npm:3.0.3" @@ -34804,13 +34924,6 @@ __metadata: languageName: node linkType: hard -"is-path-inside@npm:^4.0.0": - version: 4.0.0 - resolution: "is-path-inside@npm:4.0.0" - checksum: 10/8810fa11c58e6360b82c3e0d6cd7d9c7d0392d3ac9eb10f980b81f9839f40ac6d1d6d6f05d069db0d227759801228f0b072e1b6c343e4469b065ab5fe0b68fe5 - languageName: node - linkType: hard - "is-plain-obj@npm:^3.0.0": version: 3.0.0 resolution: "is-plain-obj@npm:3.0.0" @@ -38817,14 +38930,14 @@ __metadata: linkType: hard "mini-css-extract-plugin@npm:^2.4.2": - version: 2.10.0 - resolution: "mini-css-extract-plugin@npm:2.10.0" + version: 2.10.1 + resolution: "mini-css-extract-plugin@npm:2.10.1" dependencies: schema-utils: "npm:^4.0.0" tapable: "npm:^2.2.1" peerDependencies: webpack: ^5.0.0 - checksum: 10/bae5350ab82171c6c9a22a4397df14aa69280f5ff0e1ff4d2429ea841bc096927b1e27ba7b75a9c3dd77bd44bab449d6197bd748381f1326cbc8befcb10d1a9e + checksum: 10/2d0cecc3bea85cd7f9b1ce0974f1672976d610a9267e2988ff19f5d03b017bff12b32151a412de0f519a70be7d3b050b499b20101445fb21728cc2d35dd4041a languageName: node linkType: hard @@ -41563,13 +41676,6 @@ __metadata: languageName: node linkType: hard -"path-type@npm:^5.0.0": - version: 5.0.0 - resolution: "path-type@npm:5.0.0" - checksum: 10/15ec24050e8932c2c98d085b72cfa0d6b4eeb4cbde151a0a05726d8afae85784fc5544f733d8dfc68536587d5143d29c0bd793623fad03d7e61cc00067291cd5 - languageName: node - linkType: hard - "pathe@npm:^2.0.3": version: 2.0.3 resolution: "pathe@npm:2.0.3" @@ -42595,13 +42701,6 @@ __metadata: languageName: node linkType: hard -"presentable-error@npm:^0.0.1": - version: 0.0.1 - resolution: "presentable-error@npm:0.0.1" - checksum: 10/013809ee7a47ced847a8d860e9b89a56cdd8c4f1ad04ad8da1e58fd60843f77f497d204146bb15aaa9793d3b94ad8626eed01256fc9eb5839a545af2000a5fa4 - languageName: node - linkType: hard - "prettier@npm:^2.2.1, prettier@npm:^2.7.1": version: 2.8.8 resolution: "prettier@npm:2.8.8" @@ -45126,7 +45225,7 @@ __metadata: languageName: node linkType: hard -"rollup-plugin-postcss@npm:*, rollup-plugin-postcss@npm:^4.0.0": +"rollup-plugin-postcss@npm:^4.0.0": version: 4.0.2 resolution: "rollup-plugin-postcss@npm:4.0.2" dependencies: @@ -45271,6 +45370,7 @@ __metadata: resolution: "root@workspace:." dependencies: "@backstage/cli": "workspace:*" + "@backstage/cli-defaults": "workspace:*" "@backstage/codemods": "workspace:*" "@backstage/create-app": "workspace:*" "@backstage/e2e-test-utils": "workspace:*" @@ -46226,13 +46326,6 @@ __metadata: languageName: node linkType: hard -"slash@npm:^5.1.0": - version: 5.1.0 - resolution: "slash@npm:5.1.0" - checksum: 10/2c41ec6fb1414cd9bba0fa6b1dd00e8be739e3fe85d079c69d4b09ca5f2f86eafd18d9ce611c0c0f686428638a36c272a6ac14799146a8295f259c10cc45cde4 - languageName: node - linkType: hard - "slice-ansi@npm:^3.0.0": version: 3.0.0 resolution: "slice-ansi@npm:3.0.0" @@ -47831,7 +47924,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:*, terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.17": +"terser-webpack-plugin@npm:^5.3.17": version: 5.3.17 resolution: "terser-webpack-plugin@npm:5.3.17" dependencies: @@ -49097,13 +49190,6 @@ __metadata: languageName: node linkType: hard -"unicorn-magic@npm:^0.1.0": - version: 0.1.0 - resolution: "unicorn-magic@npm:0.1.0" - checksum: 10/9b4d0e9809807823dc91d0920a4a4c0cff2de3ebc54ee87ac1ee9bc75eafd609b09d1f14495e0173aef26e01118706196b6ab06a75fe0841028b3983a8af313f - languageName: node - linkType: hard - "unified@npm:^10.0.0": version: 10.1.0 resolution: "unified@npm:10.1.0"