Remove custom inspect flag handling, switch repo test to cleye

- Use cleye's `type: String` for --inspect/--inspect-brk instead of
  custom extractInspectFlags pre-processing in both package start and
  repo start commands.
- Switch repo test from node:util parseArgs to cleye so that --help
  shows Backstage-specific flags rather than dumping Jest's full help.
- Fix create-github-app help output to include <github-org> positional.
- Update downstream inspect types from `boolean | string` to `string`.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-01 15:02:02 +01:00
parent 3fc996fd27
commit 2946e8e4c6
9 changed files with 101 additions and 242 deletions
+11 -107
View File
@@ -131,7 +131,7 @@ Options:
### `backstage-cli create-github-app`
```
Usage: backstage-cli create-github-app
Usage: backstage-cli create-github-app <github-org>
Options:
-h, --help
@@ -310,6 +310,8 @@ Options:
--check
--config <string>
--entrypoint <string>
--inspect <string>
--inspect-brk <string>
--link <string>
--require <string>
--role <string>
@@ -513,6 +515,8 @@ Usage: backstage-cli repo start
Options:
--config <string>
--inspect <string>
--inspect-brk <string>
--link <string>
--plugin <string>
--require <string>
@@ -522,114 +526,14 @@ Options:
### `backstage-cli repo test`
```
Usage: <none>
Usage: backstage-cli repo test
Options:
--all
--automock
--cache
--cacheDirectory
--changedFilesWithAncestor
--changedSince
--ci
--clearCache
--clearMocks
--collectCoverage
--collectCoverageFrom
--color
--colors
--coverage
--coverageDirectory
--coveragePathIgnorePatterns
--coverageProvider
--coverageReporters
--coverageThreshold
--debug
--detectLeaks
--detectOpenHandles
--errorOnDeprecated
--filter
--findRelatedTests
--forceExit
--globalSetup
--globalTeardown
--globals
--haste
--help
--ignoreProjects
--injectGlobals
--json
--lastCommit
--listTests
--logHeapUsage
--maxConcurrency
--moduleDirectories
--moduleFileExtensions
--moduleNameMapper
--modulePathIgnorePatterns
--modulePaths
--noStackTrace
--notify
--notifyMode
--openHandlesTimeout
--outputFile
--passWithNoTests
--preset
--prettierPath
--projects
--randomize
--reporters
--resetMocks
--resetModules
--resolver
--restoreMocks
--rootDir
--roots
--runTestsByPath
--runner
--seed
--selectProjects
--setupFiles
--setupFilesAfterEnv
--shard
--showConfig
--showSeed
--silent
--skipFilter
--snapshotSerializers
--testEnvironment, --env
--testEnvironmentOptions
--testFailureExitCode
--testLocationInResults
--testMatch
--testPathIgnorePatterns
--testPathPatterns
--testRegex
--testResultsProcessor
--testRunner
--testSequencer
--testTimeout
--transform
--transformIgnorePatterns
--unmockedModulePathPatterns
--useStderr
--verbose
--version
--waitForUnhandledRejections
--watch
--watchAll
--watchPathIgnorePatterns
--watchman
--workerThreads
-b, --bail
-c, --config
-e, --expand
-f, --onlyFailures
-i, --runInBand
-o, --onlyChanged
-t, --testNamePattern
-u, --updateSnapshot
-w, --maxWorkers
--jest-help
--since <string>
--success-cache
--success-cache-dir <string>
-h, --help
```
### `backstage-cli translations`
@@ -22,11 +22,17 @@ import { targetPaths } from '@backstage/cli-common';
import type { CommandContext } from '../../../../../wiring/types';
export default async ({ args, info }: CommandContext) => {
const { inspectEnabled, inspectBrkEnabled, filteredArgs } =
extractInspectFlags(args);
const {
flags: { config, role, check, require: requirePath, link, entrypoint },
flags: {
config,
role,
check,
require: requirePath,
link,
entrypoint,
inspect,
inspectBrk,
},
} = cli(
{
help: info,
@@ -57,10 +63,20 @@ export default async ({ args, info }: CommandContext) => {
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"',
},
inspect: {
type: String,
description:
'Enable the Node.js inspector, optionally at a specific host:port',
},
inspectBrk: {
type: String,
description:
'Enable the Node.js inspector and break before user code starts',
},
},
},
undefined,
filteredArgs,
args,
);
await startPackage({
@@ -70,38 +86,8 @@ export default async ({ args, info }: CommandContext) => {
configPaths: config,
checksEnabled: Boolean(check),
linkedWorkspace: await resolveLinkedWorkspace(link),
inspectEnabled,
inspectBrkEnabled,
inspectEnabled: inspect,
inspectBrkEnabled: inspectBrk,
require: requirePath,
});
};
// --inspect and --inspect-brk accept an optional host value, which cleye
// can't express (it only supports required or no value). We extract them
// from the raw args before passing the rest to cleye.
function extractInspectFlags(args: string[]) {
let inspectEnabled: boolean | string | undefined;
let inspectBrkEnabled: boolean | string | undefined;
const filteredArgs: string[] = [];
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--inspect' || arg === '--inspect-brk') {
const next = args[i + 1];
const value = next && !next.startsWith('-') ? args[++i] : true;
if (arg === '--inspect') {
inspectEnabled = value;
} else {
inspectBrkEnabled = value;
}
} else if (arg.startsWith('--inspect=')) {
inspectEnabled = arg.slice('--inspect='.length);
} else if (arg.startsWith('--inspect-brk=')) {
inspectBrkEnabled = arg.slice('--inspect-brk='.length);
} else {
filteredArgs.push(arg);
}
}
return { inspectEnabled, inspectBrkEnabled, filteredArgs };
}
@@ -23,8 +23,8 @@ import { runBackend } from '../../../lib/runner';
interface StartBackendOptions {
targetDir: string;
checksEnabled: boolean;
inspectEnabled?: boolean | string;
inspectBrkEnabled?: boolean | string;
inspectEnabled?: string;
inspectBrkEnabled?: string;
linkedWorkspace?: string;
require?: string;
}
@@ -38,8 +38,8 @@ export async function startPackage(options: {
targetDir: string;
configPaths: string[];
checksEnabled: boolean;
inspectEnabled?: boolean | string;
inspectBrkEnabled?: boolean | string;
inspectEnabled?: string;
inspectBrkEnabled?: string;
linkedWorkspace?: string;
require?: string;
}): Promise<void> {
@@ -36,11 +36,8 @@ const ACCEPTED_PACKAGE_ROLES: Array<PackageRole | undefined> = [
];
export default async ({ args, info }: CommandContext) => {
const { inspectEnabled, inspectBrkEnabled, filteredArgs } =
extractInspectFlags(args);
const {
flags: { plugin, config, require: requirePath, link },
flags: { plugin, config, require: requirePath, link, inspect, inspectBrk },
_: namesOrPaths,
} = cli(
{
@@ -66,10 +63,20 @@ export default async ({ args, info }: CommandContext) => {
type: String,
description: 'Link an external workspace for module resolution',
},
inspect: {
type: String,
description:
'Enable the Node.js inspector, optionally at a specific host:port',
},
inspectBrk: {
type: String,
description:
'Enable the Node.js inspector and break before user code starts',
},
},
},
undefined,
filteredArgs,
args,
);
const targetPackages = await findTargetPackages(namesOrPaths, plugin);
@@ -77,8 +84,8 @@ export default async ({ args, info }: CommandContext) => {
const packageOptions = await resolvePackageOptions(targetPackages, {
plugin,
config,
inspect: inspectEnabled,
inspectBrk: inspectBrkEnabled,
inspect,
inspectBrk,
require: requirePath,
link,
});
@@ -204,8 +211,8 @@ export async function findTargetPackages(
type CommandOptions = {
plugin: string[];
config: string[];
inspect?: boolean | string;
inspectBrk?: boolean | string;
inspect?: string;
inspectBrk?: string;
require?: string;
link?: string;
};
@@ -263,33 +270,3 @@ async function resolvePackageOptions(
];
});
}
// --inspect and --inspect-brk accept an optional host value, which cleye
// can't express (it only supports required or no value). We extract them
// from the raw args before passing the rest to cleye.
function extractInspectFlags(args: string[]) {
let inspectEnabled: boolean | string | undefined;
let inspectBrkEnabled: boolean | string | undefined;
const filteredArgs: string[] = [];
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--inspect' || arg === '--inspect-brk') {
const next = args[i + 1];
const value = next && !next.startsWith('-') ? args[++i] : true;
if (arg === '--inspect') {
inspectEnabled = value;
} else {
inspectBrkEnabled = value;
}
} else if (arg.startsWith('--inspect=')) {
inspectEnabled = arg.slice('--inspect='.length);
} else if (arg.startsWith('--inspect-brk=')) {
inspectBrkEnabled = arg.slice('--inspect-brk='.length);
} else {
filteredArgs.push(arg);
}
}
return { inspectEnabled, inspectBrkEnabled, filteredArgs };
}
@@ -163,7 +163,7 @@ describe('runBackend', () => {
runBackend({
entry: 'src/index',
inspectEnabled: true,
inspectEnabled: '',
});
// Fast-forward past the debounce delay (100ms)
@@ -38,9 +38,9 @@ export type RunBackendOptions = {
/** relative entry point path without extension, e.g. 'src/index' */
entry: string;
/** Whether to forward the --inspect flag to the node process */
inspectEnabled?: boolean | string;
inspectEnabled?: string;
/** Whether to forward the --inspect-brk flag to the node process */
inspectBrkEnabled?: boolean | string;
inspectBrkEnabled?: string;
/** Additional module to require via the --require flag to the node process */
require?: string | string[];
/** An external linked workspace to override module resolution towards */
@@ -96,18 +96,18 @@ export async function runBackend(options: RunBackendOptions) {
}
const optionArgs = new Array<string>();
if (options.inspectEnabled) {
const inspect =
typeof options.inspectEnabled === 'string'
if (options.inspectEnabled !== undefined) {
optionArgs.push(
options.inspectEnabled
? `--inspect=${options.inspectEnabled}`
: '--inspect';
optionArgs.push(inspect);
} else if (options.inspectBrkEnabled) {
const inspect =
typeof options.inspectBrkEnabled === 'string'
: '--inspect',
);
} else if (options.inspectBrkEnabled !== undefined) {
optionArgs.push(
options.inspectBrkEnabled
? `--inspect-brk=${options.inspectBrkEnabled}`
: '--inspect-brk';
optionArgs.push(inspect);
: '--inspect-brk',
);
}
if (options.require) {
const requires = [options.require].flat();
@@ -31,7 +31,7 @@ import type { CommandContext } from '../../../../wiring/types';
export default async ({ args, info }: CommandContext) => {
const { _: positionals } = cli(
{
help: info,
help: { ...info, usage: `${info.usage} <github-org>` },
parameters: ['<github-org>'],
},
undefined,
@@ -16,7 +16,7 @@
import os from 'node:os';
import crypto from 'node:crypto';
import { parseArgs } from 'node:util';
import { cli } from 'cleye';
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
@@ -131,41 +131,41 @@ export function createFlagFinder(args: string[]) {
};
}
function removeOptionArg(args: string[], option: string, size: number = 2) {
let changed = false;
do {
changed = false;
const index = args.indexOf(option);
if (index >= 0) {
changed = true;
args.splice(index, size);
}
const indexEq = args.findIndex(arg => arg.startsWith(`${option}=`));
if (indexEq >= 0) {
changed = true;
args.splice(indexEq, 1);
}
} while (changed);
}
export default async ({ args }: CommandContext) => {
export default async ({ args, info }: CommandContext) => {
const testGlobal = global as TestGlobal;
// 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' },
// Parse Backstage-specific flags; unknown flags and arguments are left in
// args so they can be forwarded to Jest.
const { flags: opts } = cli(
{
help: info,
flags: {
since: {
type: String,
description:
'Only include test packages changed since the specified ref',
},
successCache: {
type: Boolean,
description: 'Cache and skip tests for unchanged packages',
},
successCacheDir: {
type: String,
description: 'Directory for the success cache',
},
jestHelp: {
type: Boolean,
description: "Show Jest's own help output",
},
},
ignoreArgv: type => type === 'unknown-flag' || type === 'argument',
},
});
undefined,
args,
);
const hasFlags = createFlagFinder(args);
const sinceRef = typeof opts.since === 'string' ? opts.since : undefined;
const sinceRef = 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;
@@ -252,10 +252,6 @@ export default async ({ args }: CommandContext) => {
args.push('--maxWorkers=2');
}
if (opts.since) {
removeOptionArg(args, '--since');
}
let packageGraph: PackageGraph | undefined;
async function getPackageGraph() {
if (packageGraph) {
@@ -310,17 +306,13 @@ export default async ({ args }: CommandContext) => {
}--no-node-snapshot`;
}
if (opts['jest-help']) {
removeOptionArg(args, '--jest-help');
if (opts.jestHelp) {
args.push('--help');
}
// This code path is enabled by the --successCache flag, which is specific to
// the `repo test` command in the Backstage CLI.
if (opts.successCache) {
removeOptionArg(args, '--successCache', 1);
removeOptionArg(args, '--successCacheDir');
// Refuse to run if file filters are provided
if (parsedArgs.length > 0) {
throw new Error(
@@ -338,7 +330,7 @@ export default async ({ args }: CommandContext) => {
const cache = SuccessCache.create({
name: 'test',
basePath: opts.successCacheDir as string | undefined,
basePath: opts.successCacheDir,
});
const graph = await getPackageGraph();