Add lazy loader pattern for CLI command execution

Extend `BackstageCommand.execute` to accept either a direct function or a
`{ loader }` object for lazy loading command implementations. Convert
several build and migrate commands to use the new pattern. Switch from
`program.parse` to `program.parseAsync` to properly await async actions.

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