diff --git a/.changeset/cli-use-cli-plugin-api.md b/.changeset/cli-use-cli-plugin-api.md index c16d0e2c06..b55858ba62 100644 --- a/.changeset/cli-use-cli-plugin-api.md +++ b/.changeset/cli-use-cli-plugin-api.md @@ -2,4 +2,4 @@ '@backstage/cli': patch --- -Migrated CLI plugin modules to import from the new `@backstage/cli-plugin-api` package. +Migrated CLI plugin modules to use `createCliPlugin` from the new `@backstage/cli-plugin-api` package. diff --git a/.changeset/create-cli-plugin-api.md b/.changeset/create-cli-plugin-api.md index 0b758c76dd..d740fe7897 100644 --- a/.changeset/create-cli-plugin-api.md +++ b/.changeset/create-cli-plugin-api.md @@ -2,4 +2,4 @@ '@backstage/cli-plugin-api': minor --- -Added a new `@backstage/cli-plugin-api` package that provides the public API for building Backstage CLI plugins. This includes `createCliPlugin`, `lazy`, `ExitCodeError`, `exitWithError`, and associated types. +Added a new `@backstage/cli-plugin-api` package that provides the `createCliPlugin` API for building Backstage CLI plugins. diff --git a/packages/cli-plugin-api/.eslintrc.js b/packages/cli-plugin-api/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-plugin-api/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-plugin-api/package.json b/packages/cli-plugin-api/package.json index d7fb328943..051b764c6a 100644 --- a/packages/cli-plugin-api/package.json +++ b/packages/cli-plugin-api/package.json @@ -6,9 +6,7 @@ "role": "cli-plugin" }, "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" }, "homepage": "https://backstage.io", "repository": { @@ -17,8 +15,23 @@ "directory": "packages/cli-plugin-api" }, "license": "Apache-2.0", + "exports": { + ".": "./src/index.ts", + "./internals": "./src/internals.ts", + "./package.json": "./package.json" + }, "main": "src/index.ts", "types": "src/index.ts", + "typesVersions": { + "*": { + "internals": [ + "src/internals.ts" + ], + "package.json": [ + "package.json" + ] + } + }, "files": [ "dist" ], @@ -31,8 +44,7 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/errors": "workspace:^", - "chalk": "^4.0.0" + "@backstage/errors": "workspace:^" }, "devDependencies": { "@backstage/cli": "workspace:^" diff --git a/packages/cli-plugin-api/report-internals.api.md b/packages/cli-plugin-api/report-internals.api.md new file mode 100644 index 0000000000..b5721e6972 --- /dev/null +++ b/packages/cli-plugin-api/report-internals.api.md @@ -0,0 +1,60 @@ +## API Report File for "@backstage/cli-plugin-api" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public +export interface BackstageCommand { + // (undocumented) + deprecated?: boolean; + // (undocumented) + description: string; + // (undocumented) + execute: + | CommandExecuteFn + | { + loader: () => Promise<{ + default: CommandExecuteFn; + }>; + }; + // (undocumented) + experimental?: boolean; + // (undocumented) + path: string[]; +} + +// @public +export interface CliPlugin { + // (undocumented) + readonly $$type: '@backstage/CliPlugin'; + // (undocumented) + readonly pluginId: string; +} + +// @public +export interface CommandContext { + // (undocumented) + args: string[]; + // (undocumented) + info: { + usage: string; + description: string; + }; +} + +// @public +export type CommandExecuteFn = (context: CommandContext) => Promise; + +// @public +export function initializeCliPlugin( + plugin: CliPlugin, + registry: { + addCommand: (command: BackstageCommand) => void; + }, +): Promise; + +// @public +export function isCliPlugin(value: unknown): value is CliPlugin; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli-plugin-api/report.api.md b/packages/cli-plugin-api/report.api.md index 7d5b9ed63c..3562bb20e6 100644 --- a/packages/cli-plugin-api/report.api.md +++ b/packages/cli-plugin-api/report.api.md @@ -3,17 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { CustomErrorBase } from '@backstage/errors'; - -// @public -export type ActionExports = { - [KName in keyof TModule as TModule[KName] extends ( - ...args: any[] - ) => Promise - ? KName - : never]: TModule[KName]; -}; - // @public export interface BackstageCommand { // (undocumented) @@ -29,6 +18,8 @@ export interface BackstageCommand { }>; }; // (undocumented) + experimental?: boolean; + // (undocumented) path: string[]; } @@ -65,32 +56,5 @@ export function createCliPlugin(options: { }) => Promise; }): CliPlugin; -// @public -export class ExitCodeError extends CustomErrorBase { - constructor(code: number, command?: string); - // (undocumented) - readonly code: number; -} - -// @public -export function exitWithError(error: unknown): never; - -// @public -export function initializeCliPlugin( - plugin: CliPlugin, - registry: { - addCommand: (command: BackstageCommand) => void; - }, -): Promise; - -// @public -export function isCliPlugin(value: unknown): value is CliPlugin; - -// @public -export function lazy( - moduleLoader: () => Promise, - exportName: keyof ActionExports, -): (...args: any[]) => Promise; - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/cli-plugin-api/src/errors.ts b/packages/cli-plugin-api/src/errors.ts deleted file mode 100644 index 5334345608..0000000000 --- a/packages/cli-plugin-api/src/errors.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { CustomErrorBase, isError, stringifyError } from '@backstage/errors'; -import chalk from 'chalk'; - -/** - * An error that indicates a child process exited with a specific code. - * - * @public - */ -export class ExitCodeError extends CustomErrorBase { - readonly code: number; - - constructor(code: number, command?: string) { - super( - command - ? `Command '${command}' exited with code ${code}` - : `Child exited with code ${code}`, - ); - this.code = code; - } -} - -function exit(message: string, code: number = 1): never { - process.stderr.write(`\n${chalk.red(message)}\n\n`); - process.exit(code); -} - -/** - * Exits the process with an appropriate error code based on the error type. - * - * @public - */ -export function exitWithError(error: unknown): never { - if (!isError(error)) { - process.stderr.write(`\n${chalk.red(stringifyError(error))}\n\n`); - process.exit(1); - } - - switch (error.name) { - case 'InputError': - return exit(error.message, 74 /* input/output error */); - case 'NotFoundError': - return exit(error.message, 127 /* command not found */); - case 'NotImplementedError': - return exit(error.message, 64 /* command line usage error */); - case 'AuthenticationError': - case 'NotAllowedError': - return exit(error.message, 77 /* permission denied */); - case 'ExitCodeError': - return exit( - error.message, - 'code' in error && typeof error.code === 'number' ? error.code : 1, - ); - default: - return exit(stringifyError(error), 1); - } -} diff --git a/packages/cli-plugin-api/src/index.ts b/packages/cli-plugin-api/src/index.ts index be990b7357..15082804ee 100644 --- a/packages/cli-plugin-api/src/index.ts +++ b/packages/cli-plugin-api/src/index.ts @@ -15,9 +15,6 @@ */ export { createCliPlugin } from './createCliPlugin'; -export { lazy } from './lazy'; -export type { ActionExports } from './lazy'; -export { ExitCodeError, exitWithError } from './errors'; export type { BackstageCommand, CommandContext, @@ -25,4 +22,3 @@ export type { CliPlugin, CliFeature, } from './types'; -export { isCliPlugin, initializeCliPlugin } from './internals'; diff --git a/packages/cli-plugin-api/src/internals.ts b/packages/cli-plugin-api/src/internals.ts index 99c1c4d1f8..f851decbad 100644 --- a/packages/cli-plugin-api/src/internals.ts +++ b/packages/cli-plugin-api/src/internals.ts @@ -16,6 +16,13 @@ import { BackstageCommand, CliPlugin, OpaqueCliPlugin } from './types'; +export type { + BackstageCommand, + CliPlugin, + CommandContext, + CommandExecuteFn, +} from './types'; + /** * Checks whether a value is a {@link CliPlugin}. * diff --git a/packages/cli-plugin-api/src/lazy.ts b/packages/cli-plugin-api/src/lazy.ts deleted file mode 100644 index 30b8568fb1..0000000000 --- a/packages/cli-plugin-api/src/lazy.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { assertError } from '@backstage/errors'; -import { exitWithError } from './errors'; - -/** - * A mapped type that extracts keys of a module whose values are async action functions. - * - * @public - */ -export type ActionExports = { - [KName in keyof TModule as TModule[KName] extends ( - ...args: any[] - ) => Promise - ? KName - : never]: TModule[KName]; -}; - -/** - * Wraps an action function so that it always exits and handles errors. - * - * @public - */ -export function lazy( - moduleLoader: () => Promise, - exportName: keyof ActionExports, -): (...args: any[]) => Promise { - return async (...args: any[]) => { - try { - const mod = await moduleLoader(); - const actualModule = ( - mod as unknown as { default: ActionExports } - ).default; - const actionFunc = actualModule[exportName] as ( - ...args: any[] - ) => Promise; - await actionFunc(...args); - - process.exit(0); - } catch (error) { - assertError(error); - exitWithError(error); - } - }; -} diff --git a/packages/cli/src/modules/config/commands/docs.ts b/packages/cli/src/modules/config/commands/docs.ts index 8015947a57..84d2f384e1 100644 --- a/packages/cli/src/modules/config/commands/docs.ts +++ b/packages/cli/src/modules/config/commands/docs.ts @@ -21,7 +21,7 @@ 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 '@backstage/cli-plugin-api'; +import type { CommandContext } from '../../../wiring/types'; const DOCS_URL = 'https://config.backstage.io'; diff --git a/packages/cli/src/modules/config/commands/print.ts b/packages/cli/src/modules/config/commands/print.ts index db681f4619..33f7245604 100644 --- a/packages/cli/src/modules/config/commands/print.ts +++ b/packages/cli/src/modules/config/commands/print.ts @@ -19,7 +19,7 @@ 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 '@backstage/cli-plugin-api'; +import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/config/commands/schema.ts b/packages/cli/src/modules/config/commands/schema.ts index 1635caf7e6..13f651534c 100644 --- a/packages/cli/src/modules/config/commands/schema.ts +++ b/packages/cli/src/modules/config/commands/schema.ts @@ -20,7 +20,7 @@ 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 '@backstage/cli-plugin-api'; +import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/config/commands/validate.ts b/packages/cli/src/modules/config/commands/validate.ts index 302c0b6480..f6cf9cc662 100644 --- a/packages/cli/src/modules/config/commands/validate.ts +++ b/packages/cli/src/modules/config/commands/validate.ts @@ -16,7 +16,7 @@ import { cli } from 'cleye'; import { loadCliConfig } from '../lib/config'; -import type { CommandContext } from '@backstage/cli-plugin-api'; +import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/info/commands/info.ts b/packages/cli/src/modules/info/commands/info.ts index 0170c4204a..ab26714fca 100644 --- a/packages/cli/src/modules/info/commands/info.ts +++ b/packages/cli/src/modules/info/commands/info.ts @@ -25,7 +25,7 @@ import { } from '@backstage/cli-node'; import { minimatch } from 'minimatch'; import fs from 'fs-extra'; -import type { CommandContext } from '@backstage/cli-plugin-api'; +import type { CommandContext } from '../../../wiring/types'; /** * Attempts to read package.json from node_modules for a given package name. diff --git a/packages/cli/src/modules/translations/commands/export.ts b/packages/cli/src/modules/translations/commands/export.ts index 146e9ce121..b6471f10f4 100644 --- a/packages/cli/src/modules/translations/commands/export.ts +++ b/packages/cli/src/modules/translations/commands/export.ts @@ -33,7 +33,7 @@ import { formatMessagePath, validatePattern, } from '../lib/messageFilePath'; -import type { CommandContext } from '@backstage/cli-plugin-api'; +import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/translations/commands/import.ts b/packages/cli/src/modules/translations/commands/import.ts index 2739dbac41..155724dd8c 100644 --- a/packages/cli/src/modules/translations/commands/import.ts +++ b/packages/cli/src/modules/translations/commands/import.ts @@ -28,7 +28,7 @@ import { createMessagePathParser, formatMessagePath, } from '../lib/messageFilePath'; -import type { CommandContext } from '@backstage/cli-plugin-api'; +import type { CommandContext } from '../../../wiring/types'; interface ManifestRefEntry { package: string; diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index b44dba862f..72269f3274 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -18,13 +18,13 @@ import { CommandGraph } from './CommandGraph'; import { isCliPlugin, initializeCliPlugin, - exitWithError, -} from '@backstage/cli-plugin-api'; +} from '@backstage/cli-plugin-api/internals'; import type { BackstageCommand, CliFeature } from '@backstage/cli-plugin-api'; import { CommandRegistry } from './CommandRegistry'; import { Command } from 'commander'; import { version } from './version'; import chalk from 'chalk'; +import { exitWithError } from './errors'; import { ForwardedError } from '@backstage/errors'; import { isPromise } from 'node:util/types'; diff --git a/packages/cli/src/wiring/errors.ts b/packages/cli/src/wiring/errors.ts index b684f5c64a..aee6d3f575 100644 --- a/packages/cli/src/wiring/errors.ts +++ b/packages/cli/src/wiring/errors.ts @@ -14,4 +14,49 @@ * limitations under the License. */ -export { ExitCodeError, exitWithError } from '@backstage/cli-plugin-api'; +import { CustomErrorBase, isError, stringifyError } from '@backstage/errors'; +import chalk from 'chalk'; + +export class ExitCodeError extends CustomErrorBase { + readonly code: number; + + constructor(code: number, command?: string) { + super( + command + ? `Command '${command}' exited with code ${code}` + : `Child exited with code ${code}`, + ); + this.code = code; + } +} + +function exit(message: string, code: number = 1): never { + process.stderr.write(`\n${chalk.red(message)}\n\n`); + process.exit(code); +} + +export function exitWithError(error: unknown): never { + if (!isError(error)) { + process.stderr.write(`\n${chalk.red(stringifyError(error))}\n\n`); + process.exit(1); + } + + switch (error.name) { + case 'InputError': + return exit(error.message, 74 /* input/output error */); + case 'NotFoundError': + return exit(error.message, 127 /* command not found */); + case 'NotImplementedError': + return exit(error.message, 64 /* command line usage error */); + case 'AuthenticationError': + case 'NotAllowedError': + return exit(error.message, 77 /* permissino denied */); + case 'ExitCodeError': + return exit( + error.message, + 'code' in error && typeof error.code === 'number' ? error.code : 1, + ); + default: + return exit(stringifyError(error), 1); + } +} diff --git a/packages/cli/src/wiring/lazy.ts b/packages/cli/src/wiring/lazy.ts index feda134d07..64a4d43ef4 100644 --- a/packages/cli/src/wiring/lazy.ts +++ b/packages/cli/src/wiring/lazy.ts @@ -14,4 +14,34 @@ * limitations under the License. */ -export { lazy } from '@backstage/cli-plugin-api'; +import { assertError } from '@backstage/errors'; +import { exitWithError } from './errors'; + +type ActionFunc = (...args: any[]) => Promise; +type ActionExports = { + [KName in keyof TModule as TModule[KName] extends ActionFunc + ? KName + : never]: TModule[KName]; +}; + +// Wraps an action function so that it always exits and handles errors +export function lazy( + moduleLoader: () => Promise, + exportName: keyof ActionExports, +): (...args: any[]) => Promise { + return async (...args: any[]) => { + try { + const mod = await moduleLoader(); + const actualModule = ( + mod as unknown as { default: ActionExports } + ).default; + const actionFunc = actualModule[exportName] as ActionFunc; + await actionFunc(...args); + + process.exit(0); + } catch (error) { + assertError(error); + exitWithError(error); + } + }; +} diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts index 0cf4f849a9..4fd58a5c73 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -// Re-export types from the plugin API for internal use within the CLI package. export type { CommandContext, CommandExecuteFn, diff --git a/yarn.lock b/yarn.lock index 6977b9161f..2173b5a314 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2828,7 +2828,6 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/errors": "workspace:^" - chalk: "npm:^4.0.0" languageName: unknown linkType: soft