Migrate remaining CLI commands from commander to cleye
All command handlers in the CLI now use cleye or node:util.parseArgs for argument parsing instead of commander. The top-level command registration in CliInitializer still uses commander, but all individual command implementations are now independent of it. Modules migrated: build, lint, test, maintenance, migrate, new, and create-github-app. Also replaced createScriptOptionsParser in both build and lint modules to use node:util.parseArgs instead of commander. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Migrated remaining CLI command handlers from `commander` to `cleye` for argument parsing.
|
||||
@@ -15,20 +15,39 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { cli } from 'cleye';
|
||||
import { createDistWorkspace } from '../lib/packager';
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
type Options = {
|
||||
alwaysPack?: boolean;
|
||||
};
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { alwaysPack },
|
||||
_: positionals,
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
parameters: ['<workspace-dir>', '[packages...]'],
|
||||
flags: {
|
||||
alwaysPack: {
|
||||
type: Boolean,
|
||||
description:
|
||||
'Force workspace output to be a result of running `yarn pack` on each package (warning: very slow)',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const [dir, ...packages] = positionals;
|
||||
|
||||
export default async (dir: string, packages: string[], options: Options) => {
|
||||
if (!(await fs.pathExists(dir))) {
|
||||
throw new Error(`Target workspace directory doesn't exist, '${dir}'`);
|
||||
}
|
||||
|
||||
await createDistWorkspace(packages, {
|
||||
targetDir: dir,
|
||||
alwaysPack: options.alwaysPack,
|
||||
alwaysPack,
|
||||
enableFeatureDetection: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OptionValues } from 'commander';
|
||||
import { cli } from 'cleye';
|
||||
import fs from 'fs-extra';
|
||||
import { buildPackage, Output } from '../../../lib/builder';
|
||||
import { findRoleFromCommand } from '../../../lib/role';
|
||||
@@ -29,40 +29,90 @@ 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';
|
||||
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: {
|
||||
role,
|
||||
minify,
|
||||
skipBuildDependencies,
|
||||
stats,
|
||||
config,
|
||||
moduleFederation,
|
||||
},
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
role: {
|
||||
type: String,
|
||||
description: 'Run the command with an explicit package role',
|
||||
},
|
||||
minify: {
|
||||
type: Boolean,
|
||||
description:
|
||||
'Minify the generated code. Does not apply to app package (app is minified by default).',
|
||||
},
|
||||
skipBuildDependencies: {
|
||||
type: Boolean,
|
||||
description:
|
||||
'Skip the automatic building of local dependencies. Applies to backend packages only.',
|
||||
},
|
||||
stats: {
|
||||
type: Boolean,
|
||||
description:
|
||||
'If bundle stats are available, write them to the output directory. Applies to app packages only.',
|
||||
},
|
||||
config: {
|
||||
type: [String],
|
||||
description:
|
||||
'Config files to load instead of app-config.yaml. Applies to app packages only.',
|
||||
default: [],
|
||||
},
|
||||
moduleFederation: {
|
||||
type: Boolean,
|
||||
description:
|
||||
'Build a package as a module federation remote. Applies to frontend plugin packages only.',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
export async function command(opts: OptionValues): Promise<void> {
|
||||
const webpack = process.env.LEGACY_WEBPACK_BUILD
|
||||
? (require('webpack') as typeof import('webpack'))
|
||||
: undefined;
|
||||
|
||||
const role = await findRoleFromCommand(opts);
|
||||
const resolvedRole = await findRoleFromCommand({ role });
|
||||
|
||||
if (role === 'frontend' || role === 'backend') {
|
||||
const configPaths = (opts.config as string[]).map(arg => {
|
||||
if (resolvedRole === 'frontend' || resolvedRole === 'backend') {
|
||||
const configPaths = config.map(arg => {
|
||||
if (isValidUrl(arg)) {
|
||||
return arg;
|
||||
}
|
||||
return targetPaths.resolve(arg);
|
||||
});
|
||||
|
||||
if (role === 'frontend') {
|
||||
if (resolvedRole === 'frontend') {
|
||||
return buildFrontend({
|
||||
targetDir: targetPaths.dir,
|
||||
configPaths,
|
||||
writeStats: Boolean(opts.stats),
|
||||
writeStats: Boolean(stats),
|
||||
webpack,
|
||||
});
|
||||
}
|
||||
return buildBackend({
|
||||
targetDir: targetPaths.dir,
|
||||
configPaths,
|
||||
skipBuildDependencies: Boolean(opts.skipBuildDependencies),
|
||||
minify: Boolean(opts.minify),
|
||||
skipBuildDependencies: Boolean(skipBuildDependencies),
|
||||
minify: Boolean(minify),
|
||||
});
|
||||
}
|
||||
|
||||
let isModuleFederationRemote: boolean | undefined = undefined;
|
||||
if ((role as string) === 'frontend-dynamic-container') {
|
||||
if ((resolvedRole as string) === 'frontend-dynamic-container') {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`⚠️ WARNING: The 'frontend-dynamic-container' package role is experimental and will receive immediate breaking changes in the future.`,
|
||||
@@ -70,7 +120,7 @@ export async function command(opts: OptionValues): Promise<void> {
|
||||
);
|
||||
isModuleFederationRemote = true;
|
||||
}
|
||||
if (opts.moduleFederation) {
|
||||
if (moduleFederation) {
|
||||
isModuleFederationRemote = true;
|
||||
}
|
||||
|
||||
@@ -79,13 +129,13 @@ export async function command(opts: OptionValues): Promise<void> {
|
||||
return buildFrontend({
|
||||
targetDir: targetPaths.dir,
|
||||
configPaths: [],
|
||||
writeStats: Boolean(opts.stats),
|
||||
writeStats: Boolean(stats),
|
||||
isModuleFederationRemote,
|
||||
webpack,
|
||||
});
|
||||
}
|
||||
|
||||
const roleInfo = PackageRoles.getRoleInfo(role);
|
||||
const roleInfo = PackageRoles.getRoleInfo(resolvedRole);
|
||||
|
||||
const outputs = new Set<Output>();
|
||||
|
||||
@@ -106,7 +156,7 @@ export async function command(opts: OptionValues): Promise<void> {
|
||||
return buildPackage({
|
||||
outputs,
|
||||
packageJson,
|
||||
minify: Boolean(opts.minify),
|
||||
minify: Boolean(minify),
|
||||
workspacePackages: await PackageGraph.listTargetPackages(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { command } from './command';
|
||||
export { default } from './command';
|
||||
|
||||
@@ -14,22 +14,79 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OptionValues } from 'commander';
|
||||
import { cli } from 'cleye';
|
||||
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';
|
||||
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: {
|
||||
config,
|
||||
role,
|
||||
check,
|
||||
inspect,
|
||||
inspectBrk,
|
||||
require: requirePaths,
|
||||
link,
|
||||
entrypoint,
|
||||
},
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
config: {
|
||||
type: [String],
|
||||
description: 'Config files to load instead of app-config.yaml',
|
||||
default: [],
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
description: 'Run the command with an explicit package role',
|
||||
},
|
||||
check: {
|
||||
type: Boolean,
|
||||
description: 'Enable type checking and linting if available',
|
||||
},
|
||||
inspect: {
|
||||
type: String,
|
||||
description: 'Enable debugger in Node.js environments',
|
||||
},
|
||||
inspectBrk: {
|
||||
type: String,
|
||||
description:
|
||||
'Enable debugger in Node.js environments, breaking before code starts',
|
||||
},
|
||||
require: {
|
||||
type: [String],
|
||||
description: 'Add a --require argument to the node process',
|
||||
},
|
||||
link: {
|
||||
type: String,
|
||||
description: 'Link an external workspace for module resolution',
|
||||
},
|
||||
entrypoint: {
|
||||
type: String,
|
||||
description:
|
||||
'The entrypoint to start from, relative to the package root. Can point to either a file (without extension) or a directory (in which case the index file in that directory is used). Defaults to "dev"',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
export async function command(opts: OptionValues): Promise<void> {
|
||||
await startPackage({
|
||||
role: await findRoleFromCommand(opts),
|
||||
entrypoint: opts.entrypoint,
|
||||
role: await findRoleFromCommand({ role }),
|
||||
entrypoint,
|
||||
targetDir: targetPaths.dir,
|
||||
configPaths: opts.config as string[],
|
||||
checksEnabled: Boolean(opts.check),
|
||||
linkedWorkspace: await resolveLinkedWorkspace(opts.link),
|
||||
inspectEnabled: opts.inspect,
|
||||
inspectBrkEnabled: opts.inspectBrk,
|
||||
require: opts.require,
|
||||
configPaths: config,
|
||||
checksEnabled: Boolean(check),
|
||||
linkedWorkspace: await resolveLinkedWorkspace(link),
|
||||
inspectEnabled: inspect,
|
||||
inspectBrkEnabled: inspectBrk,
|
||||
require: requirePaths?.[0],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { command } from './command';
|
||||
export { default } from './command';
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, OptionValues } from 'commander';
|
||||
import { cli } from 'cleye';
|
||||
import { relative as relativePath } from 'node:path';
|
||||
import { buildPackages, getOutputsForRole } from '../../lib/builder';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
@@ -29,18 +29,46 @@ import {
|
||||
import { buildFrontend } from '../../lib/buildFrontend';
|
||||
import { buildBackend } from '../../lib/buildBackend';
|
||||
import { createScriptOptionsParser } from '../../lib/optionsParser';
|
||||
import type { CommandContext } from '../../../../wiring/types';
|
||||
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { all, since, minify },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
all: {
|
||||
type: Boolean,
|
||||
description:
|
||||
'Build all packages, including bundled app and backend packages.',
|
||||
},
|
||||
since: {
|
||||
type: String,
|
||||
description:
|
||||
'Only build packages and their dev dependents that changed since the specified ref',
|
||||
},
|
||||
minify: {
|
||||
type: Boolean,
|
||||
description:
|
||||
'Minify the generated code. Does not apply to app package (app is minified by default).',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
let packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
const webpack = process.env.LEGACY_WEBPACK_BUILD
|
||||
? (require('webpack') as typeof import('webpack'))
|
||||
: undefined;
|
||||
|
||||
if (opts.since) {
|
||||
if (since) {
|
||||
const graph = PackageGraph.fromPackages(packages);
|
||||
const changedPackages = await graph.listChangedPackages({
|
||||
ref: opts.since,
|
||||
ref: since,
|
||||
analyzeLockfile: true,
|
||||
});
|
||||
const withDevDependents = graph.collectPackageNames(
|
||||
@@ -53,7 +81,14 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
const apps = new Array<BackstagePackage>();
|
||||
const backends = new Array<BackstagePackage>();
|
||||
|
||||
const parseBuildScript = createScriptOptionsParser(cmd, ['package', 'build']);
|
||||
const parseBuildScript = createScriptOptionsParser(['package', 'build'], {
|
||||
role: { type: 'string' },
|
||||
minify: { type: 'boolean' },
|
||||
'skip-build-dependencies': { type: 'boolean' },
|
||||
stats: { type: 'boolean' },
|
||||
config: { type: 'string', multiple: true },
|
||||
'module-federation': { type: 'boolean' },
|
||||
});
|
||||
|
||||
const options = packages.flatMap(pkg => {
|
||||
const role =
|
||||
@@ -92,14 +127,14 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
outputs,
|
||||
logPrefix: `${chalk.cyan(relativePath(targetPaths.rootDir, pkg.dir))}: `,
|
||||
workspacePackages: packages,
|
||||
minify: opts.minify ?? buildOptions.minify,
|
||||
minify: minify ?? Boolean(buildOptions.minify),
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Building packages');
|
||||
await buildPackages(options);
|
||||
|
||||
if (opts.all) {
|
||||
if (all) {
|
||||
console.log('Building apps');
|
||||
await runConcurrentTasks({
|
||||
items: apps,
|
||||
@@ -112,9 +147,12 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const configPaths = buildOptions.config;
|
||||
await buildFrontend({
|
||||
targetDir: pkg.dir,
|
||||
configPaths: (buildOptions.config as string[]) ?? [],
|
||||
configPaths: Array.isArray(configPaths)
|
||||
? (configPaths as string[])
|
||||
: [],
|
||||
writeStats: Boolean(buildOptions.stats),
|
||||
webpack,
|
||||
});
|
||||
@@ -136,9 +174,9 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
await buildBackend({
|
||||
targetDir: pkg.dir,
|
||||
skipBuildDependencies: true,
|
||||
minify: opts.minify ?? buildOptions.minify,
|
||||
minify: minify ?? Boolean(buildOptions.minify),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,10 +21,12 @@ import {
|
||||
} from '@backstage/cli-node';
|
||||
import { relative as relativePath } from 'node:path';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
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';
|
||||
|
||||
const ACCEPTED_PACKAGE_ROLES: Array<PackageRole | undefined> = [
|
||||
'frontend',
|
||||
@@ -33,19 +35,60 @@ const ACCEPTED_PACKAGE_ROLES: Array<PackageRole | undefined> = [
|
||||
'backend-plugin',
|
||||
];
|
||||
|
||||
type CommandOptions = {
|
||||
plugin: string[];
|
||||
config: string[];
|
||||
inspect?: boolean | string;
|
||||
inspectBrk?: boolean | string;
|
||||
require?: string;
|
||||
link?: string;
|
||||
};
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { plugin, config, inspect, inspectBrk, require: requirePath, link },
|
||||
_: namesOrPaths,
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
plugin: {
|
||||
type: [String],
|
||||
description:
|
||||
'Start the dev entry-point for any matching plugin package in the repo',
|
||||
default: [],
|
||||
},
|
||||
config: {
|
||||
type: [String],
|
||||
description: 'Config files to load instead of app-config.yaml',
|
||||
default: [],
|
||||
},
|
||||
inspect: {
|
||||
type: String,
|
||||
description:
|
||||
'Enable debugger in Node.js environments. Applies to backend package only',
|
||||
},
|
||||
inspectBrk: {
|
||||
type: String,
|
||||
description:
|
||||
'Enable debugger in Node.js environments, breaking before code starts. Applies to backend package only',
|
||||
},
|
||||
require: {
|
||||
type: String,
|
||||
description:
|
||||
'Add a --require argument to the node process. Applies to backend package only',
|
||||
},
|
||||
link: {
|
||||
type: String,
|
||||
description: 'Link an external workspace for module resolution',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
export async function command(namesOrPaths: string[], options: CommandOptions) {
|
||||
const targetPackages = await findTargetPackages(namesOrPaths, options.plugin);
|
||||
const targetPackages = await findTargetPackages(namesOrPaths, plugin);
|
||||
|
||||
const packageOptions = await resolvePackageOptions(targetPackages, options);
|
||||
const packageOptions = await resolvePackageOptions(targetPackages, {
|
||||
plugin,
|
||||
config,
|
||||
inspect,
|
||||
inspectBrk,
|
||||
require: requirePath,
|
||||
link,
|
||||
});
|
||||
|
||||
if (packageOptions.length === 0) {
|
||||
console.log('No packages found to start');
|
||||
@@ -60,7 +103,7 @@ export async function command(namesOrPaths: string[], options: CommandOptions) {
|
||||
|
||||
// Each of these block until interrupted by user
|
||||
await Promise.all(packageOptions.map(entry => startPackage(entry.options)));
|
||||
}
|
||||
};
|
||||
|
||||
export async function findTargetPackages(
|
||||
namesOrPaths: string[],
|
||||
@@ -165,6 +208,15 @@ export async function findTargetPackages(
|
||||
);
|
||||
}
|
||||
|
||||
type CommandOptions = {
|
||||
plugin: string[];
|
||||
config: string[];
|
||||
inspect?: string;
|
||||
inspectBrk?: string;
|
||||
require?: string;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
async function resolvePackageOptions(
|
||||
targetPackages: BackstagePackage[],
|
||||
options: CommandOptions,
|
||||
|
||||
@@ -14,46 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command, Option } from 'commander';
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
const configOption = [
|
||||
'--config <path>',
|
||||
'Config files to load instead of app-config.yaml',
|
||||
(opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]),
|
||||
Array<string>(),
|
||||
] as const;
|
||||
|
||||
export function registerPackageCommands(command: Command) {
|
||||
command
|
||||
.command('build')
|
||||
.description('Build a package for production deployment or publishing')
|
||||
.option('--role <name>', 'Run the command with an explicit package role')
|
||||
.option(
|
||||
'--minify',
|
||||
'Minify the generated code. Does not apply to app package (app is minified by default).',
|
||||
)
|
||||
.option(
|
||||
'--skip-build-dependencies',
|
||||
'Skip the automatic building of local dependencies. Applies to backend packages only.',
|
||||
)
|
||||
.option(
|
||||
'--stats',
|
||||
'If bundle stats are available, write them to the output directory. Applies to app packages only.',
|
||||
)
|
||||
.option(
|
||||
'--config <path>',
|
||||
'Config files to load instead of app-config.yaml. Applies to app packages only.',
|
||||
(opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]),
|
||||
Array<string>(),
|
||||
)
|
||||
.option(
|
||||
'--module-federation',
|
||||
'Build a package as a module federation remote. Applies to frontend plugin packages only.',
|
||||
)
|
||||
.action(lazy(() => import('./commands/package/build'), 'command'));
|
||||
}
|
||||
|
||||
export const buildPlugin = createCliPlugin({
|
||||
pluginId: 'build',
|
||||
@@ -61,146 +22,26 @@ export const buildPlugin = createCliPlugin({
|
||||
reg.addCommand({
|
||||
path: ['package', 'build'],
|
||||
description: 'Build a package for production deployment or publishing',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
|
||||
const defaultCommand = command
|
||||
.option(
|
||||
'--role <name>',
|
||||
'Run the command with an explicit package role',
|
||||
)
|
||||
.option(
|
||||
'--minify',
|
||||
'Minify the generated code. Does not apply to app package (app is minified by default).',
|
||||
)
|
||||
.option(
|
||||
'--skip-build-dependencies',
|
||||
'Skip the automatic building of local dependencies. Applies to backend packages only.',
|
||||
)
|
||||
.option(
|
||||
'--stats',
|
||||
'If bundle stats are available, write them to the output directory. Applies to app packages only.',
|
||||
)
|
||||
.option(
|
||||
'--config <path>',
|
||||
'Config files to load instead of app-config.yaml. Applies to app packages only.',
|
||||
(opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]),
|
||||
Array<string>(),
|
||||
)
|
||||
.option(
|
||||
'--module-federation',
|
||||
'Build a package as a module federation remote. Applies to frontend plugin packages only.',
|
||||
)
|
||||
.action(lazy(() => import('./commands/package/build'), 'command'));
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/package/build') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['repo', 'build'],
|
||||
description:
|
||||
'Build packages in the project, excluding bundled app and backend packages.',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
|
||||
// This command expect `package build` to be registered, as its used to parse
|
||||
// individual plugins' package build scripts.
|
||||
registerPackageCommands(command.command('package'));
|
||||
|
||||
const defaultCommand = command
|
||||
.option(
|
||||
'--all',
|
||||
'Build all packages, including bundled app and backend packages.',
|
||||
)
|
||||
.option(
|
||||
'--since <ref>',
|
||||
'Only build packages and their dev dependents that changed since the specified ref',
|
||||
)
|
||||
.option(
|
||||
'--minify',
|
||||
'Minify the generated code. Does not apply to app package (app is minified by default).',
|
||||
)
|
||||
.action(lazy(() => import('./commands/repo/build'), 'command'));
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/repo/build') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['package', 'start'],
|
||||
description: 'Start a package for local development',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
|
||||
const defaultCommand = command
|
||||
.option(...configOption)
|
||||
.option(
|
||||
'--role <name>',
|
||||
'Run the command with an explicit package role',
|
||||
)
|
||||
.option('--check', 'Enable type checking and linting if available')
|
||||
.option('--inspect [host]', 'Enable debugger in Node.js environments')
|
||||
.option(
|
||||
'--inspect-brk [host]',
|
||||
'Enable debugger in Node.js environments, breaking before code starts',
|
||||
)
|
||||
.option(
|
||||
'--require <path...>',
|
||||
'Add a --require argument to the node process',
|
||||
)
|
||||
.option(
|
||||
'--link <path>',
|
||||
'Link an external workspace for module resolution',
|
||||
)
|
||||
.option(
|
||||
'--entrypoint <path>',
|
||||
'The entrypoint to start from, relative to the package root. Can point to either a file (without extension) or a directory (in which case the index file in that directory is used). Defaults to "dev"',
|
||||
)
|
||||
.action(lazy(() => import('./commands/package/start'), 'command'));
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/package/start') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['repo', 'start'],
|
||||
description: 'Starts packages in the repo for local development',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
|
||||
const defaultCommand = command
|
||||
.argument(
|
||||
'[packageNameOrPath...]',
|
||||
'Run the specified package instead of the defaults.',
|
||||
)
|
||||
.option(
|
||||
'--plugin <pluginId>',
|
||||
'Start the dev entry-point for any matching plugin package in the repo',
|
||||
(opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]),
|
||||
Array<string>(),
|
||||
)
|
||||
.option(...configOption)
|
||||
.option(
|
||||
'--inspect [host]',
|
||||
'Enable debugger in Node.js environments. Applies to backend package only',
|
||||
)
|
||||
.option(
|
||||
'--inspect-brk [host]',
|
||||
'Enable debugger in Node.js environments, breaking before code starts. Applies to backend package only',
|
||||
)
|
||||
.option(
|
||||
'--require <path...>',
|
||||
'Add a --require argument to the node process. Applies to backend package only',
|
||||
)
|
||||
.option(
|
||||
'--link <path>',
|
||||
'Link an external workspace for module resolution',
|
||||
)
|
||||
.action(
|
||||
lazy(() => import('../build/commands/repo/start'), 'command'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/repo/start') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
@@ -239,25 +80,7 @@ export const buildPlugin = createCliPlugin({
|
||||
path: ['build-workspace'],
|
||||
description:
|
||||
'Builds a temporary dist workspace from the provided packages',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command
|
||||
.arguments('<workspace-dir> [packages...]')
|
||||
.addOption(
|
||||
new Option(
|
||||
'--alwaysYarnPack',
|
||||
'Alias for --alwaysPack for backwards compatibility.',
|
||||
)
|
||||
.implies({ alwaysPack: true })
|
||||
.hideHelp(true),
|
||||
)
|
||||
.option(
|
||||
'--alwaysPack',
|
||||
'Force workspace output to be a result of running `yarn pack` on each package (warning: very slow)',
|
||||
)
|
||||
.action(lazy(() => import('./commands/buildWorkspace'), 'default'));
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/buildWorkspace') },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,34 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Command } from 'commander';
|
||||
import { parseArgs, type ParseArgsConfig } from 'node:util';
|
||||
|
||||
export function createScriptOptionsParser(
|
||||
anyCmd: Command,
|
||||
commandPath: string[],
|
||||
options: ParseArgsConfig['options'],
|
||||
) {
|
||||
// Regardless of what command instance is passed in we want to find
|
||||
// the root command and resolve the path from there
|
||||
let rootCmd = anyCmd;
|
||||
while (rootCmd.parent) {
|
||||
rootCmd = rootCmd.parent;
|
||||
}
|
||||
|
||||
// Now find the command that was requested
|
||||
let targetCmd = rootCmd as Command | undefined;
|
||||
for (const name of commandPath) {
|
||||
targetCmd = targetCmd?.commands.find(c => c.name() === name) as
|
||||
| Command
|
||||
| undefined;
|
||||
}
|
||||
|
||||
if (!targetCmd) {
|
||||
throw new Error(
|
||||
`Could not find package command '${commandPath.join(' ')}'`,
|
||||
);
|
||||
}
|
||||
const cmd = targetCmd;
|
||||
|
||||
const expectedScript = `backstage-cli ${commandPath.join(' ')}`;
|
||||
|
||||
return (scriptStr?: string) => {
|
||||
@@ -49,22 +27,9 @@ export function createScriptOptionsParser(
|
||||
}
|
||||
|
||||
const argsStr = scriptStr.slice(expectedScript.length).trim();
|
||||
const args = argsStr ? argsStr.split(/\s+/) : [];
|
||||
|
||||
// Can't clone or copy or even use commands as prototype, so we mutate
|
||||
// the necessary members instead, and then reset them once we're done
|
||||
const currentOpts = (cmd as any)._optionValues;
|
||||
const currentStore = (cmd as any)._storeOptionsAsProperties;
|
||||
|
||||
const result: Record<string, any> = {};
|
||||
(cmd as any)._storeOptionsAsProperties = false;
|
||||
(cmd as any)._optionValues = result;
|
||||
|
||||
// Triggers the writing of options to the result object
|
||||
cmd.parseOptions(argsStr.split(' '));
|
||||
|
||||
(cmd as any)._optionValues = currentOpts;
|
||||
(cmd as any)._storeOptionsAsProperties = currentStore;
|
||||
|
||||
return result;
|
||||
const { values } = parseArgs({ args, strict: false, options });
|
||||
return values;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,20 +16,12 @@
|
||||
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import { overrideTargetPaths } from '@backstage/cli-common/testUtils';
|
||||
import { Command } from 'commander';
|
||||
import { findRoleFromCommand } from './role';
|
||||
|
||||
const mockDir = createMockDirectory();
|
||||
overrideTargetPaths(mockDir.path);
|
||||
|
||||
describe('findRoleFromCommand', () => {
|
||||
function mkCommand(args?: string) {
|
||||
const parsed = new Command()
|
||||
.option('--role <role>', 'test role')
|
||||
.parse(args?.split(' ') ?? [], { from: 'user' });
|
||||
return parsed.opts();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({
|
||||
@@ -42,16 +34,14 @@ describe('findRoleFromCommand', () => {
|
||||
});
|
||||
|
||||
it('provides role info by role', async () => {
|
||||
await expect(findRoleFromCommand(mkCommand())).resolves.toEqual(
|
||||
'web-library',
|
||||
);
|
||||
await expect(findRoleFromCommand({})).resolves.toEqual('web-library');
|
||||
|
||||
await expect(
|
||||
findRoleFromCommand(mkCommand('--role node-library')),
|
||||
findRoleFromCommand({ role: 'node-library' }),
|
||||
).resolves.toEqual('node-library');
|
||||
|
||||
await expect(
|
||||
findRoleFromCommand(mkCommand('--role invalid')),
|
||||
).rejects.toThrow(`Unknown package role 'invalid'`);
|
||||
await expect(findRoleFromCommand({ role: 'invalid' })).rejects.toThrow(
|
||||
`Unknown package role 'invalid'`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,14 +15,13 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { OptionValues } from 'commander';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
|
||||
import { PackageRoles, PackageRole } from '@backstage/cli-node';
|
||||
|
||||
export async function findRoleFromCommand(
|
||||
opts: OptionValues,
|
||||
): Promise<PackageRole> {
|
||||
export async function findRoleFromCommand(opts: {
|
||||
role?: string;
|
||||
}): Promise<PackageRole> {
|
||||
if (opts.role) {
|
||||
return PackageRoles.getRoleInfo(opts.role)?.role;
|
||||
}
|
||||
|
||||
@@ -19,14 +19,27 @@ import chalk from 'chalk';
|
||||
import { stringify as stringifyYaml } from 'yaml';
|
||||
import inquirer, { Question, Answers } from 'inquirer';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
import { cli } from 'cleye';
|
||||
|
||||
import { GithubCreateAppServer } from './GithubCreateAppServer';
|
||||
import openBrowser from 'react-dev-utils/openBrowser';
|
||||
import type { CommandContext } from '../../../../wiring/types';
|
||||
|
||||
// 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 (org: string) => {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const { _: positionals } = cli(
|
||||
{
|
||||
help: info,
|
||||
parameters: ['<github-org>'],
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const org = positionals[0];
|
||||
|
||||
const answers: Answers = await inquirer.prompt({
|
||||
name: 'appType',
|
||||
type: 'checkbox',
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { Command } from 'commander';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export default createCliPlugin({
|
||||
pluginId: 'new',
|
||||
@@ -23,16 +21,7 @@ export default createCliPlugin({
|
||||
reg.addCommand({
|
||||
path: ['create-github-app'],
|
||||
description: 'Create new GitHub App in your organization.',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command
|
||||
.argument('<github-org>')
|
||||
.action(
|
||||
lazy(() => import('./commands/create-github-app'), 'default'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/create-github-app') },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -15,15 +15,46 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { OptionValues } from 'commander';
|
||||
import { cli } from 'cleye';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
|
||||
import { ESLint } from 'eslint';
|
||||
import type { CommandContext } from '../../../../wiring/types';
|
||||
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { fix, format, outputFile, maxWarnings },
|
||||
_: directories,
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
fix: {
|
||||
type: Boolean,
|
||||
description: 'Attempt to automatically fix violations',
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
description: 'Lint report output format',
|
||||
default: 'eslint-formatter-friendly',
|
||||
},
|
||||
outputFile: {
|
||||
type: String,
|
||||
description: 'Write the lint report to a file instead of stdout',
|
||||
},
|
||||
maxWarnings: {
|
||||
type: String,
|
||||
description:
|
||||
'Fail if more than this number of warnings. -1 allows warnings. (default: -1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
export default async (directories: string[], opts: OptionValues) => {
|
||||
const eslint = new ESLint({
|
||||
cwd: targetPaths.dir,
|
||||
fix: opts.fix,
|
||||
fix,
|
||||
extensions: ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'],
|
||||
});
|
||||
|
||||
@@ -31,31 +62,31 @@ export default async (directories: string[], opts: OptionValues) => {
|
||||
directories.length ? directories : ['.'],
|
||||
);
|
||||
|
||||
const maxWarnings = opts.maxWarnings ?? -1;
|
||||
const ignoreWarnings = +maxWarnings === -1;
|
||||
const maxWarningsNum = maxWarnings ? +maxWarnings : -1;
|
||||
const ignoreWarnings = maxWarningsNum === -1;
|
||||
|
||||
const failed =
|
||||
results.some(r => r.errorCount > 0) ||
|
||||
(!ignoreWarnings &&
|
||||
results.reduce((current, next) => current + next.warningCount, 0) >
|
||||
maxWarnings);
|
||||
maxWarningsNum);
|
||||
|
||||
if (opts.fix) {
|
||||
if (fix) {
|
||||
await ESLint.outputFixes(results);
|
||||
}
|
||||
|
||||
const formatter = await eslint.loadFormatter(opts.format);
|
||||
const formatter = await eslint.loadFormatter(format);
|
||||
|
||||
// This formatter uses the cwd to format file paths, so let's have that happen from the root instead
|
||||
if (opts.format === 'eslint-formatter-friendly') {
|
||||
if (format === 'eslint-formatter-friendly') {
|
||||
process.chdir(targetPaths.rootDir);
|
||||
}
|
||||
|
||||
const resultText = await formatter.format(results);
|
||||
|
||||
if (resultText) {
|
||||
if (opts.outputFile) {
|
||||
await fs.writeFile(targetPaths.resolve(opts.outputFile), resultText);
|
||||
if (outputFile) {
|
||||
await fs.writeFile(targetPaths.resolve(outputFile), resultText);
|
||||
} else {
|
||||
console.log(resultText);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import { Command, OptionValues } from 'commander';
|
||||
import { cli } from 'cleye';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { relative as relativePath } from 'node:path';
|
||||
import {
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
|
||||
import { createScriptOptionsParser } from '../../lib/optionsParser';
|
||||
import type { CommandContext } from '../../../../wiring/types';
|
||||
|
||||
function depCount(pkg: BackstagePackageJson) {
|
||||
const deps = pkg.dependencies ? Object.keys(pkg.dependencies).length : 0;
|
||||
@@ -38,24 +39,77 @@ function depCount(pkg: BackstagePackageJson) {
|
||||
return deps + devDeps;
|
||||
}
|
||||
|
||||
export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: {
|
||||
fix,
|
||||
format,
|
||||
outputFile,
|
||||
successCache: useSuccessCache,
|
||||
successCacheDir,
|
||||
since,
|
||||
maxWarnings,
|
||||
},
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
fix: {
|
||||
type: Boolean,
|
||||
description: 'Attempt to automatically fix violations',
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
description: 'Lint report output format',
|
||||
default: 'eslint-formatter-friendly',
|
||||
},
|
||||
outputFile: {
|
||||
type: String,
|
||||
description: 'Write the lint report to a file instead of stdout',
|
||||
},
|
||||
successCache: {
|
||||
type: Boolean,
|
||||
description:
|
||||
'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run',
|
||||
},
|
||||
successCacheDir: {
|
||||
type: String,
|
||||
description:
|
||||
'Set the success cache location, (default: node_modules/.cache/backstage-cli)',
|
||||
},
|
||||
since: {
|
||||
type: String,
|
||||
description:
|
||||
'Only lint packages that changed since the specified ref',
|
||||
},
|
||||
maxWarnings: {
|
||||
type: String,
|
||||
description:
|
||||
'Fail if more than this number of warnings. -1 allows warnings. (default: -1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
let packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
const cache = SuccessCache.create({
|
||||
name: 'lint',
|
||||
basePath: opts.successCacheDir,
|
||||
basePath: successCacheDir,
|
||||
});
|
||||
const cacheContext = opts.successCache
|
||||
const cacheContext = useSuccessCache
|
||||
? {
|
||||
entries: await cache.read(),
|
||||
lockfile: await Lockfile.load(targetPaths.resolveRoot('yarn.lock')),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (opts.since) {
|
||||
if (since) {
|
||||
const graph = PackageGraph.fromPackages(packages);
|
||||
packages = await graph.listChangedPackages({
|
||||
ref: opts.since,
|
||||
ref: since,
|
||||
analyzeLockfile: true,
|
||||
});
|
||||
}
|
||||
@@ -65,7 +119,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
packages.sort((a, b) => depCount(b.packageJson) - depCount(a.packageJson));
|
||||
|
||||
// This formatter uses the cwd to format file paths, so let's have that happen from the root instead
|
||||
if (opts.format === 'eslint-formatter-friendly') {
|
||||
if (format === 'eslint-formatter-friendly') {
|
||||
process.chdir(targetPaths.rootDir);
|
||||
}
|
||||
|
||||
@@ -74,7 +128,12 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
process.env.FORCE_COLOR = '1';
|
||||
}
|
||||
|
||||
const parseLintScript = createScriptOptionsParser(cmd, ['package', 'lint']);
|
||||
const parseLintScript = createScriptOptionsParser(['package', 'lint'], {
|
||||
fix: { type: 'boolean' },
|
||||
format: { type: 'string' },
|
||||
'output-file': { type: 'string' },
|
||||
'max-warnings': { type: 'string' },
|
||||
});
|
||||
|
||||
const items = await Promise.all(
|
||||
packages.map(async pkg => {
|
||||
@@ -112,20 +171,20 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
const { results: resultsList } = await runWorkerQueueThreads({
|
||||
items: items.filter(item => item.lintOptions), // Filter out packages without lint script
|
||||
context: {
|
||||
fix: Boolean(opts.fix),
|
||||
format: opts.format as string | undefined,
|
||||
fix: Boolean(fix),
|
||||
format: format as string | undefined,
|
||||
shouldCache: Boolean(cacheContext),
|
||||
maxWarnings: opts.maxWarnings ?? -1,
|
||||
maxWarnings: maxWarnings ?? '-1',
|
||||
successCache: cacheContext?.entries,
|
||||
rootDir: targetPaths.rootDir,
|
||||
},
|
||||
workerFactory: async ({
|
||||
fix,
|
||||
format,
|
||||
fix: workerFix,
|
||||
format: workerFormat,
|
||||
shouldCache,
|
||||
successCache,
|
||||
rootDir,
|
||||
maxWarnings,
|
||||
maxWarnings: workerMaxWarnings,
|
||||
}) => {
|
||||
const { ESLint } = require('eslint') as typeof import('eslint');
|
||||
const crypto = require('node:crypto') as typeof import('crypto');
|
||||
@@ -151,7 +210,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
const start = Date.now();
|
||||
const eslint = new ESLint({
|
||||
cwd: fullDir,
|
||||
fix,
|
||||
fix: workerFix,
|
||||
extensions: ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'],
|
||||
});
|
||||
|
||||
@@ -192,7 +251,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const formatter = await eslint.loadFormatter(format);
|
||||
const formatter = await eslint.loadFormatter(workerFormat);
|
||||
|
||||
const results = await eslint.lintFiles(['.']);
|
||||
|
||||
@@ -200,18 +259,18 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
const time = ((Date.now() - start) / 1000).toFixed(2);
|
||||
console.log(`Checked ${count} files in ${relativeDir} ${time}s`);
|
||||
|
||||
if (fix) {
|
||||
if (workerFix) {
|
||||
await ESLint.outputFixes(results);
|
||||
}
|
||||
|
||||
const ignoreWarnings = +maxWarnings === -1;
|
||||
const ignoreWarnings = +workerMaxWarnings === -1;
|
||||
|
||||
const resultText = formatter.format(results) as string;
|
||||
const failed =
|
||||
results.some(r => r.errorCount > 0) ||
|
||||
(!ignoreWarnings &&
|
||||
results.reduce((current, next) => current + next.warningCount, 0) >
|
||||
maxWarnings);
|
||||
+workerMaxWarnings);
|
||||
|
||||
return {
|
||||
relativeDir,
|
||||
@@ -242,8 +301,8 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
// When doing repo lint, only list the results if the lint failed to avoid a log
|
||||
// dump of all warnings that might be irrelevant
|
||||
if (resultText) {
|
||||
if (opts.outputFile) {
|
||||
if (opts.format === 'json') {
|
||||
if (outputFile) {
|
||||
if (format === 'json') {
|
||||
jsonResults.push(resultText);
|
||||
} else {
|
||||
errorOutput += `${resultText}\n`;
|
||||
@@ -258,7 +317,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.format === 'json') {
|
||||
if (format === 'json') {
|
||||
let mergedJsonResults: any[] = [];
|
||||
for (const jsonResult of jsonResults) {
|
||||
mergedJsonResults = mergedJsonResults.concat(JSON.parse(jsonResult));
|
||||
@@ -266,8 +325,8 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
errorOutput = JSON.stringify(mergedJsonResults, null, 2);
|
||||
}
|
||||
|
||||
if (opts.outputFile && errorOutput) {
|
||||
await fs.writeFile(targetPaths.resolveRoot(opts.outputFile), errorOutput);
|
||||
if (outputFile && errorOutput) {
|
||||
await fs.writeFile(targetPaths.resolveRoot(outputFile), errorOutput);
|
||||
}
|
||||
|
||||
if (cacheContext) {
|
||||
@@ -277,4 +336,4 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
if (failed) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,28 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { Command } from 'commander';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export function registerPackageLintCommand(command: Command) {
|
||||
command.arguments('[directories...]');
|
||||
command.option('--fix', 'Attempt to automatically fix violations');
|
||||
command.option(
|
||||
'--format <format>',
|
||||
'Lint report output format',
|
||||
'eslint-formatter-friendly',
|
||||
);
|
||||
command.option(
|
||||
'--output-file <path>',
|
||||
'Write the lint report to a file instead of stdout',
|
||||
);
|
||||
command.option(
|
||||
'--max-warnings <number>',
|
||||
'Fail if more than this number of warnings. -1 allows warnings. (default: -1)',
|
||||
);
|
||||
command.description('Lint a package');
|
||||
command.action(lazy(() => import('./commands/package/lint'), 'default'));
|
||||
}
|
||||
|
||||
export default createCliPlugin({
|
||||
pluginId: 'lint',
|
||||
@@ -43,53 +21,13 @@ export default createCliPlugin({
|
||||
reg.addCommand({
|
||||
path: ['package', 'lint'],
|
||||
description: 'Lint a package',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
registerPackageLintCommand(command);
|
||||
|
||||
await command.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/package/lint') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['repo', 'lint'],
|
||||
description: 'Lint a repository',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
|
||||
registerPackageLintCommand(command.command('package').command('lint'));
|
||||
|
||||
command.option('--fix', 'Attempt to automatically fix violations');
|
||||
command.option(
|
||||
'--format <format>',
|
||||
'Lint report output format',
|
||||
'eslint-formatter-friendly',
|
||||
);
|
||||
command.option(
|
||||
'--output-file <path>',
|
||||
'Write the lint report to a file instead of stdout',
|
||||
);
|
||||
command.option(
|
||||
'--successCache',
|
||||
'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run',
|
||||
);
|
||||
command.option(
|
||||
'--successCacheDir <path>',
|
||||
'Set the success cache location, (default: node_modules/.cache/backstage-cli)',
|
||||
);
|
||||
command.option(
|
||||
'--since <ref>',
|
||||
'Only lint packages that changed since the specified ref',
|
||||
);
|
||||
command.option(
|
||||
'--max-warnings <number>',
|
||||
'Fail if more than this number of warnings. -1 allows warnings. (default: -1)',
|
||||
);
|
||||
command.description('Lint a repository');
|
||||
command.action(lazy(() => import('./commands/repo/lint'), 'command'));
|
||||
|
||||
await command.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/repo/lint') },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,34 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Command } from 'commander';
|
||||
import { parseArgs, type ParseArgsConfig } from 'node:util';
|
||||
|
||||
export function createScriptOptionsParser(
|
||||
anyCmd: Command,
|
||||
commandPath: string[],
|
||||
options: ParseArgsConfig['options'],
|
||||
) {
|
||||
// Regardless of what command instance is passed in we want to find
|
||||
// the root command and resolve the path from there
|
||||
let rootCmd = anyCmd;
|
||||
while (rootCmd.parent) {
|
||||
rootCmd = rootCmd.parent;
|
||||
}
|
||||
|
||||
// Now find the command that was requested
|
||||
let targetCmd = rootCmd as Command | undefined;
|
||||
for (const name of commandPath) {
|
||||
targetCmd = targetCmd?.commands.find(c => c.name() === name) as
|
||||
| Command
|
||||
| undefined;
|
||||
}
|
||||
|
||||
if (!targetCmd) {
|
||||
throw new Error(
|
||||
`Could not find package command '${commandPath.join(' ')}'`,
|
||||
);
|
||||
}
|
||||
const cmd = targetCmd;
|
||||
|
||||
const expectedScript = `backstage-cli ${commandPath.join(' ')}`;
|
||||
|
||||
return (scriptStr?: string) => {
|
||||
@@ -49,22 +27,9 @@ export function createScriptOptionsParser(
|
||||
}
|
||||
|
||||
const argsStr = scriptStr.slice(expectedScript.length).trim();
|
||||
const args = argsStr ? argsStr.split(/\s+/) : [];
|
||||
|
||||
// Can't clone or copy or even use commands as prototype, so we mutate
|
||||
// the necessary members instead, and then reset them once we're done
|
||||
const currentOpts = (cmd as any)._optionValues;
|
||||
const currentStore = (cmd as any)._storeOptionsAsProperties;
|
||||
|
||||
const result: Record<string, any> = {};
|
||||
(cmd as any)._storeOptionsAsProperties = false;
|
||||
(cmd as any)._optionValues = result;
|
||||
|
||||
// Triggers the writing of options to the result object
|
||||
cmd.parseOptions(argsStr.split(' '));
|
||||
|
||||
(cmd as any)._optionValues = currentOpts;
|
||||
(cmd as any)._storeOptionsAsProperties = currentStore;
|
||||
|
||||
return result;
|
||||
const { values } = parseArgs({ args, strict: false, options });
|
||||
return values;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
PackageRole,
|
||||
PackageRoles,
|
||||
} from '@backstage/cli-node';
|
||||
import { OptionValues } from 'commander';
|
||||
import { cli } from 'cleye';
|
||||
import fs from 'fs-extra';
|
||||
import {
|
||||
resolve as resolvePath,
|
||||
@@ -492,14 +492,38 @@ export function fixPeerModules(pkg: FixablePackage) {
|
||||
|
||||
type PackageFixer = (pkg: FixablePackage, packages: FixablePackage[]) => void;
|
||||
|
||||
export async function command(opts: OptionValues): Promise<void> {
|
||||
export default async ({
|
||||
args,
|
||||
info,
|
||||
}: import('../../../../wiring/types').CommandContext) => {
|
||||
const {
|
||||
flags: { publish, check },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
publish: {
|
||||
type: Boolean,
|
||||
description:
|
||||
'Enable additional fixes that only apply when publishing packages',
|
||||
},
|
||||
check: {
|
||||
type: Boolean,
|
||||
description:
|
||||
'Fail if any packages would have been changed by the command',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const packages = await readFixablePackages();
|
||||
const fixRepositoryField = createRepositoryFieldFixer();
|
||||
|
||||
const fixers: PackageFixer[] = [fixPackageExports, fixSideEffects];
|
||||
|
||||
// Fixers that only apply to repos that publish packages
|
||||
if (opts.publish) {
|
||||
if (publish) {
|
||||
fixers.push(
|
||||
fixRepositoryField,
|
||||
fixPluginId,
|
||||
@@ -514,11 +538,11 @@ export async function command(opts: OptionValues): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.check) {
|
||||
if (check) {
|
||||
if (printPackageFixHint(packages)) {
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
await writeFixedPackages(packages);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,12 +16,26 @@
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { ESLint } from 'eslint';
|
||||
import { OptionValues } from 'commander';
|
||||
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';
|
||||
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { json },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
json: { type: Boolean, description: 'Output as JSON' },
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
export async function command(opts: OptionValues) {
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
const eslint = new ESLint({
|
||||
@@ -74,7 +88,7 @@ export async function command(opts: OptionValues) {
|
||||
stderr.cursorTo(0);
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
if (json) {
|
||||
console.log(JSON.stringify(deprecations, null, 2));
|
||||
} else {
|
||||
for (const d of deprecations) {
|
||||
@@ -87,4 +101,4 @@ export async function command(opts: OptionValues) {
|
||||
if (deprecations.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,9 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Command } from 'commander';
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export default createCliPlugin({
|
||||
pluginId: 'maintenance',
|
||||
@@ -23,36 +21,13 @@ export default createCliPlugin({
|
||||
reg.addCommand({
|
||||
path: ['repo', 'fix'],
|
||||
description: 'Automatically fix packages in the project',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command
|
||||
.option(
|
||||
'--publish',
|
||||
'Enable additional fixes that only apply when publishing packages',
|
||||
)
|
||||
.option(
|
||||
'--check',
|
||||
'Fail if any packages would have been changed by the command',
|
||||
)
|
||||
.action(lazy(() => import('./commands/repo/fix'), 'command'));
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/repo/fix') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['repo', 'list-deprecations'],
|
||||
description: 'List deprecations',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(
|
||||
lazy(() => import('./commands/repo/list-deprecations'), 'command'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/repo/list-deprecations') },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import fs from 'fs-extra';
|
||||
import { Command } from 'commander';
|
||||
import * as runObj from '@backstage/cli-common';
|
||||
import { overrideTargetPaths } from '@backstage/cli-common/testUtils';
|
||||
import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump';
|
||||
import type { CommandContext } from '../../../../wiring/types';
|
||||
import { registerMswTestHooks, withLogCollector } from '@backstage/test-utils';
|
||||
import { YarnInfoInspectData } from '../../lib/versioning/packages';
|
||||
import { setupServer } from 'msw/node';
|
||||
@@ -126,6 +126,28 @@ const expectLogsToMatch = (
|
||||
expect(receivedLogs.filter(Boolean).sort()).toEqual(expected.sort());
|
||||
};
|
||||
|
||||
function callBump(flags: Record<string, string | boolean | null>) {
|
||||
const args: string[] = [];
|
||||
for (const [key, value] of Object.entries(flags)) {
|
||||
if (value === null || value === undefined) {
|
||||
continue;
|
||||
}
|
||||
const flag = `--${key.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)}`;
|
||||
if (typeof value === 'boolean') {
|
||||
if (value) {
|
||||
args.push(flag);
|
||||
}
|
||||
} else {
|
||||
args.push(flag, value);
|
||||
}
|
||||
}
|
||||
const context: CommandContext = {
|
||||
args,
|
||||
info: { usage: 'backstage-cli versions:bump', description: 'test' },
|
||||
};
|
||||
return bump(context);
|
||||
}
|
||||
|
||||
describe('bump', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
@@ -190,7 +212,7 @@ describe('bump', () => {
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
await callBump({ release: 'main' });
|
||||
});
|
||||
expectLogsToMatch(logs, [
|
||||
'Using default pattern glob @backstage/*',
|
||||
@@ -283,11 +305,7 @@ describe('bump', () => {
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({
|
||||
pattern: null,
|
||||
release: 'main',
|
||||
skipInstall: true,
|
||||
} as unknown as Command);
|
||||
await callBump({ release: 'main', skipInstall: true });
|
||||
});
|
||||
expectLogsToMatch(logs, [
|
||||
'Using default pattern glob @backstage/*',
|
||||
@@ -389,7 +407,7 @@ describe('bump', () => {
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
await callBump({ release: 'main' });
|
||||
});
|
||||
expectLogsToMatch(logs, [
|
||||
'Using default pattern glob @backstage/*',
|
||||
@@ -493,7 +511,7 @@ describe('bump', () => {
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
await callBump({ release: 'main' });
|
||||
});
|
||||
expectLogsToMatch(logs, [
|
||||
'Using default pattern glob @backstage/*',
|
||||
@@ -588,9 +606,9 @@ describe('bump', () => {
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await expect(
|
||||
bump({ pattern: null, release: '999.0.1' } as unknown as Command),
|
||||
).rejects.toThrow('No release found for 999.0.1 version');
|
||||
await expect(callBump({ release: '999.0.1' })).rejects.toThrow(
|
||||
'No release found for 999.0.1 version',
|
||||
);
|
||||
});
|
||||
expect(logs.filter(Boolean)).toEqual([
|
||||
'Using default pattern glob @backstage/*',
|
||||
@@ -694,7 +712,7 @@ describe('bump', () => {
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'next' } as unknown as Command);
|
||||
await callBump({ release: 'next' });
|
||||
});
|
||||
expectLogsToMatch(logs, [
|
||||
'Using default pattern glob @backstage/*',
|
||||
@@ -773,10 +791,10 @@ describe('bump', () => {
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({
|
||||
await callBump({
|
||||
pattern: '@{backstage,backstage-extra}/*',
|
||||
release: 'main',
|
||||
} as any);
|
||||
});
|
||||
});
|
||||
expectLogsToMatch(logs, [
|
||||
'Using custom pattern glob @{backstage,backstage-extra}/*',
|
||||
@@ -882,7 +900,7 @@ describe('bump', () => {
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
await callBump({ release: 'main' });
|
||||
});
|
||||
expectLogsToMatch(logs, [
|
||||
'Using default pattern glob @backstage/*',
|
||||
@@ -1121,7 +1139,7 @@ describe('environment variables', () => {
|
||||
);
|
||||
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
await callBump({ release: 'main' });
|
||||
});
|
||||
|
||||
expectLogsToMatch(logs, [
|
||||
@@ -1195,7 +1213,7 @@ describe('environment variables', () => {
|
||||
} as any);
|
||||
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
await callBump({ release: 'main' });
|
||||
});
|
||||
|
||||
expectLogsToMatch(logs, [
|
||||
@@ -1280,7 +1298,7 @@ describe('environment variables', () => {
|
||||
);
|
||||
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
await callBump({ release: 'main' });
|
||||
});
|
||||
|
||||
expectLogsToMatch(logs, [
|
||||
@@ -1329,9 +1347,7 @@ describe('environment variables', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
bump({ pattern: null, release: 'main' } as unknown as Command),
|
||||
).rejects.toThrow();
|
||||
await expect(callBump({ release: 'main' })).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle network errors when using custom base URL', async () => {
|
||||
@@ -1359,8 +1375,6 @@ describe('environment variables', () => {
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
bump({ pattern: null, release: 'main' } as unknown as Command),
|
||||
).rejects.toThrow();
|
||||
await expect(callBump({ release: 'main' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ import fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import { minimatch } from 'minimatch';
|
||||
import semver from 'semver';
|
||||
import { OptionValues } from 'commander';
|
||||
import { cli } from 'cleye';
|
||||
import { isError, NotFoundError } from '@backstage/errors';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
import { migrateMovedPackages } from './migrate';
|
||||
import { runYarnInstall } from '../../lib/utils';
|
||||
import { run } from '@backstage/cli-common';
|
||||
import type { CommandContext } from '../../../../wiring/types';
|
||||
|
||||
const DEP_TYPES = [
|
||||
'dependencies',
|
||||
@@ -73,12 +74,41 @@ function extendsDefaultPattern(pattern: string): boolean {
|
||||
return minimatch('@backstage/', pattern.slice(0, -1));
|
||||
}
|
||||
|
||||
export default async (opts: OptionValues) => {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { pattern: patternFlag, release, skipInstall, skipMigrate },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
pattern: {
|
||||
type: String,
|
||||
description: 'Override glob for matching packages to upgrade',
|
||||
},
|
||||
release: {
|
||||
type: String,
|
||||
description: 'Bump to a specific Backstage release line or version',
|
||||
default: 'main',
|
||||
},
|
||||
skipInstall: {
|
||||
type: Boolean,
|
||||
description: 'Skips yarn install step',
|
||||
},
|
||||
skipMigrate: {
|
||||
type: Boolean,
|
||||
description: 'Skips migration of any moved packages',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const lockfilePath = targetPaths.resolveRoot('yarn.lock');
|
||||
const lockfile = await Lockfile.load(lockfilePath);
|
||||
const yarnPluginEnabled = await hasBackstageYarnPlugin();
|
||||
|
||||
let pattern = opts.pattern;
|
||||
let pattern = patternFlag;
|
||||
|
||||
if (!pattern) {
|
||||
console.log(`Using default pattern glob ${DEFAULT_PATTERN_GLOB}`);
|
||||
@@ -97,15 +127,15 @@ export default async (opts: OptionValues) => {
|
||||
findTargetVersion = createStrictVersionFinder({
|
||||
releaseManifest,
|
||||
});
|
||||
} else if (semver.valid(opts.release)) {
|
||||
} else if (semver.valid(release)) {
|
||||
// Specific release specified. Be strict when resolving versions
|
||||
releaseManifest = await getManifestByVersion({ version: opts.release });
|
||||
releaseManifest = await getManifestByVersion({ version: release! });
|
||||
findTargetVersion = createStrictVersionFinder({
|
||||
releaseManifest,
|
||||
});
|
||||
} else {
|
||||
// Release line specified. Be lenient when resolving versions.
|
||||
if (opts.release === 'next') {
|
||||
if (release === 'next') {
|
||||
const next = await getManifestByReleaseLine({
|
||||
releaseLine: 'next',
|
||||
versionsBaseUrl: env.BACKSTAGE_VERSIONS_BASE_URL,
|
||||
@@ -120,12 +150,12 @@ export default async (opts: OptionValues) => {
|
||||
: main;
|
||||
} else {
|
||||
releaseManifest = await getManifestByReleaseLine({
|
||||
releaseLine: opts.release,
|
||||
releaseLine: release!,
|
||||
versionsBaseUrl: env.BACKSTAGE_VERSIONS_BASE_URL,
|
||||
});
|
||||
}
|
||||
findTargetVersion = createVersionFinder({
|
||||
releaseLine: opts.releaseLine,
|
||||
releaseLine: release,
|
||||
releaseManifest,
|
||||
});
|
||||
}
|
||||
@@ -264,7 +294,7 @@ export default async (opts: OptionValues) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!opts.skipInstall) {
|
||||
if (!skipInstall) {
|
||||
await runYarnInstall();
|
||||
} else {
|
||||
console.log();
|
||||
@@ -272,14 +302,14 @@ export default async (opts: OptionValues) => {
|
||||
console.log(chalk.yellow(`Skipping yarn install`));
|
||||
}
|
||||
|
||||
if (!opts.skipMigrate) {
|
||||
if (!skipMigrate) {
|
||||
console.log();
|
||||
|
||||
const changed = await migrateMovedPackages({
|
||||
pattern: opts.pattern,
|
||||
pattern: patternFlag,
|
||||
});
|
||||
|
||||
if (changed && !opts.skipInstall) {
|
||||
if (changed && !skipInstall) {
|
||||
await runYarnInstall();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ describe('versions:migrate', () => {
|
||||
});
|
||||
|
||||
const { warn, log: logs } = await withLogCollector(async () => {
|
||||
await migrate({});
|
||||
await migrate({ args: [], info: { usage: 'test', description: 'test' } });
|
||||
});
|
||||
|
||||
expectLogsToMatch(logs, [
|
||||
@@ -229,7 +229,7 @@ describe('versions:migrate', () => {
|
||||
});
|
||||
|
||||
await withLogCollector(async () => {
|
||||
await migrate({});
|
||||
await migrate({ args: [], info: { usage: 'test', description: 'test' } });
|
||||
});
|
||||
|
||||
expect(runObj.run).toHaveBeenCalledTimes(1);
|
||||
@@ -311,7 +311,7 @@ describe('versions:migrate', () => {
|
||||
});
|
||||
|
||||
await withLogCollector(async () => {
|
||||
await migrate({});
|
||||
await migrate({ args: [], info: { usage: 'test', description: 'test' } });
|
||||
});
|
||||
|
||||
expect(runObj.run).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -16,11 +16,12 @@
|
||||
import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node';
|
||||
import chalk from 'chalk';
|
||||
import { resolve as resolvePath, join as joinPath } from 'node:path';
|
||||
import { OptionValues } from 'commander';
|
||||
import { cli } from 'cleye';
|
||||
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';
|
||||
|
||||
declare module 'replace-in-file' {
|
||||
export default function (config: {
|
||||
@@ -38,10 +39,30 @@ declare module 'replace-in-file' {
|
||||
>;
|
||||
}
|
||||
|
||||
export default async (options: OptionValues) => {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { pattern, skipCodeChanges },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
pattern: {
|
||||
type: String,
|
||||
description: 'Override glob for matching packages to upgrade',
|
||||
},
|
||||
skipCodeChanges: {
|
||||
type: Boolean,
|
||||
description: 'Skip code changes and only update package.json files',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const changed = await migrateMovedPackages({
|
||||
pattern: options.pattern,
|
||||
skipCodeChanges: options.skipCodeChanges,
|
||||
pattern,
|
||||
skipCodeChanges,
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { Command } from 'commander';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export default createCliPlugin({
|
||||
pluginId: 'migrate',
|
||||
@@ -24,45 +22,13 @@ export default createCliPlugin({
|
||||
path: ['versions:migrate'],
|
||||
description:
|
||||
'Migrate any plugins that have been moved to the @backstage-community namespace automatically',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command
|
||||
.option(
|
||||
'--pattern <glob>',
|
||||
'Override glob for matching packages to upgrade',
|
||||
)
|
||||
.option(
|
||||
'--skip-code-changes',
|
||||
'Skip code changes and only update package.json files',
|
||||
)
|
||||
.action(lazy(() => import('./commands/versions/migrate'), 'default'));
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/versions/migrate') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['versions:bump'],
|
||||
description: 'Bump Backstage packages to the latest versions',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
|
||||
const defaultCommand = command
|
||||
.option(
|
||||
'--pattern <glob>',
|
||||
'Override glob for matching packages to upgrade',
|
||||
)
|
||||
.option(
|
||||
'--release <version|next|main>',
|
||||
'Bump to a specific Backstage release line or version',
|
||||
'main',
|
||||
)
|
||||
.option('--skip-install', 'Skips yarn install step')
|
||||
.option('--skip-migrate', 'Skips migration of any moved packages')
|
||||
.action(lazy(() => import('./commands/versions/bump'), 'default'));
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/versions/bump') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { createNewPackage } from '../lib/createNewPackage';
|
||||
import { default as newCommand } from './new';
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
jest.mock('../lib/createNewPackage');
|
||||
|
||||
@@ -34,13 +35,21 @@ describe.each([
|
||||
});
|
||||
|
||||
it(`should generate naming options for --scope=${scope}`, async () => {
|
||||
await newCommand({ scope, option: [], skipInstall: false });
|
||||
const args = ['--skip-install'];
|
||||
if (scope) {
|
||||
args.push('--scope', scope);
|
||||
}
|
||||
const context: CommandContext = {
|
||||
args,
|
||||
info: { usage: 'backstage-cli new', description: 'test' },
|
||||
};
|
||||
await newCommand(context);
|
||||
expect(createNewPackage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
configOverrides: {
|
||||
configOverrides: expect.objectContaining({
|
||||
packageNamePrefix: prefix,
|
||||
packageNamePluginInfix: infix,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -14,28 +14,67 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import { createNewPackage } from '../lib/createNewPackage';
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
type ArgOptions = {
|
||||
option: string[];
|
||||
select?: string;
|
||||
skipInstall: boolean;
|
||||
private?: boolean;
|
||||
npmRegistry?: string;
|
||||
scope?: string;
|
||||
license?: string;
|
||||
baseVersion?: string;
|
||||
};
|
||||
|
||||
export default async (opts: ArgOptions) => {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
option: rawArgOptions,
|
||||
select: preselectedTemplateId,
|
||||
skipInstall,
|
||||
scope,
|
||||
private: isPrivate,
|
||||
...otherGlobals
|
||||
} = opts;
|
||||
flags: {
|
||||
select,
|
||||
option: rawArgOptions,
|
||||
skipInstall,
|
||||
scope,
|
||||
npmRegistry,
|
||||
baseVersion,
|
||||
license,
|
||||
private: isPrivate,
|
||||
},
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
select: {
|
||||
type: String,
|
||||
description: 'Select the thing you want to be creating upfront',
|
||||
},
|
||||
option: {
|
||||
type: [String],
|
||||
description: 'Pre-fill options for the creation process',
|
||||
default: [],
|
||||
},
|
||||
skipInstall: {
|
||||
type: Boolean,
|
||||
description: `Skips running 'yarn install' and 'yarn lint --fix'`,
|
||||
},
|
||||
scope: {
|
||||
type: String,
|
||||
description: 'The scope to use for new packages',
|
||||
},
|
||||
npmRegistry: {
|
||||
type: String,
|
||||
description: 'The package registry to use for new packages',
|
||||
},
|
||||
baseVersion: {
|
||||
type: String,
|
||||
description:
|
||||
'The version to use for any new packages (default: 0.1.0)',
|
||||
},
|
||||
license: {
|
||||
type: String,
|
||||
description:
|
||||
'The license to use for any new packages (default: Apache-2.0)',
|
||||
},
|
||||
private: {
|
||||
type: Boolean,
|
||||
description: 'Mark new packages as private',
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const prefilledParams = parseParams(rawArgOptions);
|
||||
|
||||
@@ -50,8 +89,8 @@ export default async (opts: ArgOptions) => {
|
||||
}
|
||||
|
||||
if (
|
||||
isPrivate === false || // set to false with --no-private flag
|
||||
Object.values(otherGlobals).filter(Boolean).length !== 0
|
||||
isPrivate === false ||
|
||||
[npmRegistry, baseVersion, license].filter(Boolean).length !== 0
|
||||
) {
|
||||
console.warn(
|
||||
`Global template configuration via CLI flags is deprecated, see https://backstage.io/docs/cli/new for information on how to configure package templating`,
|
||||
@@ -60,16 +99,16 @@ export default async (opts: ArgOptions) => {
|
||||
|
||||
await createNewPackage({
|
||||
prefilledParams,
|
||||
preselectedTemplateId,
|
||||
preselectedTemplateId: select,
|
||||
configOverrides: {
|
||||
license: otherGlobals.license,
|
||||
version: otherGlobals.baseVersion,
|
||||
license,
|
||||
version: baseVersion,
|
||||
private: isPrivate,
|
||||
publishRegistry: otherGlobals.npmRegistry,
|
||||
publishRegistry: npmRegistry,
|
||||
packageNamePrefix: packagePrefix,
|
||||
packageNamePluginInfix: pluginInfix,
|
||||
},
|
||||
skipInstall,
|
||||
skipInstall: Boolean(skipInstall),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { Command } from 'commander';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
import { NotImplementedError } from '@backstage/errors';
|
||||
|
||||
export default createCliPlugin({
|
||||
@@ -25,45 +23,7 @@ export default createCliPlugin({
|
||||
path: ['new'],
|
||||
description:
|
||||
'Open up an interactive guide to creating new things in your app',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command
|
||||
.storeOptionsAsProperties(false)
|
||||
.description(
|
||||
'Open up an interactive guide to creating new things in your app',
|
||||
)
|
||||
.option(
|
||||
'--select <name>',
|
||||
'Select the thing you want to be creating upfront',
|
||||
)
|
||||
.option(
|
||||
'--option <name>=<value>',
|
||||
'Pre-fill options for the creation process',
|
||||
(opt, arr: string[]) => [...arr, opt],
|
||||
[],
|
||||
)
|
||||
.option(
|
||||
'--skip-install',
|
||||
`Skips running 'yarn install' and 'yarn lint --fix'`,
|
||||
)
|
||||
.option('--scope <scope>', 'The scope to use for new packages')
|
||||
.option(
|
||||
'--npm-registry <URL>',
|
||||
'The package registry to use for new packages',
|
||||
)
|
||||
.option(
|
||||
'--baseVersion <version>',
|
||||
'The version to use for any new packages (default: 0.1.0)',
|
||||
)
|
||||
.option(
|
||||
'--license <license>',
|
||||
'The license to use for any new packages (default: Apache-2.0)',
|
||||
)
|
||||
.option('--no-private', 'Do not mark new packages as private')
|
||||
.action(lazy(() => import('./commands/new'), 'default'));
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/new') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
|
||||
@@ -14,9 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command, OptionValues } from 'commander';
|
||||
|
||||
import { runCheck, findOwnPaths } from '@backstage/cli-common';
|
||||
import type { CommandContext } from '../../../../wiring/types';
|
||||
|
||||
function includesAnyOf(hayStack: string[], ...needles: string[]) {
|
||||
for (const needle of needles) {
|
||||
@@ -27,15 +26,7 @@ function includesAnyOf(hayStack: string[], ...needles: string[]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export default async (_opts: OptionValues, cmd: Command) => {
|
||||
// all args are forwarded to jest
|
||||
let parent = cmd;
|
||||
while (parent.parent) {
|
||||
parent = parent.parent;
|
||||
}
|
||||
const allArgs = parent.args as string[];
|
||||
const args = allArgs.slice(allArgs.indexOf('test') + 1);
|
||||
|
||||
export default async ({ args }: CommandContext) => {
|
||||
// Only include our config if caller isn't passing their own config
|
||||
if (!includesAnyOf(args, '-c', '--config')) {
|
||||
/* eslint-disable-next-line no-restricted-syntax */
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
|
||||
import os from 'node:os';
|
||||
import crypto from 'node:crypto';
|
||||
import { parseArgs } from 'node:util';
|
||||
import yargs from 'yargs';
|
||||
// 'jest-cli' is included with jest and should be kept in sync with the installed jest version
|
||||
// eslint-disable-next-line @backstage/no-undeclared-imports
|
||||
import { run as runJest, yargsOptions as jestYargsOptions } from 'jest-cli';
|
||||
import { relative as relativePath } from 'node:path';
|
||||
import { Command, OptionValues } from 'commander';
|
||||
import { Lockfile, PackageGraph, SuccessCache } from '@backstage/cli-node';
|
||||
|
||||
import {
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
findOwnPaths,
|
||||
isChildPath,
|
||||
} from '@backstage/cli-common';
|
||||
import type { CommandContext } from '../../../../wiring/types';
|
||||
|
||||
type JestProject = {
|
||||
displayName: string;
|
||||
@@ -148,18 +149,23 @@ function removeOptionArg(args: string[], option: string, size: number = 2) {
|
||||
} while (changed);
|
||||
}
|
||||
|
||||
export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
export default async ({ args }: CommandContext) => {
|
||||
const testGlobal = global as TestGlobal;
|
||||
|
||||
// all args are forwarded to jest
|
||||
let parent = cmd;
|
||||
while (parent.parent) {
|
||||
parent = parent.parent;
|
||||
}
|
||||
const allArgs = parent.args as string[];
|
||||
const args = allArgs.slice(allArgs.indexOf('test') + 1);
|
||||
// Parse our own flags from the raw args using strict: false to allow Jest flags through
|
||||
const { values: opts } = parseArgs({
|
||||
args,
|
||||
strict: false,
|
||||
options: {
|
||||
since: { type: 'string' },
|
||||
successCache: { type: 'boolean' },
|
||||
successCacheDir: { type: 'string' },
|
||||
'jest-help': { type: 'boolean' },
|
||||
},
|
||||
});
|
||||
|
||||
const hasFlags = createFlagFinder(args);
|
||||
const sinceRef = typeof opts.since === 'string' ? opts.since : undefined;
|
||||
|
||||
// Parse the args to ensure that no file filters are provided, in which case we refuse to run
|
||||
const { _: parsedArgs } = await yargs(args).options(jestYargsOptions).argv;
|
||||
@@ -177,7 +183,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
// Run in watch mode unless in CI, coverage mode, or running all tests
|
||||
let isSingleWatchMode = args.includes('--watch');
|
||||
if (
|
||||
!opts.since &&
|
||||
!sinceRef &&
|
||||
!process.env.CI &&
|
||||
!hasFlags('--coverage', '--watch', '--watchAll')
|
||||
) {
|
||||
@@ -261,10 +267,10 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
}
|
||||
|
||||
let selectedProjects: string[] | undefined = undefined;
|
||||
if (opts.since && !hasFlags('--selectProjects')) {
|
||||
if (sinceRef && !hasFlags('--selectProjects')) {
|
||||
const graph = await getPackageGraph();
|
||||
const changedPackages = await graph.listChangedPackages({
|
||||
ref: opts.since,
|
||||
ref: sinceRef,
|
||||
analyzeLockfile: true,
|
||||
});
|
||||
|
||||
@@ -304,7 +310,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
}--no-node-snapshot`;
|
||||
}
|
||||
|
||||
if (args.includes('--jest-help')) {
|
||||
if (opts['jest-help']) {
|
||||
removeOptionArg(args, '--jest-help');
|
||||
args.push('--help');
|
||||
}
|
||||
@@ -332,7 +338,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
|
||||
const cache = SuccessCache.create({
|
||||
name: 'test',
|
||||
basePath: opts.successCacheDir,
|
||||
basePath: opts.successCacheDir as string | undefined,
|
||||
});
|
||||
const graph = await getPackageGraph();
|
||||
|
||||
@@ -439,4 +445,4 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
}
|
||||
|
||||
await runJest(args);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { Command } from 'commander';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export default createCliPlugin({
|
||||
pluginId: 'test',
|
||||
@@ -24,43 +22,14 @@ export default createCliPlugin({
|
||||
path: ['repo', 'test'],
|
||||
description:
|
||||
'Run tests, forwarding args to Jest, defaulting to watch mode',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
command.allowUnknownOption(true);
|
||||
command.allowExcessArguments(true);
|
||||
command.option(
|
||||
'--since <ref>',
|
||||
'Only test packages that changed since the specified ref',
|
||||
);
|
||||
command.option('--successCache', 'Enable success caching');
|
||||
command.option(
|
||||
'--successCacheDir <path>',
|
||||
'Set the success cache location, (default: node_modules/.cache/backstage-cli)',
|
||||
);
|
||||
command.option(
|
||||
'--jest-help',
|
||||
'Show help for Jest CLI options, which are passed through',
|
||||
);
|
||||
command.action(lazy(() => import('./commands/repo/test'), 'command'));
|
||||
await command.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/repo/test') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['package', 'test'],
|
||||
description:
|
||||
'Run tests, forwarding args to Jest, defaulting to watch mode',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
|
||||
command.allowUnknownOption(true);
|
||||
command.allowExcessArguments(true);
|
||||
command.helpOption('--backstage-cli-help');
|
||||
command.action(
|
||||
lazy(() => import('./commands/package/test'), 'default'),
|
||||
);
|
||||
await command.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/package/test') },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user