Add lazy loader pattern for CLI command execution
Extend `BackstageCommand.execute` to accept either a direct function or a
`{ loader }` object for lazy loading command implementations. Convert
several build and migrate commands to use the new pattern. Switch from
`program.parse` to `program.parseAsync` to properly await async actions.
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Added support for lazy loading of CLI command implementations through a new loader pattern for `BackstageCommand.execute`.
|
||||
@@ -81,6 +81,7 @@
|
||||
"buffer": "^6.0.3",
|
||||
"chalk": "^4.0.0",
|
||||
"chokidar": "^3.3.1",
|
||||
"cleye": "^2.2.1",
|
||||
"commander": "^14.0.3",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
|
||||
@@ -20,7 +20,7 @@ import { PackageGraph } from '@backstage/cli-node';
|
||||
|
||||
import { run, targetPaths } from '@backstage/cli-common';
|
||||
|
||||
export async function command(): Promise<void> {
|
||||
export default async function command(): Promise<void> {
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
await fs.remove(targetPaths.resolveRoot('dist'));
|
||||
|
||||
@@ -206,52 +206,34 @@ export const buildPlugin = createCliPlugin({
|
||||
reg.addCommand({
|
||||
path: ['package', 'clean'],
|
||||
description: 'Delete cache directories',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/package/clean'), 'default'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: {
|
||||
loader: () => import('./commands/package/clean'),
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['package', 'prepack'],
|
||||
description: 'Prepares a package for packaging before publishing',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/package/pack'), 'pre'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: async () => {
|
||||
const { pre } = await import('./commands/package/pack');
|
||||
await pre();
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['package', 'postpack'],
|
||||
description: 'Restores the changes made by the prepack command',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/package/pack'), 'post'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: async () => {
|
||||
const { post } = await import('./commands/package/pack');
|
||||
await post();
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['repo', 'clean'],
|
||||
description: 'Delete cache and output directories',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/repo/clean'), 'command'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: {
|
||||
loader: () => import('./commands/repo/clean'),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -14,20 +14,38 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { mergeConfigSchemas } from '@backstage/config-loader';
|
||||
import { OptionValues } from 'commander';
|
||||
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';
|
||||
|
||||
const DOCS_URL = 'https://config.backstage.io';
|
||||
|
||||
export default async (opts: OptionValues) => {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { package: pkg },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
package: {
|
||||
type: String,
|
||||
description:
|
||||
'Only include the schema that applies to the given package',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const { schema: appSchemas } = await loadCliConfig({
|
||||
args: [],
|
||||
fromPackage: opts.package,
|
||||
fromPackage: pkg,
|
||||
mockEnv: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -14,36 +14,67 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OptionValues } from 'commander';
|
||||
import { cli } from 'cleye';
|
||||
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';
|
||||
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { config, lax, frontend, withSecrets, format, package: pkg },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
package: { type: String, description: 'Package to print config for' },
|
||||
lax: {
|
||||
type: Boolean,
|
||||
description: 'Do not require environment variables to be set',
|
||||
},
|
||||
frontend: { type: Boolean, description: 'Only print frontend config' },
|
||||
withSecrets: {
|
||||
type: Boolean,
|
||||
description: 'Include secrets in the output',
|
||||
},
|
||||
format: { type: String, description: 'Output format (yaml or json)' },
|
||||
config: {
|
||||
type: [String],
|
||||
description: 'Config files to load instead of app-config.yaml',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
export default async (opts: OptionValues) => {
|
||||
const { schema, appConfigs } = await loadCliConfig({
|
||||
args: opts.config,
|
||||
fromPackage: opts.package,
|
||||
mockEnv: opts.lax,
|
||||
fullVisibility: !opts.frontend,
|
||||
args: config,
|
||||
fromPackage: pkg,
|
||||
mockEnv: lax,
|
||||
fullVisibility: !frontend,
|
||||
});
|
||||
const visibility = getVisibilityOption(opts);
|
||||
const visibility = getVisibilityOption(frontend, withSecrets);
|
||||
const data = serializeConfigData(appConfigs, schema, visibility);
|
||||
|
||||
if (opts.format === 'json') {
|
||||
if (format === 'json') {
|
||||
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
||||
} else {
|
||||
process.stdout.write(`${stringifyYaml(data)}\n`);
|
||||
}
|
||||
};
|
||||
|
||||
function getVisibilityOption(opts: OptionValues): ConfigVisibility {
|
||||
if (opts.frontend && opts.withSecrets) {
|
||||
function getVisibilityOption(
|
||||
frontend: boolean | undefined,
|
||||
withSecrets: boolean | undefined,
|
||||
): ConfigVisibility {
|
||||
if (frontend && withSecrets) {
|
||||
throw new Error('Not allowed to combine frontend and secret config');
|
||||
}
|
||||
if (opts.frontend) {
|
||||
if (frontend) {
|
||||
return 'frontend';
|
||||
} else if (opts.withSecrets) {
|
||||
} else if (withSecrets) {
|
||||
return 'secret';
|
||||
}
|
||||
return 'backend';
|
||||
|
||||
@@ -14,22 +14,41 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OptionValues } from 'commander';
|
||||
import { cli } from 'cleye';
|
||||
import { JSONSchema7 as JSONSchema } from 'json-schema';
|
||||
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';
|
||||
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { merge, format, package: pkg },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
package: { type: String, description: 'Package to print schema for' },
|
||||
format: { type: String, description: 'Output format (yaml or json)' },
|
||||
merge: {
|
||||
type: Boolean,
|
||||
description: 'Merge all schemas into a single schema',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
export default async (opts: OptionValues) => {
|
||||
const { schema } = await loadCliConfig({
|
||||
args: [],
|
||||
fromPackage: opts.package,
|
||||
fromPackage: pkg,
|
||||
mockEnv: true,
|
||||
});
|
||||
|
||||
let configSchema: JsonObject | JSONSchema;
|
||||
if (opts.merge) {
|
||||
if (merge) {
|
||||
configSchema = mergeConfigSchemas(
|
||||
(schema.serialize().schemas as JsonObject[]).map(
|
||||
_ => _.value as JSONSchema,
|
||||
@@ -42,7 +61,7 @@ export default async (opts: OptionValues) => {
|
||||
configSchema = schema.serialize();
|
||||
}
|
||||
|
||||
if (opts.format === 'json') {
|
||||
if (format === 'json') {
|
||||
process.stdout.write(`${JSON.stringify(configSchema, null, 2)}\n`);
|
||||
} else {
|
||||
process.stdout.write(`${stringifyYaml(configSchema)}\n`);
|
||||
|
||||
@@ -14,16 +14,47 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OptionValues } from 'commander';
|
||||
import { cli } from 'cleye';
|
||||
import { loadCliConfig } from '../lib/config';
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { config, lax, frontend, deprecated, strict, package: pkg },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
package: {
|
||||
type: String,
|
||||
description: 'Package to validate config for',
|
||||
},
|
||||
lax: {
|
||||
type: Boolean,
|
||||
description: 'Do not require environment variables to be set',
|
||||
},
|
||||
frontend: {
|
||||
type: Boolean,
|
||||
description: 'Only validate frontend config',
|
||||
},
|
||||
deprecated: { type: Boolean, description: 'Output deprecated keys' },
|
||||
strict: { type: Boolean, description: 'Enable strict validation' },
|
||||
config: {
|
||||
type: [String],
|
||||
description: 'Config files to load instead of app-config.yaml',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
export default async (opts: OptionValues) => {
|
||||
await loadCliConfig({
|
||||
args: opts.config,
|
||||
fromPackage: opts.package,
|
||||
mockEnv: opts.lax,
|
||||
fullVisibility: !opts.frontend,
|
||||
withDeprecatedKeys: opts.deprecated,
|
||||
strict: opts.strict,
|
||||
args: config,
|
||||
fromPackage: pkg,
|
||||
mockEnv: lax,
|
||||
fullVisibility: !frontend,
|
||||
withDeprecatedKeys: deprecated,
|
||||
strict,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import yargs from 'yargs';
|
||||
import { Command } from 'commander';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export const configOption = [
|
||||
'--config <path>',
|
||||
@@ -31,108 +28,33 @@ export default createCliPlugin({
|
||||
reg.addCommand({
|
||||
path: ['config:docs'],
|
||||
description: 'Browse the configuration reference documentation',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command
|
||||
.option(
|
||||
'--package <name>',
|
||||
'Only include the schema that applies to the given package',
|
||||
)
|
||||
.description('Browse the configuration reference documentation')
|
||||
.action(lazy(() => import('./commands/docs'), 'default'));
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/docs') },
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['config', 'docs'],
|
||||
description: 'Browse the configuration reference documentation',
|
||||
execute: async ({ args, info }) => {
|
||||
await new Command(info.usage)
|
||||
.option(
|
||||
'--package <name>',
|
||||
'Only include the schema that applies to the given package',
|
||||
)
|
||||
.description(info.description)
|
||||
.action(lazy(() => import('./commands/docs'), 'default'))
|
||||
.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/docs') },
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['config:print'],
|
||||
description: 'Print the app configuration for the current package',
|
||||
execute: async ({ args, info }) => {
|
||||
const argv = await yargs()
|
||||
.options({
|
||||
package: { type: 'string' },
|
||||
lax: { type: 'boolean' },
|
||||
frontend: { type: 'boolean' },
|
||||
'with-secrets': { type: 'boolean' },
|
||||
format: { type: 'string' },
|
||||
config: { type: 'string', array: true, default: [] },
|
||||
})
|
||||
.usage('$0', info.description)
|
||||
.help()
|
||||
.parse(args);
|
||||
await lazy(() => import('./commands/print'), 'default')(argv);
|
||||
},
|
||||
execute: { loader: () => import('./commands/print') },
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['config:check'],
|
||||
description:
|
||||
'Validate that the given configuration loads and matches schema',
|
||||
execute: async ({ args }) => {
|
||||
const argv = await yargs()
|
||||
.options({
|
||||
package: { type: 'string' },
|
||||
lax: { type: 'boolean' },
|
||||
frontend: { type: 'boolean' },
|
||||
deprecated: { type: 'boolean' },
|
||||
strict: { type: 'boolean' },
|
||||
config: {
|
||||
type: 'string',
|
||||
array: true,
|
||||
default: [],
|
||||
},
|
||||
})
|
||||
.help()
|
||||
.parse(args);
|
||||
await lazy(() => import('./commands/validate'), 'default')(argv);
|
||||
},
|
||||
execute: { loader: () => import('./commands/validate') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['config:schema'],
|
||||
description: 'Print the JSON schema for the given configuration',
|
||||
execute: async ({ args }) => {
|
||||
const argv = await yargs()
|
||||
.options({
|
||||
package: { type: 'string' },
|
||||
format: { type: 'string' },
|
||||
merge: { type: 'boolean' },
|
||||
'no-merge': { type: 'boolean' },
|
||||
})
|
||||
.help()
|
||||
.parse(args);
|
||||
await lazy(() => import('./commands/schema'), 'default')(argv);
|
||||
},
|
||||
execute: { loader: () => import('./commands/schema') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['config', 'schema'],
|
||||
description: 'Print the JSON schema for the given configuration',
|
||||
execute: async ({ args }) => {
|
||||
const argv = await yargs()
|
||||
.options({
|
||||
package: { type: 'string' },
|
||||
format: { type: 'string' },
|
||||
merge: { type: 'boolean' },
|
||||
'no-merge': { type: 'boolean' },
|
||||
})
|
||||
.help()
|
||||
.parse(args);
|
||||
await lazy(() => import('./commands/schema'), 'default')(argv);
|
||||
},
|
||||
execute: { loader: () => import('./commands/schema') },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import { version as cliVersion } from '../../../../package.json';
|
||||
import os from 'node:os';
|
||||
import { runOutput, targetPaths, findOwnPaths } from '@backstage/cli-common';
|
||||
@@ -24,11 +25,7 @@ import {
|
||||
} from '@backstage/cli-node';
|
||||
import { minimatch } from 'minimatch';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
interface InfoOptions {
|
||||
include: string[];
|
||||
format: 'text' | 'json';
|
||||
}
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
/**
|
||||
* Attempts to read package.json from node_modules for a given package name.
|
||||
@@ -55,7 +52,31 @@ function hasBackstageField(packageName: string, targetPath: string): boolean {
|
||||
return pkg?.backstage !== undefined;
|
||||
}
|
||||
|
||||
export default async (options: InfoOptions) => {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { include, format },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
include: {
|
||||
type: [String],
|
||||
description:
|
||||
'Glob patterns for additional packages to include (e.g., @spotify/backstage*)',
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
description: 'Output format (text or json)',
|
||||
default: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const options = { include, format: format as 'text' | 'json' };
|
||||
|
||||
await new Promise(async () => {
|
||||
const yarnVersion = await runOutput(['yarn', '--version']);
|
||||
/* eslint-disable-next-line no-restricted-syntax */
|
||||
|
||||
@@ -13,9 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import yargs from 'yargs';
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export default createCliPlugin({
|
||||
pluginId: 'info',
|
||||
@@ -23,27 +21,7 @@ export default createCliPlugin({
|
||||
reg.addCommand({
|
||||
path: ['info'],
|
||||
description: 'Show helpful information for debugging and reporting bugs',
|
||||
execute: async ({ args }) => {
|
||||
const argv = await yargs()
|
||||
.options({
|
||||
include: {
|
||||
type: 'string',
|
||||
array: true,
|
||||
default: [],
|
||||
description:
|
||||
'Glob patterns for additional packages to include (e.g., @spotify/backstage*)',
|
||||
},
|
||||
format: {
|
||||
type: 'string',
|
||||
choices: ['text', 'json'],
|
||||
default: 'text',
|
||||
description: 'Output format (text or json)',
|
||||
},
|
||||
})
|
||||
.help()
|
||||
.parse(args);
|
||||
await lazy(() => import('./commands/info'), 'default')(argv);
|
||||
},
|
||||
execute: { loader: () => import('./commands/info') },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export async function command() {
|
||||
export default async function command() {
|
||||
throw new Error(
|
||||
'The `migrate package-exports` command has been removed, use `repo fix` instead.',
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@ import { runOutput } from '@backstage/cli-common';
|
||||
|
||||
const PREFIX = `module.exports = require('@backstage/cli/config/eslint-factory')`;
|
||||
|
||||
export async function command() {
|
||||
export default async function command() {
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
const oldConfigs = [
|
||||
|
||||
@@ -22,7 +22,7 @@ const configArgPattern = /--config[=\s][^\s$]+/;
|
||||
|
||||
const noStartRoles: PackageRole[] = ['cli', 'common-library'];
|
||||
|
||||
export async function command() {
|
||||
export default async function command() {
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
await Promise.all(
|
||||
|
||||
@@ -21,7 +21,7 @@ import { PackageGraph, PackageRoles } 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 async function command() {
|
||||
export default async function command() {
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
await Promise.all(
|
||||
|
||||
@@ -68,39 +68,24 @@ export default createCliPlugin({
|
||||
reg.addCommand({
|
||||
path: ['migrate', 'package-roles'],
|
||||
description: `Add package role field to packages that don't have it`,
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/packageRole'), 'default'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: {
|
||||
loader: () => import('./commands/packageRole'),
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['migrate', 'package-scripts'],
|
||||
description: 'Set package scripts according to each package role',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/packageScripts'), 'command'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: {
|
||||
loader: () => import('./commands/packageScripts'),
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['migrate', 'package-exports'],
|
||||
description: 'Synchronize package subpath export definitions',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/packageExports'), 'command'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: {
|
||||
loader: () => import('./commands/packageExports'),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -108,13 +93,8 @@ export default createCliPlugin({
|
||||
path: ['migrate', 'package-lint-configs'],
|
||||
description:
|
||||
'Migrates all packages to use @backstage/cli/config/eslint-factory',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/packageLintConfigs'), 'command'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: {
|
||||
loader: () => import('./commands/packageLintConfigs'),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -122,13 +102,8 @@ export default createCliPlugin({
|
||||
path: ['migrate', 'react-router-deps'],
|
||||
description:
|
||||
'Migrates the react-router dependencies for all packages to be peer dependencies',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/reactRouterDeps'), 'command'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: {
|
||||
loader: () => import('./commands/reactRouterDeps'),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
import fs from 'fs-extra';
|
||||
import { dirname, resolve as resolvePath } from 'node:path';
|
||||
@@ -28,16 +29,38 @@ import {
|
||||
} from '../lib/extractTranslations';
|
||||
import {
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_MESSAGE_PATTERN,
|
||||
formatMessagePath,
|
||||
validatePattern,
|
||||
} from '../lib/messageFilePath';
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
interface ExportOptions {
|
||||
output: string;
|
||||
pattern: string;
|
||||
}
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { output, pattern },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
output: {
|
||||
type: String,
|
||||
default: 'translations',
|
||||
description: 'Output directory for exported messages and manifest',
|
||||
},
|
||||
pattern: {
|
||||
type: String,
|
||||
default: DEFAULT_MESSAGE_PATTERN,
|
||||
description:
|
||||
'File path pattern for message files, with {id} and {lang} placeholders',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const options = { output, pattern };
|
||||
|
||||
export default async (options: ExportOptions) => {
|
||||
validatePattern(options.pattern);
|
||||
|
||||
const targetPackageJson = await readTargetPackage(
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
import fs from 'fs-extra';
|
||||
import {
|
||||
@@ -27,11 +28,7 @@ import {
|
||||
createMessagePathParser,
|
||||
formatMessagePath,
|
||||
} from '../lib/messageFilePath';
|
||||
|
||||
interface ImportOptions {
|
||||
input: string;
|
||||
output: string;
|
||||
}
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
interface ManifestRefEntry {
|
||||
package: string;
|
||||
@@ -44,7 +41,31 @@ interface Manifest {
|
||||
refs: Record<string, ManifestRefEntry>;
|
||||
}
|
||||
|
||||
export default async (options: ImportOptions) => {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { input, output },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
input: {
|
||||
type: String,
|
||||
default: 'translations',
|
||||
description:
|
||||
'Input directory containing the manifest and translated message files',
|
||||
},
|
||||
output: {
|
||||
type: String,
|
||||
default: 'src/translations/resources.ts',
|
||||
description: 'Output path for the generated wiring module',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const options = { input, output };
|
||||
await readTargetPackage(targetPaths.dir, targetPaths.rootDir);
|
||||
|
||||
const inputDir = resolvePath(targetPaths.dir, options.input);
|
||||
|
||||
@@ -13,10 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import yargs from 'yargs';
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
import { DEFAULT_MESSAGE_PATTERN } from './lib/messageFilePath';
|
||||
|
||||
export default createCliPlugin({
|
||||
pluginId: 'translations',
|
||||
@@ -25,51 +22,14 @@ export default createCliPlugin({
|
||||
path: ['translations', 'export'],
|
||||
description:
|
||||
'Export translation messages from an app and all of its frontend plugins to JSON files',
|
||||
execute: async ({ args }) => {
|
||||
const argv = await yargs()
|
||||
.options({
|
||||
output: {
|
||||
type: 'string',
|
||||
default: 'translations',
|
||||
description:
|
||||
'Output directory for exported messages and manifest',
|
||||
},
|
||||
pattern: {
|
||||
type: 'string',
|
||||
default: DEFAULT_MESSAGE_PATTERN,
|
||||
description:
|
||||
'File path pattern for message files, with {id} and {lang} placeholders',
|
||||
},
|
||||
})
|
||||
.help()
|
||||
.parse(args);
|
||||
await lazy(() => import('./commands/export'), 'default')(argv);
|
||||
},
|
||||
execute: { loader: () => import('./commands/export') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['translations', 'import'],
|
||||
description:
|
||||
'Generate translation resource wiring from translated JSON files',
|
||||
execute: async ({ args }) => {
|
||||
const argv = await yargs()
|
||||
.options({
|
||||
input: {
|
||||
type: 'string',
|
||||
default: 'translations',
|
||||
description:
|
||||
'Input directory containing the manifest and translated message files',
|
||||
},
|
||||
output: {
|
||||
type: 'string',
|
||||
default: 'src/translations/resources.ts',
|
||||
description: 'Output path for the generated wiring module',
|
||||
},
|
||||
})
|
||||
.help()
|
||||
.parse(args);
|
||||
await lazy(() => import('./commands/import'), 'default')(argv);
|
||||
},
|
||||
execute: { loader: () => import('./commands/import') },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -67,6 +67,31 @@ describe('CliInitializer', () => {
|
||||
expect(process.exit).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('should run commands using a loader', async () => {
|
||||
expect.assertions(2);
|
||||
process.argv = ['node', 'cli', 'test', '--verbose'];
|
||||
const initializer = new CliInitializer();
|
||||
initializer.add(
|
||||
createCliPlugin({
|
||||
pluginId: 'test',
|
||||
init: async reg =>
|
||||
reg.addCommand({
|
||||
path: ['test'],
|
||||
description: 'test',
|
||||
execute: {
|
||||
loader: async () => ({
|
||||
default: async ({ args }) => {
|
||||
expect(args).toEqual(['--verbose']);
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
await initializer.run();
|
||||
expect(process.exit).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('should pass positional args to the subcommand if nested', async () => {
|
||||
expect.assertions(2);
|
||||
process.argv = [
|
||||
|
||||
@@ -118,13 +118,25 @@ export class CliInitializer {
|
||||
}
|
||||
positionalArgs.push(nonProcessArgs[argIndex]);
|
||||
}
|
||||
await node.command.execute({
|
||||
const context = {
|
||||
args: [...positionalArgs, ...args.unknown],
|
||||
info: {
|
||||
usage: [programName, ...node.command.path].join(' '),
|
||||
description: node.command.description,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof node.command.execute === 'function') {
|
||||
await node.command.execute(context);
|
||||
} else {
|
||||
const mod = await node.command.execute.loader();
|
||||
// Handle CJS double-wrapping of default exports
|
||||
const fn =
|
||||
typeof mod.default === 'function'
|
||||
? mod.default
|
||||
: (mod.default as any).default;
|
||||
await fn(context);
|
||||
}
|
||||
process.exit(0);
|
||||
} catch (error: unknown) {
|
||||
exitWithError(error);
|
||||
@@ -144,7 +156,7 @@ export class CliInitializer {
|
||||
exitWithError(new ForwardedError('Unhandled rejection', rejection));
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
||||
await program.parseAsync(process.argv);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,23 +15,31 @@
|
||||
*/
|
||||
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<void>;
|
||||
|
||||
export interface BackstageCommand {
|
||||
path: string[];
|
||||
description: string;
|
||||
deprecated?: boolean;
|
||||
execute: (context: {
|
||||
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;
|
||||
};
|
||||
}) => Promise<void>;
|
||||
execute:
|
||||
| CommandExecuteFn
|
||||
| {
|
||||
loader: () => Promise<{ default: CommandExecuteFn }>;
|
||||
};
|
||||
}
|
||||
|
||||
export type CliFeature = CliPlugin;
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "cli"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -3352,6 +3352,7 @@ __metadata:
|
||||
buffer: "npm:^6.0.3"
|
||||
chalk: "npm:^4.0.0"
|
||||
chokidar: "npm:^3.3.1"
|
||||
cleye: "npm:^2.2.1"
|
||||
commander: "npm:^14.0.3"
|
||||
cross-fetch: "npm:^4.0.0"
|
||||
cross-spawn: "npm:^7.0.3"
|
||||
@@ -27244,6 +27245,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cleye@npm:^2.2.1":
|
||||
version: 2.2.1
|
||||
resolution: "cleye@npm:2.2.1"
|
||||
dependencies:
|
||||
terminal-columns: "npm:^2.0.0"
|
||||
type-flag: "npm:^4.0.3"
|
||||
checksum: 10/4d24a7e4863d38b9a40c01ca332baa7526b320c14fc488e7fb2d3f54b0ad5baa8c5defa6fc6adc079f50f629409056d9da74be6611f6394b709c9062d39b2216
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cli-boxes@npm:^2.2.0, cli-boxes@npm:^2.2.1":
|
||||
version: 2.2.1
|
||||
resolution: "cli-boxes@npm:2.2.1"
|
||||
@@ -48462,6 +48473,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"terminal-columns@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "terminal-columns@npm:2.0.0"
|
||||
checksum: 10/f958993886e09d7c0780f47833316532a0c7dd7db2fb43da9d34a88c821633408a6e7084af59f99c20b045776814748f14a8e33573886186be7a30174092964f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"terser-webpack-plugin@npm:*, terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.16":
|
||||
version: 5.3.16
|
||||
resolution: "terser-webpack-plugin@npm:5.3.16"
|
||||
@@ -49321,6 +49339,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-flag@npm:^4.0.3":
|
||||
version: 4.0.3
|
||||
resolution: "type-flag@npm:4.0.3"
|
||||
checksum: 10/ffa9e56e50ed73ed802aa45b8d47eeba7677d2d66785e9153bfbc758c2613b6ba78b621d7766b713a287dbbe6758a61000f57d2ce8afc94e1876a37de914c73c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-is@npm:^1.6.18, type-is@npm:~1.6.18":
|
||||
version: 1.6.18
|
||||
resolution: "type-is@npm:1.6.18"
|
||||
|
||||
Reference in New Issue
Block a user