🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@backstage/cli-node

Package Overview
Dependencies
Maintainers
3
Versions
528
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@backstage/cli-node - npm Package Compare versions

Comparing version
0.0.0-nightly-20260316031711
to
0.0.0-nightly-20260317031259
+87
config/nodeTransform.cjs
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { pathToFileURL } = require('node:url');
const { transformSync } = require('@swc/core');
const { addHook } = require('pirates');
const { Module } = require('node:module');
// This hooks into module resolution and overrides imports of packages that
// exist in the linked workspace to instead be resolved from the linked workspace.
if (process.env.BACKSTAGE_CLI_LINKED_WORKSPACE) {
const { join: joinPath } = require('node:path');
const { getPackagesSync } = require('@manypkg/get-packages');
const { packages: linkedPackages, root: linkedRoot } = getPackagesSync(
process.env.BACKSTAGE_CLI_LINKED_WORKSPACE,
);
// Matches all packages in the linked workspaces, as well as sub-path exports from them
const replacementRegex = new RegExp(
`^(?:${linkedPackages
.map(pkg => pkg.packageJson.name)
.join('|')})(?:/.*)?$`,
);
const origLoad = Module._load;
Module._load = function requireHook(request, parent) {
if (!replacementRegex.test(request)) {
return origLoad.call(this, request, parent);
}
// The package import that we're overriding will always existing in the root
// node_modules of the linked workspace, so it's enough to override the
// parent paths with that single entry
return origLoad.call(this, request, {
...parent,
paths: [joinPath(linkedRoot.dir, 'node_modules')],
});
};
}
addHook(
(code, filename) => {
const transformed = transformSync(code, {
filename,
sourceMaps: 'inline',
module: {
type: 'commonjs',
ignoreDynamic: true,
},
jsc: {
target: 'es2023',
parser: {
syntax: 'typescript',
},
},
});
process.send?.({ type: 'watch', path: filename });
return transformed.code;
},
{ extensions: ['.ts', '.cts'], ignoreNodeModules: true },
);
addHook(
(code, filename) => {
process.send?.({ type: 'watch', path: filename });
return code;
},
{ extensions: ['.js', '.cjs'], ignoreNodeModules: true },
);
// Register module hooks, used by "type": "module" in package.json, .mjs and
// .mts files, as well as dynamic import(...)s, although dynamic imports will be
// handled be the CommonJS hooks in this file if what it points to is CommonJS.
Module.register('./nodeTransformHooks.mjs', pathToFileURL(__filename));
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { dirname, extname, resolve as resolvePath } from 'node:path';
import { fileURLToPath } from 'node:url';
import { transformFile } from '@swc/core';
import { isBuiltin } from 'node:module';
import { readFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
// @ts-check
// No explicit file extension, no type in package.json
const DEFAULT_MODULE_FORMAT = 'commonjs';
// Source file extensions to look for when using bundle resolution strategy
const SRC_EXTS = ['.ts', '.js'];
const TS_EXTS = ['.ts', '.mts', '.cts'];
const moduleTypeTable = {
'.mjs': 'module',
'.mts': 'module',
'.cjs': 'commonjs',
'.cts': 'commonjs',
'.ts': undefined,
'.js': undefined,
};
/** @type {import('module').ResolveHook} */
export async function resolve(specifier, context, nextResolve) {
// Built-in modules are handled by the default resolver
if (isBuiltin(specifier)) {
return nextResolve(specifier, context);
}
const ext = extname(specifier);
// Unless there's an explicit import attribute, JSON files are loaded with our custom loader that's defined below.
if (ext === '.json' && !context.importAttributes?.type) {
const jsonResult = await nextResolve(specifier, context);
return {
...jsonResult,
format: 'commonjs',
importAttributes: { type: 'json' },
};
}
// Anything else with an explicit extension is handled by the default
// resolver, except that we help determine the module type where needed.
if (ext !== '') {
return withDetectedModuleType(await nextResolve(specifier, context));
}
// Other external modules are handled by the default resolver, but again we
// help determine the module type where needed.
if (!specifier.startsWith('.')) {
return withDetectedModuleType(await nextResolve(specifier, context));
}
// The rest of this function handles the case of resolving imports that do not
// specify any extension and might point to a directory with an `index.*`
// file. We resolve those using the same logic as most JS bundlers would, with
// the addition of checking if there's an explicit module format listed in the
// closest `package.json` file.
//
// We use a bundle resolution strategy in order to keep code consistent across
// Backstage codebases that contains code both for Web and Node.js, and to
// support packages with common code that can be used in both environments.
try {
// This is expected to throw, but in the event that this module specifier is
// supported we prefer to use the default resolver.
return await nextResolve(specifier, context);
} catch (error) {
if (error.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
const spec = `${specifier}${specifier.endsWith('/') ? '' : '/'}index`;
const resolved = await resolveWithoutExt(spec, context, nextResolve);
if (resolved) {
return withDetectedModuleType(resolved);
}
} else if (error.code === 'ERR_MODULE_NOT_FOUND') {
const resolved = await resolveWithoutExt(specifier, context, nextResolve);
if (resolved) {
return withDetectedModuleType(resolved);
}
}
// Unexpected error or no resolution found
throw error;
}
}
/**
* Populates the `format` field in the resolved object based on the closest `package.json` file.
*
* @param {import('module').ResolveFnOutput} resolved
* @returns {Promise<import('module').ResolveFnOutput>}
*/
async function withDetectedModuleType(resolved) {
// Already has an explicit format
if (resolved.format) {
return resolved;
}
// Happens in Node.js v22 when there's a package.json without an explicit "type" field. Use the default.
if (resolved.format === null) {
return { ...resolved, format: DEFAULT_MODULE_FORMAT };
}
const ext = extname(resolved.url);
const explicitFormat = moduleTypeTable[ext];
if (explicitFormat) {
return {
...resolved,
format: explicitFormat,
};
}
// Under normal circumstances .js files should reliably have a format and so
// we should only reach this point for .ts files. However, if additional
// custom loaders are being used the format may not be detected for .js files
// either. As such we don't restrict the file format at this point.
// TODO(Rugvip): Does this need caching? kept it simple for now but worth exploring
const packageJsonPath = await findPackageJSON(fileURLToPath(resolved.url));
if (!packageJsonPath) {
return resolved;
}
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
return {
...resolved,
format: packageJson.type ?? DEFAULT_MODULE_FORMAT,
};
}
/**
* Find the closest package.json file from the given path.
*
* TODO(Rugvip): This can be replaced with the Node.js built-in with the same name once it is stable.
* @param {string} startPath
* @returns {Promise<string | undefined>}
*/
async function findPackageJSON(startPath) {
let path = startPath;
// Some confidence check to avoid infinite loop
for (let i = 0; i < 1000; i++) {
const packagePath = resolvePath(path, 'package.json');
if (existsSync(packagePath)) {
return packagePath;
}
const newPath = dirname(path);
if (newPath === path) {
return undefined;
}
path = newPath;
}
throw new Error(
`Iteration limit reached when searching for package.json at ${startPath}`,
);
}
/** @type {import('module').ResolveHook} */
async function resolveWithoutExt(specifier, context, nextResolve) {
for (const tryExt of SRC_EXTS) {
try {
const resolved = await nextResolve(specifier + tryExt, {
...context,
format: 'commonjs',
});
return {
...resolved,
format: moduleTypeTable[tryExt] ?? resolved.format,
};
} catch {
/* ignore */
}
}
return undefined;
}
/** @type {import('module').LoadHook} */
export async function load(url, context, nextLoad) {
// Non-file URLs are handled by the default loader
if (!url.startsWith('file://')) {
return nextLoad(url, context);
}
// JSON files loaded as CommonJS are handled by this custom loader, because
// the default one doesn't work. For JSON loading to work we'd need the
// synchronous hooks that aren't supported yet, or avoid using the CommonJS
// compatibility.
if (
context.format === 'commonjs' &&
context.importAttributes?.type === 'json'
) {
try {
// TODO(Rugvip): Make sure this is valid JSON
const content = await readFile(fileURLToPath(url), 'utf8');
return {
source: `module.exports = (${content})`,
format: 'commonjs',
shortCircuit: true,
};
} catch {
// Let the default loader generate the error
return nextLoad(url, context);
}
}
const ext = extname(url);
// Non-TS files are handled by the default loader
if (!TS_EXTS.includes(ext)) {
return nextLoad(url, context);
}
const format = context.format ?? DEFAULT_MODULE_FORMAT;
// We have two choices at this point, we can either transform CommonJS files
// and return the transformed source code, or let the default loader handle
// them. If we transform them ourselves we will enter CommonJS compatibility
// mode in the new module system in Node.js, this effectively means all
// CommonJS loaded via `require` calls from this point will all be treated as
// if it was loaded via `import` calls from modules.
//
// The CommonJS compatibility layer will try to identify named exports and
// make them available directly, which is convenient as it avoids things like
// `import(...).then(m => m.default.foo)`, allowing you to instead write
// `import(...).then(m => m.foo)`. The compatibility layer doesn't always work
// all that well though, and can lead to module loading issues in many cases,
// especially for older code.
// This `if` block opts-out of using CommonJS compatibility mode by default,
// and instead leaves it to our existing loader to transform CommonJS. We do
// however use compatibility mode for the more explicit .cts file extension,
// allows for a way to opt-in to the new behavior.
//
// TODO(Rugvip): Once the synchronous hooks API is available for us to use, we might be able to adopt that instead
if (format === 'commonjs' && ext !== '.cts') {
return nextLoad(url, { ...context, format });
}
// If the Node.js version we're running supports TypeScript, i.e. type
// stripping, we hand over to the default loader. This is done for all cases
// except if we're loading a .ts file that's been resolved to CommonJS format.
// This is because these files aren't actually CommonJS in the Backstage build
// system, and need to be transformed to CommonJS.
if (
format === 'module-typescript' ||
(format === 'module-commonjs' && ext !== '.ts')
) {
return nextLoad(url, { ...context, format });
}
const transformed = await transformFile(fileURLToPath(url), {
sourceMaps: 'inline',
module: {
type: format === 'module' ? 'es6' : 'commonjs',
ignoreDynamic: true,
// This helps the Node.js CommonJS compat layer identify named exports.
exportInteropAnnotation: true,
},
jsc: {
target: 'es2023',
parser: {
syntax: 'typescript',
},
},
});
return {
...context,
shortCircuit: true,
source: transformed.code,
format,
responseURL: url,
};
}
'use strict';
var OpaqueType = require('../../opaque-internal/src/OpaqueType.cjs.js');
const OpaqueCliModule = OpaqueType.OpaqueType.create({
type: "@backstage/CliModule",
versions: ["v1"]
});
exports.OpaqueCliModule = OpaqueCliModule;
//# sourceMappingURL=InternalCliModule.cjs.js.map
{"version":3,"file":"InternalCliModule.cjs.js","sources":["../../../../cli-internal/src/InternalCliModule.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { CliCommand, CliModule } from '@backstage/cli-node';\nimport { OpaqueType } from '@internal/opaque';\n\nexport const OpaqueCliModule = OpaqueType.create<{\n public: CliModule;\n versions: {\n readonly version: 'v1';\n readonly packageName: string;\n readonly commands: Promise<ReadonlyArray<CliCommand>>;\n };\n}>({\n type: '@backstage/CliModule',\n versions: ['v1'],\n});\n"],"names":["OpaqueType"],"mappings":";;;;AAmBO,MAAM,eAAA,GAAkBA,sBAAW,MAAA,CAOvC;AAAA,EACD,IAAA,EAAM,sBAAA;AAAA,EACN,QAAA,EAAU,CAAC,IAAI;AACjB,CAAC;;;;"}
'use strict';
var OpaqueType = require('../../opaque-internal/src/OpaqueType.cjs.js');
const OpaqueCommandTreeNode = OpaqueType.OpaqueType.create({
type: "@backstage/CommandTreeNode",
versions: ["v1"]
});
const OpaqueCommandLeafNode = OpaqueType.OpaqueType.create({
type: "@backstage/CommandLeafNode",
versions: ["v1"]
});
function isCommandNodeHidden(node) {
if (OpaqueCommandLeafNode.isType(node)) {
const { command } = OpaqueCommandLeafNode.toInternal(node);
return !!command.deprecated || !!command.experimental;
}
const { children } = OpaqueCommandTreeNode.toInternal(node);
return children.every((child) => isCommandNodeHidden(child));
}
exports.OpaqueCommandLeafNode = OpaqueCommandLeafNode;
exports.OpaqueCommandTreeNode = OpaqueCommandTreeNode;
exports.isCommandNodeHidden = isCommandNodeHidden;
//# sourceMappingURL=InternalCommandNode.cjs.js.map
{"version":3,"file":"InternalCommandNode.cjs.js","sources":["../../../../cli-internal/src/InternalCommandNode.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { CliCommand, CliModule } from '@backstage/cli-node';\nimport { OpaqueType } from '@internal/opaque';\n\n/** @internal */\nexport interface CommandTreeNode {\n readonly $$type: '@backstage/CommandTreeNode';\n}\n\n/** @internal */\nexport interface CommandLeafNode {\n readonly $$type: '@backstage/CommandLeafNode';\n}\n\nexport type CommandNode = CommandTreeNode | CommandLeafNode;\n\nexport const OpaqueCommandTreeNode = OpaqueType.create<{\n public: CommandTreeNode;\n versions: {\n readonly version: 'v1';\n readonly name: string;\n readonly children: CommandNode[];\n };\n}>({\n type: '@backstage/CommandTreeNode',\n versions: ['v1'],\n});\n\nexport const OpaqueCommandLeafNode = OpaqueType.create<{\n public: CommandLeafNode;\n versions: {\n readonly version: 'v1';\n readonly name: string;\n readonly command: CliCommand;\n readonly module?: CliModule;\n };\n}>({\n type: '@backstage/CommandLeafNode',\n versions: ['v1'],\n});\n\n/**\n * Checks whether a command node should be hidden from help output.\n * Leaf nodes are hidden if they are deprecated or experimental.\n * Tree nodes are hidden if all of their children are hidden.\n */\nexport function isCommandNodeHidden(node: CommandNode): boolean {\n if (OpaqueCommandLeafNode.isType(node)) {\n const { command } = OpaqueCommandLeafNode.toInternal(node);\n return !!command.deprecated || !!command.experimental;\n }\n const { children } = OpaqueCommandTreeNode.toInternal(node);\n return children.every(child => isCommandNodeHidden(child));\n}\n"],"names":["OpaqueType"],"mappings":";;;;AA+BO,MAAM,qBAAA,GAAwBA,sBAAW,MAAA,CAO7C;AAAA,EACD,IAAA,EAAM,4BAAA;AAAA,EACN,QAAA,EAAU,CAAC,IAAI;AACjB,CAAC;AAEM,MAAM,qBAAA,GAAwBA,sBAAW,MAAA,CAQ7C;AAAA,EACD,IAAA,EAAM,4BAAA;AAAA,EACN,QAAA,EAAU,CAAC,IAAI;AACjB,CAAC;AAOM,SAAS,oBAAoB,IAAA,EAA4B;AAC9D,EAAA,IAAI,qBAAA,CAAsB,MAAA,CAAO,IAAI,CAAA,EAAG;AACtC,IAAA,MAAM,EAAE,OAAA,EAAQ,GAAI,qBAAA,CAAsB,WAAW,IAAI,CAAA;AACzD,IAAA,OAAO,CAAC,CAAC,OAAA,CAAQ,UAAA,IAAc,CAAC,CAAC,OAAA,CAAQ,YAAA;AAAA,EAC3C;AACA,EAAA,MAAM,EAAE,QAAA,EAAS,GAAI,qBAAA,CAAsB,WAAW,IAAI,CAAA;AAC1D,EAAA,OAAO,QAAA,CAAS,KAAA,CAAM,CAAA,KAAA,KAAS,mBAAA,CAAoB,KAAK,CAAC,CAAA;AAC3D;;;;;;"}
'use strict';
var InternalCliModule = require('../cli-internal/src/InternalCliModule.cjs.js');
require('../cli-internal/src/InternalCommandNode.cjs.js');
function createCliModule(options) {
if (!options.packageJson.name) {
throw new Error(
"The packageJson provided to createCliModule must have a name"
);
}
const commands = [];
const commandsPromise = Promise.resolve().then(() => options.init({ addCommand: (command) => commands.push(command) })).then(() => commands);
return InternalCliModule.OpaqueCliModule.createInstance("v1", {
packageName: options.packageJson.name,
commands: commandsPromise
});
}
exports.createCliModule = createCliModule;
//# sourceMappingURL=createCliModule.cjs.js.map
{"version":3,"file":"createCliModule.cjs.js","sources":["../../src/cli-module/createCliModule.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { OpaqueCliModule } from '@internal/cli';\nimport { CliCommand, CliModule } from './types';\n\n/**\n * Creates a new CLI plugin that provides commands to the Backstage CLI.\n *\n * The `init` callback is invoked immediately at creation time and is used\n * to register commands via the provided registry. The commands are then\n * made available to the CLI host once the returned promise resolves.\n *\n * @example\n * ```\n * import { createCliModule } from '@backstage/cli-node';\n * import packageJson from '../package.json';\n *\n * export default createCliModule({\n * packageJson,\n * init: async reg => {\n * reg.addCommand({\n * path: ['repo', 'test'],\n * description: 'Run tests across the repository',\n * execute: { loader: () => import('./commands/test') },\n * });\n * },\n * });\n * ```\n *\n * @public\n */\nexport function createCliModule(options: {\n /** The `package.json` contents of the plugin package, used to identify the plugin. */\n packageJson: { name: string };\n /**\n * An initialization callback that registers commands with the CLI.\n * Called immediately when the plugin is created.\n */\n init: (registry: {\n /** Registers a new command with the CLI. */\n addCommand: (command: CliCommand) => void;\n }) => Promise<void>;\n}): CliModule {\n if (!options.packageJson.name) {\n throw new Error(\n 'The packageJson provided to createCliModule must have a name',\n );\n }\n\n const commands: CliCommand[] = [];\n const commandsPromise = Promise.resolve()\n .then(() => options.init({ addCommand: command => commands.push(command) }))\n .then(() => commands);\n\n return OpaqueCliModule.createInstance('v1', {\n packageName: options.packageJson.name,\n commands: commandsPromise,\n });\n}\n"],"names":["OpaqueCliModule"],"mappings":";;;;;AA6CO,SAAS,gBAAgB,OAAA,EAWlB;AACZ,EAAA,IAAI,CAAC,OAAA,CAAQ,WAAA,CAAY,IAAA,EAAM;AAC7B,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AAEA,EAAA,MAAM,WAAyB,EAAC;AAChC,EAAA,MAAM,eAAA,GAAkB,QAAQ,OAAA,EAAQ,CACrC,KAAK,MAAM,OAAA,CAAQ,KAAK,EAAE,UAAA,EAAY,aAAW,QAAA,CAAS,IAAA,CAAK,OAAO,CAAA,EAAG,CAAC,CAAA,CAC1E,IAAA,CAAK,MAAM,QAAQ,CAAA;AAEtB,EAAA,OAAOA,iCAAA,CAAgB,eAAe,IAAA,EAAM;AAAA,IAC1C,WAAA,EAAa,QAAQ,WAAA,CAAY,IAAA;AAAA,IACjC,QAAA,EAAU;AAAA,GACX,CAAA;AACH;;;;"}
'use strict';
var InternalCliModule = require('../cli-internal/src/InternalCliModule.cjs.js');
var InternalCommandNode = require('../cli-internal/src/InternalCommandNode.cjs.js');
var commander = require('commander');
var chalk = require('chalk');
var errors = require('@backstage/errors');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
var chalk__default = /*#__PURE__*/_interopDefaultCompat(chalk);
function buildCommandGraph(commands) {
const graph = [];
for (const command of commands) {
const { path } = command;
let current = graph;
for (let i = 0; i < path.length - 1; i++) {
const name = path[i];
let next = current.find(
(n) => InternalCommandNode.OpaqueCommandTreeNode.isType(n) && InternalCommandNode.OpaqueCommandTreeNode.toInternal(n).name === name
);
if (!next) {
next = InternalCommandNode.OpaqueCommandTreeNode.createInstance("v1", {
name,
children: []
});
current.push(next);
}
current = InternalCommandNode.OpaqueCommandTreeNode.toInternal(next).children;
}
current.push(
InternalCommandNode.OpaqueCommandLeafNode.createInstance("v1", {
name: path[path.length - 1],
command
})
);
}
return graph;
}
function exitWithError(error) {
process.stderr.write(`
${chalk__default.default.red(errors.stringifyError(error))}
`);
process.exit(
errors.isError(error) && "code" in error && typeof error.code === "number" ? error.code : 1
);
}
function registerCommands(graph, program, programName) {
const queue = graph.map((node) => ({ node, argParser: program }));
while (queue.length) {
const { node, argParser } = queue.shift();
if (InternalCommandNode.OpaqueCommandTreeNode.isType(node)) {
const internal = InternalCommandNode.OpaqueCommandTreeNode.toInternal(node);
const treeParser = argParser.command(`${internal.name} [command]`, {
hidden: InternalCommandNode.isCommandNodeHidden(node)
}).description(internal.name);
queue.push(
...internal.children.map((child) => ({
node: child,
argParser: treeParser
}))
);
} else {
const internal = InternalCommandNode.OpaqueCommandLeafNode.toInternal(node);
argParser.command(internal.name, {
hidden: !!internal.command.deprecated || !!internal.command.experimental
}).description(internal.command.description).helpOption(false).allowUnknownOption(true).allowExcessArguments(true).action(async () => {
try {
const args = program.parseOptions(process.argv);
const nonProcessArgs = args.operands.slice(2);
const positionalArgs = [];
let index = 0;
for (let argIndex = 0; argIndex < nonProcessArgs.length; argIndex++) {
if (argIndex === index && internal.command.path[argIndex] === nonProcessArgs[argIndex]) {
index += 1;
continue;
}
positionalArgs.push(nonProcessArgs[argIndex]);
}
const context = {
args: [...positionalArgs, ...args.unknown],
info: {
usage: [programName, ...internal.command.path].join(" "),
name: internal.command.path.join(" ")
}
};
if (typeof internal.command.execute === "function") {
await internal.command.execute(context);
} else {
const mod = await internal.command.execute.loader();
const fn = typeof mod.default === "function" ? mod.default : mod.default.default;
await fn(context);
}
process.exit(0);
} catch (error) {
exitWithError(error);
}
});
}
}
}
async function runCliModule(options) {
const { module: cliModule, name, version } = options;
if (!InternalCliModule.OpaqueCliModule.isType(cliModule)) {
throw new Error(
`Invalid CLI module: expected a module created with createCliModule`
);
}
const internal = InternalCliModule.OpaqueCliModule.toInternal(cliModule);
const commands = await internal.commands;
const graph = buildCommandGraph(commands);
const program = new commander.Command();
program.name(name).allowUnknownOption(true).allowExcessArguments(true);
if (version) {
program.version(version);
}
registerCommands(graph, program, name);
program.on("command:*", () => {
console.log();
console.log(chalk__default.default.red(`Invalid command: ${program.args.join(" ")}`));
console.log();
program.outputHelp();
process.exit(1);
});
process.on("unhandledRejection", (rejection) => {
exitWithError(rejection);
});
await program.parseAsync(process.argv);
}
exports.runCliModule = runCliModule;
//# sourceMappingURL=runCliModule.cjs.js.map
{"version":3,"file":"runCliModule.cjs.js","sources":["../../src/cli-module/runCliModule.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n OpaqueCliModule,\n OpaqueCommandTreeNode,\n OpaqueCommandLeafNode,\n isCommandNodeHidden,\n} from '@internal/cli';\nimport type { CommandNode } from '@internal/cli';\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport { isError, stringifyError } from '@backstage/errors';\nimport type { CliModule, CliCommand } from './types';\n\nfunction buildCommandGraph(commands: ReadonlyArray<CliCommand>): CommandNode[] {\n const graph: CommandNode[] = [];\n\n for (const command of commands) {\n const { path } = command;\n let current = graph;\n\n for (let i = 0; i < path.length - 1; i++) {\n const name = path[i];\n let next = current.find(\n n =>\n OpaqueCommandTreeNode.isType(n) &&\n OpaqueCommandTreeNode.toInternal(n).name === name,\n );\n if (!next) {\n next = OpaqueCommandTreeNode.createInstance('v1', {\n name,\n children: [],\n });\n current.push(next);\n }\n current = OpaqueCommandTreeNode.toInternal(next).children;\n }\n\n current.push(\n OpaqueCommandLeafNode.createInstance('v1', {\n name: path[path.length - 1],\n command,\n }),\n );\n }\n\n return graph;\n}\n\nfunction exitWithError(error: unknown): never {\n process.stderr.write(`\\n${chalk.red(stringifyError(error))}\\n\\n`);\n process.exit(\n isError(error) && 'code' in error && typeof error.code === 'number'\n ? error.code\n : 1,\n );\n}\n\nfunction registerCommands(\n graph: CommandNode[],\n program: Command,\n programName: string,\n): void {\n const queue = graph.map(node => ({ node, argParser: program }));\n\n while (queue.length) {\n const { node, argParser } = queue.shift()!;\n\n if (OpaqueCommandTreeNode.isType(node)) {\n const internal = OpaqueCommandTreeNode.toInternal(node);\n const treeParser = argParser\n .command(`${internal.name} [command]`, {\n hidden: isCommandNodeHidden(node),\n })\n .description(internal.name);\n\n queue.push(\n ...internal.children.map(child => ({\n node: child,\n argParser: treeParser,\n })),\n );\n } else {\n const internal = OpaqueCommandLeafNode.toInternal(node);\n argParser\n .command(internal.name, {\n hidden:\n !!internal.command.deprecated || !!internal.command.experimental,\n })\n .description(internal.command.description)\n .helpOption(false)\n .allowUnknownOption(true)\n .allowExcessArguments(true)\n .action(async () => {\n try {\n const args = program.parseOptions(process.argv);\n\n const nonProcessArgs = args.operands.slice(2);\n const positionalArgs = [];\n let index = 0;\n for (\n let argIndex = 0;\n argIndex < nonProcessArgs.length;\n argIndex++\n ) {\n if (\n argIndex === index &&\n internal.command.path[argIndex] === nonProcessArgs[argIndex]\n ) {\n index += 1;\n continue;\n }\n positionalArgs.push(nonProcessArgs[argIndex]);\n }\n const context = {\n args: [...positionalArgs, ...args.unknown],\n info: {\n usage: [programName, ...internal.command.path].join(' '),\n name: internal.command.path.join(' '),\n },\n };\n\n if (typeof internal.command.execute === 'function') {\n await internal.command.execute(context);\n } else {\n const mod = await internal.command.execute.loader();\n const fn =\n typeof mod.default === 'function'\n ? mod.default\n : (mod.default as any).default;\n await fn(context);\n }\n process.exit(0);\n } catch (error: unknown) {\n exitWithError(error);\n }\n });\n }\n }\n}\n\n/**\n * Runs a CLI module as a standalone program.\n *\n * This helper extracts the commands from a {@link CliModule} and exposes\n * them as a fully functional CLI with help output and argument parsing.\n * It is intended to be called from a module package's `bin` entry point\n * so that the module can be executed directly without being wired into\n * a larger CLI host.\n *\n * @example\n * ```ts\n * #!/usr/bin/env node\n * import { runCliModule } from '@backstage/cli-node';\n * import cliModule from './index';\n *\n * runCliModule({\n * module: cliModule,\n * name: 'backstage-auth',\n * version: require('../package.json').version,\n * });\n * ```\n *\n * @public\n */\nexport async function runCliModule(options: {\n /** The CLI module to run. */\n module: CliModule;\n /** The program name shown in help output and usage strings. */\n name: string;\n /** The version string shown when `--version` is passed. */\n version?: string;\n}): Promise<void> {\n const { module: cliModule, name, version } = options;\n\n if (!OpaqueCliModule.isType(cliModule)) {\n throw new Error(\n `Invalid CLI module: expected a module created with createCliModule`,\n );\n }\n\n const internal = OpaqueCliModule.toInternal(cliModule);\n const commands = await internal.commands;\n const graph = buildCommandGraph(commands);\n\n const program = new Command();\n program.name(name).allowUnknownOption(true).allowExcessArguments(true);\n\n if (version) {\n program.version(version);\n }\n\n registerCommands(graph, program, name);\n\n program.on('command:*', () => {\n console.log();\n console.log(chalk.red(`Invalid command: ${program.args.join(' ')}`));\n console.log();\n program.outputHelp();\n process.exit(1);\n });\n\n process.on('unhandledRejection', rejection => {\n exitWithError(rejection);\n });\n\n await program.parseAsync(process.argv);\n}\n"],"names":["OpaqueCommandTreeNode","OpaqueCommandLeafNode","chalk","stringifyError","isError","isCommandNodeHidden","OpaqueCliModule","Command"],"mappings":"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"}
'use strict';
class OpaqueType {
/**
* Creates a new opaque type.
*
* @param options.type The type identifier of the opaque type
* @param options.versions The available versions of the opaque type
* @returns A new opaque type helper
*/
static create(options) {
return new OpaqueType(options.type, new Set(options.versions));
}
#type;
#versions;
constructor(type, versions) {
this.#type = type;
this.#versions = versions;
}
/**
* The internal version of the opaque type, used like this: `typeof MyOpaqueType.TPublic`
*
* @remarks
*
* This property is only useful for type checking, its runtime value is `undefined`.
*/
TPublic = void 0;
/**
* The internal version of the opaque type, used like this: `typeof MyOpaqueType.TInternal`
*
* @remarks
*
* This property is only useful for type checking, its runtime value is `undefined`.
*/
TInternal = void 0;
/**
* @param value Input value expected to be an instance of this opaque type
* @returns True if the value matches this opaque type
*/
isType = (value) => {
return this.#isThisInternalType(value);
};
/**
* @param value Input value expected to be an instance of this opaque type
* @throws If the value is not an instance of this opaque type or is of an unsupported version
* @returns The internal version of the opaque type
*/
toInternal = (value) => {
if (!this.#isThisInternalType(value)) {
throw new TypeError(
`Invalid opaque type, expected '${this.#type}', but got '${this.#stringifyUnknown(value)}'`
);
}
if (!this.#versions.has(value.version)) {
const versions = Array.from(this.#versions).map(this.#stringifyVersion);
if (versions.length > 1) {
versions[versions.length - 1] = `or ${versions[versions.length - 1]}`;
}
const expected = versions.length > 2 ? versions.join(", ") : versions.join(" ");
throw new TypeError(
`Invalid opaque type instance, got version ${this.#stringifyVersion(
value.version
)}, expected ${expected}`
);
}
return value;
};
/**
* Creates an instance of the opaque type, returning the public type.
*
* @param version The version of the instance to create
* @param value The remaining public and internal properties of the instance
* @returns An instance of the opaque type
*/
createInstance(version, props) {
return Object.assign(props, {
$$type: this.#type,
...version && { version }
});
}
#isThisInternalType(value) {
if (value === null || typeof value !== "object") {
return false;
}
return value.$$type === this.#type;
}
#stringifyUnknown(value) {
if (typeof value !== "object") {
return `<${typeof value}>`;
}
if (value === null) {
return "<null>";
}
if ("$$type" in value) {
return String(value.$$type);
}
return String(value);
}
#stringifyVersion = (version) => {
return version ? `'${version}'` : "undefined";
};
}
exports.OpaqueType = OpaqueType;
//# sourceMappingURL=OpaqueType.cjs.js.map
{"version":3,"file":"OpaqueType.cjs.js","sources":["../../../../opaque-internal/src/OpaqueType.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// TODO(Rugvip): This lives here temporarily, but should be moved to a more\n// central location. It's useful for backend packages too so we'll need to have\n// it in a common package, but it might also be that we want to make it\n// available publicly too in which case it would make sense to have this be part\n// of @backstage/version-bridge. The problem with exporting it from there is\n// that it would need to be very stable at that point, so it might be a bit\n// early to put it there already.\n\n/**\n * A helper for working with opaque types.\n */\nexport class OpaqueType<\n T extends {\n public: { $$type: string };\n versions: { version: string | undefined };\n },\n> {\n /**\n * Creates a new opaque type.\n *\n * @param options.type The type identifier of the opaque type\n * @param options.versions The available versions of the opaque type\n * @returns A new opaque type helper\n */\n static create<\n T extends {\n public: { $$type: string };\n versions: { version: string | undefined };\n },\n >(options: {\n type: T['public']['$$type'];\n versions: Array<T['versions']['version']>;\n }) {\n return new OpaqueType<T>(options.type, new Set(options.versions));\n }\n\n #type: string;\n #versions: Set<string | undefined>;\n\n private constructor(type: string, versions: Set<string | undefined>) {\n this.#type = type;\n this.#versions = versions;\n }\n\n /**\n * The internal version of the opaque type, used like this: `typeof MyOpaqueType.TPublic`\n *\n * @remarks\n *\n * This property is only useful for type checking, its runtime value is `undefined`.\n */\n TPublic: T['public'] = undefined as any;\n\n /**\n * The internal version of the opaque type, used like this: `typeof MyOpaqueType.TInternal`\n *\n * @remarks\n *\n * This property is only useful for type checking, its runtime value is `undefined`.\n */\n TInternal: T['public'] & T['versions'] = undefined as any;\n\n /**\n * @param value Input value expected to be an instance of this opaque type\n * @returns True if the value matches this opaque type\n */\n isType = (value: unknown): value is T['public'] => {\n return this.#isThisInternalType(value);\n };\n\n /**\n * @param value Input value expected to be an instance of this opaque type\n * @throws If the value is not an instance of this opaque type or is of an unsupported version\n * @returns The internal version of the opaque type\n */\n toInternal = (value: unknown): T['public'] & T['versions'] => {\n if (!this.#isThisInternalType(value)) {\n throw new TypeError(\n `Invalid opaque type, expected '${\n this.#type\n }', but got '${this.#stringifyUnknown(value)}'`,\n );\n }\n\n if (!this.#versions.has(value.version)) {\n const versions = Array.from(this.#versions).map(this.#stringifyVersion);\n if (versions.length > 1) {\n versions[versions.length - 1] = `or ${versions[versions.length - 1]}`;\n }\n const expected =\n versions.length > 2 ? versions.join(', ') : versions.join(' ');\n throw new TypeError(\n `Invalid opaque type instance, got version ${this.#stringifyVersion(\n value.version,\n )}, expected ${expected}`,\n );\n }\n\n return value;\n };\n\n /**\n * Creates an instance of the opaque type, returning the public type.\n *\n * @param version The version of the instance to create\n * @param value The remaining public and internal properties of the instance\n * @returns An instance of the opaque type\n */\n createInstance<\n TVersion extends T['versions']['version'],\n TPublic extends T['public'],\n >(\n version: TVersion,\n props: Omit<T['public'], '$$type'> &\n (T['versions'] extends infer UVersion\n ? UVersion extends { version: TVersion }\n ? Omit<UVersion, 'version'>\n : never\n : never) &\n Object, // & Object to allow for object properties too, e.g. toString()\n ): TPublic {\n return Object.assign(props as object, {\n $$type: this.#type,\n ...(version && { version }),\n }) as unknown as TPublic;\n }\n\n #isThisInternalType(value: unknown): value is T['public'] & T['versions'] {\n if (value === null || typeof value !== 'object') {\n return false;\n }\n return (value as T['public']).$$type === this.#type;\n }\n\n #stringifyUnknown(value: unknown) {\n if (typeof value !== 'object') {\n return `<${typeof value}>`;\n }\n if (value === null) {\n return '<null>';\n }\n if ('$$type' in value) {\n return String(value.$$type);\n }\n return String(value);\n }\n\n #stringifyVersion = (version: string | undefined) => {\n return version ? `'${version}'` : 'undefined';\n };\n}\n"],"names":[],"mappings":"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"}
+7
-2
# @backstage/cli-node
## 0.0.0-nightly-20260316031711
## 0.0.0-nightly-20260317031259
### Minor Changes
- 7d055ef: Added `createCliModule` API and related types for building Backstage CLI plugins.
### Patch Changes
- 94a885a: Added a new `cli-module` package role for packages that provide CLI plugin extensions.
- 61cb976: Added `toString()` method to `Lockfile` for serializing lockfiles back to string format.

@@ -13,3 +18,3 @@ - 06c2015: Added `runConcurrentTasks` and `runWorkerQueueThreads` utilities, moved from the `@backstage/cli` internal code.

- Updated dependencies
- @backstage/cli-common@0.0.0-nightly-20260316031711
- @backstage/cli-common@0.0.0-nightly-20260317031259
- @backstage/errors@1.2.7

@@ -16,0 +21,0 @@ - @backstage/types@1.2.2

'use strict';
var SuccessCache = require('./cache/SuccessCache.cjs.js');
var createCliModule = require('./cli-module/createCliModule.cjs.js');
var runCliModule = require('./cli-module/runCliModule.cjs.js');
var runConcurrentTasks = require('./concurrency/runConcurrentTasks.cjs.js');

@@ -16,2 +18,4 @@ var runWorkerQueueThreads = require('./concurrency/runWorkerQueueThreads.cjs.js');

exports.SuccessCache = SuccessCache.SuccessCache;
exports.createCliModule = createCliModule.createCliModule;
exports.runCliModule = runCliModule.runCliModule;
exports.runConcurrentTasks = runConcurrentTasks.runConcurrentTasks;

@@ -18,0 +22,0 @@ exports.runWorkerQueueThreads = runWorkerQueueThreads.runWorkerQueueThreads;

@@ -1,1 +0,1 @@

{"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;"}
{"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;"}

@@ -28,2 +28,175 @@ import { Package } from '@manypkg/get-packages';

/**
* The context provided to a CLI command at the time of execution.
*
* Contains the parsed arguments and metadata about the command being run.
*
* @public
*/
interface CliCommandContext {
/**
* The remaining arguments passed to the command after the command path
* has been resolved. This includes both positional arguments and flags.
*
* For example, running `backstage-cli repo test --verbose src/` would
* result in `args` being `['--verbose', 'src/']`.
*/
args: string[];
/**
* Metadata about the command being executed.
*/
info: {
/**
* The full usage string of the command including the program name,
* for example `"backstage-cli repo test"`.
*/
usage: string;
/**
* The name of the command as defined by its path,
* for example `"repo test"`.
*/
name: string;
};
}
/**
* A command definition for a Backstage CLI plugin.
*
* Each command is identified by a `path` that determines its position in
* the command tree. For example, a path of `['repo', 'test']` registers
* the command as `backstage-cli repo test`.
*
* Commands can either provide an `execute` function directly, or use a
* `loader` for deferred loading of the implementation. The loader pattern
* is recommended for commands with heavy dependencies, as it avoids
* loading the implementation until the command is actually invoked.
*
* @public
*/
interface CliCommand {
/**
* The path segments that define the command's position in the CLI tree.
* For example, `['repo', 'test']` maps to `backstage-cli repo test`.
*/
path: string[];
/**
* A short description of the command, displayed in help output.
*/
description: string;
/**
* If `true`, the command is deprecated and will be hidden from help output
* but can still be invoked.
*/
deprecated?: boolean;
/**
* If `true`, the command is experimental and will be hidden from help
* output but can still be invoked.
*/
experimental?: boolean;
/**
* The command implementation, either as a direct function or as a loader
* that returns the implementation as a default export. The loader form
* is useful for deferring heavy imports until the command is invoked.
*
* @example
* Direct execution:
* ```
* execute: async ({ args }) => { ... }
* ```
*
* @example
* Deferred loading:
* ```
* execute: { loader: () => import('./my-command') }
* ```
*/
execute: ((context: CliCommandContext) => Promise<void>) | {
loader: () => Promise<{
default: (context: CliCommandContext) => Promise<void>;
}>;
};
}
/**
* An opaque representation of a Backstage CLI plugin, created
* using {@link createCliModule}.
*
* @public
*/
interface CliModule {
readonly $$type: '@backstage/CliModule';
}
/**
* Creates a new CLI plugin that provides commands to the Backstage CLI.
*
* The `init` callback is invoked immediately at creation time and is used
* to register commands via the provided registry. The commands are then
* made available to the CLI host once the returned promise resolves.
*
* @example
* ```
* import { createCliModule } from '@backstage/cli-node';
* import packageJson from '../package.json';
*
* export default createCliModule({
* packageJson,
* init: async reg => {
* reg.addCommand({
* path: ['repo', 'test'],
* description: 'Run tests across the repository',
* execute: { loader: () => import('./commands/test') },
* });
* },
* });
* ```
*
* @public
*/
declare function createCliModule(options: {
/** The `package.json` contents of the plugin package, used to identify the plugin. */
packageJson: {
name: string;
};
/**
* An initialization callback that registers commands with the CLI.
* Called immediately when the plugin is created.
*/
init: (registry: {
/** Registers a new command with the CLI. */
addCommand: (command: CliCommand) => void;
}) => Promise<void>;
}): CliModule;
/**
* Runs a CLI module as a standalone program.
*
* This helper extracts the commands from a {@link CliModule} and exposes
* them as a fully functional CLI with help output and argument parsing.
* It is intended to be called from a module package's `bin` entry point
* so that the module can be executed directly without being wired into
* a larger CLI host.
*
* @example
* ```ts
* #!/usr/bin/env node
* import { runCliModule } from '@backstage/cli-node';
* import cliModule from './index';
*
* runCliModule({
* module: cliModule,
* name: 'backstage-auth',
* version: require('../package.json').version,
* });
* ```
*
* @public
*/
declare function runCliModule(options: {
/** The CLI module to run. */
module: CliModule;
/** The program name shown in help output and usage strings. */
name: string;
/** The version string shown when `--version` is passed. */
version?: string;
}): Promise<void>;
/**
* Options for {@link runConcurrentTasks}.

@@ -118,3 +291,3 @@ *

*/
type PackageRole = 'frontend' | 'backend' | 'cli' | 'web-library' | 'node-library' | 'common-library' | 'frontend-plugin' | 'frontend-plugin-module' | 'backend-plugin' | 'backend-plugin-module';
type PackageRole = 'frontend' | 'backend' | 'cli' | 'cli-module' | 'web-library' | 'node-library' | 'common-library' | 'frontend-plugin' | 'frontend-plugin-module' | 'backend-plugin' | 'backend-plugin-module';
/**

@@ -411,3 +584,3 @@ * A type of platform that a package can be built for.

export { GitUtils, Lockfile, PackageGraph, PackageRoles, SuccessCache, hasBackstageYarnPlugin, isMonoRepo, packageFeatureType, runConcurrentTasks, runWorkerQueueThreads };
export type { BackstagePackage, BackstagePackageFeatureType, BackstagePackageJson, ConcurrentTasksOptions, LockfileDiff, LockfileDiffEntry, LockfileQueryEntry, PackageGraphNode, PackageOutputType, PackagePlatform, PackageRole, PackageRoleInfo, WorkerQueueThreadsOptions };
export { GitUtils, Lockfile, PackageGraph, PackageRoles, SuccessCache, createCliModule, hasBackstageYarnPlugin, isMonoRepo, packageFeatureType, runCliModule, runConcurrentTasks, runWorkerQueueThreads };
export type { BackstagePackage, BackstagePackageFeatureType, BackstagePackageJson, CliCommand, CliCommandContext, CliModule, ConcurrentTasksOptions, LockfileDiff, LockfileDiffEntry, LockfileQueryEntry, PackageGraphNode, PackageOutputType, PackagePlatform, PackageRole, PackageRoleInfo, WorkerQueueThreadsOptions };

@@ -22,2 +22,7 @@ 'use strict';

{
role: "cli-module",
platform: "node",
output: ["types", "cjs"]
},
{
role: "web-library",

@@ -24,0 +29,0 @@ platform: "web",

@@ -1,1 +0,1 @@

{"version":3,"file":"PackageRoles.cjs.js","sources":["../../src/roles/PackageRoles.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { z } from 'zod';\nimport { PackageRole, PackageRoleInfo } from './types';\n\nconst packageRoleInfos: PackageRoleInfo[] = [\n {\n role: 'frontend',\n platform: 'web',\n output: ['bundle'],\n },\n {\n role: 'backend',\n platform: 'node',\n output: ['bundle'],\n },\n {\n role: 'cli',\n platform: 'node',\n output: ['cjs'],\n },\n {\n role: 'web-library',\n platform: 'web',\n output: ['types', 'esm'],\n },\n {\n role: 'node-library',\n platform: 'node',\n output: ['types', 'cjs'],\n },\n {\n role: 'common-library',\n platform: 'common',\n output: ['types', 'esm', 'cjs'],\n },\n {\n role: 'frontend-plugin',\n platform: 'web',\n output: ['types', 'esm'],\n },\n {\n role: 'frontend-plugin-module',\n platform: 'web',\n output: ['types', 'esm'],\n },\n {\n role: 'frontend-dynamic-container' as PackageRole, // experimental\n platform: 'web',\n output: ['bundle'],\n },\n {\n role: 'backend-plugin',\n platform: 'node',\n output: ['types', 'cjs'],\n },\n {\n role: 'backend-plugin-module',\n platform: 'node',\n output: ['types', 'cjs'],\n },\n];\n\nconst readSchema = z.object({\n name: z.string().optional(),\n backstage: z\n .object({\n role: z.string().optional(),\n })\n .optional(),\n});\n\nconst detectionSchema = z.object({\n name: z.string().optional(),\n scripts: z\n .object({\n start: z.string().optional(),\n build: z.string().optional(),\n })\n .optional(),\n publishConfig: z\n .object({\n main: z.string().optional(),\n types: z.string().optional(),\n module: z.string().optional(),\n })\n .optional(),\n main: z.string().optional(),\n types: z.string().optional(),\n module: z.string().optional(),\n});\n\n/**\n * Utilities for working with Backstage package roles.\n *\n * @public\n */\nexport class PackageRoles {\n /**\n * Get the associated info for a package role.\n */\n static getRoleInfo(role: string): PackageRoleInfo {\n const roleInfo = packageRoleInfos.find(r => r.role === role);\n if (!roleInfo) {\n throw new Error(`Unknown package role '${role}'`);\n }\n return roleInfo;\n }\n\n /**\n * Given package JSON data, get the package role.\n */\n static getRoleFromPackage(pkgJson: unknown): PackageRole | undefined {\n const pkg = readSchema.parse(pkgJson);\n\n if (pkg.backstage) {\n const { role } = pkg.backstage;\n if (!role) {\n throw new Error(\n `Package ${pkg.name} must specify a role in the \"backstage\" field`,\n );\n }\n\n return this.getRoleInfo(role).role;\n }\n\n return undefined;\n }\n\n /**\n * Attempt to detect the role of a package from its package.json.\n */\n static detectRoleFromPackage(pkgJson: unknown): PackageRole | undefined {\n const pkg = detectionSchema.parse(pkgJson);\n\n if (pkg.scripts?.start?.includes('app:serve')) {\n return 'frontend';\n }\n if (pkg.scripts?.build?.includes('backend:bundle')) {\n return 'backend';\n }\n if (\n pkg.name?.includes('plugin-') &&\n pkg.name?.includes('-backend-module-')\n ) {\n return 'backend-plugin-module';\n }\n if (pkg.name?.includes('plugin-') && pkg.name?.includes('-module-')) {\n return 'frontend-plugin-module';\n }\n if (pkg.scripts?.start?.includes('plugin:serve')) {\n return 'frontend-plugin';\n }\n if (pkg.scripts?.start?.includes('backend:dev')) {\n return 'backend-plugin';\n }\n\n const mainEntry = pkg.publishConfig?.main || pkg.main;\n const moduleEntry = pkg.publishConfig?.module || pkg.module;\n const typesEntry = pkg.publishConfig?.types || pkg.types;\n if (typesEntry) {\n if (mainEntry && moduleEntry) {\n return 'common-library';\n }\n if (moduleEntry || mainEntry?.endsWith('.esm.js')) {\n return 'web-library';\n }\n if (mainEntry) {\n return 'node-library';\n }\n } else if (mainEntry) {\n return 'cli';\n }\n\n return undefined;\n }\n}\n"],"names":["z"],"mappings":"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"}
{"version":3,"file":"PackageRoles.cjs.js","sources":["../../src/roles/PackageRoles.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { z } from 'zod';\nimport { PackageRole, PackageRoleInfo } from './types';\n\nconst packageRoleInfos: PackageRoleInfo[] = [\n {\n role: 'frontend',\n platform: 'web',\n output: ['bundle'],\n },\n {\n role: 'backend',\n platform: 'node',\n output: ['bundle'],\n },\n {\n role: 'cli',\n platform: 'node',\n output: ['cjs'],\n },\n {\n role: 'cli-module',\n platform: 'node',\n output: ['types', 'cjs'],\n },\n {\n role: 'web-library',\n platform: 'web',\n output: ['types', 'esm'],\n },\n {\n role: 'node-library',\n platform: 'node',\n output: ['types', 'cjs'],\n },\n {\n role: 'common-library',\n platform: 'common',\n output: ['types', 'esm', 'cjs'],\n },\n {\n role: 'frontend-plugin',\n platform: 'web',\n output: ['types', 'esm'],\n },\n {\n role: 'frontend-plugin-module',\n platform: 'web',\n output: ['types', 'esm'],\n },\n {\n role: 'frontend-dynamic-container' as PackageRole, // experimental\n platform: 'web',\n output: ['bundle'],\n },\n {\n role: 'backend-plugin',\n platform: 'node',\n output: ['types', 'cjs'],\n },\n {\n role: 'backend-plugin-module',\n platform: 'node',\n output: ['types', 'cjs'],\n },\n];\n\nconst readSchema = z.object({\n name: z.string().optional(),\n backstage: z\n .object({\n role: z.string().optional(),\n })\n .optional(),\n});\n\nconst detectionSchema = z.object({\n name: z.string().optional(),\n scripts: z\n .object({\n start: z.string().optional(),\n build: z.string().optional(),\n })\n .optional(),\n publishConfig: z\n .object({\n main: z.string().optional(),\n types: z.string().optional(),\n module: z.string().optional(),\n })\n .optional(),\n main: z.string().optional(),\n types: z.string().optional(),\n module: z.string().optional(),\n});\n\n/**\n * Utilities for working with Backstage package roles.\n *\n * @public\n */\nexport class PackageRoles {\n /**\n * Get the associated info for a package role.\n */\n static getRoleInfo(role: string): PackageRoleInfo {\n const roleInfo = packageRoleInfos.find(r => r.role === role);\n if (!roleInfo) {\n throw new Error(`Unknown package role '${role}'`);\n }\n return roleInfo;\n }\n\n /**\n * Given package JSON data, get the package role.\n */\n static getRoleFromPackage(pkgJson: unknown): PackageRole | undefined {\n const pkg = readSchema.parse(pkgJson);\n\n if (pkg.backstage) {\n const { role } = pkg.backstage;\n if (!role) {\n throw new Error(\n `Package ${pkg.name} must specify a role in the \"backstage\" field`,\n );\n }\n\n return this.getRoleInfo(role).role;\n }\n\n return undefined;\n }\n\n /**\n * Attempt to detect the role of a package from its package.json.\n */\n static detectRoleFromPackage(pkgJson: unknown): PackageRole | undefined {\n const pkg = detectionSchema.parse(pkgJson);\n\n if (pkg.scripts?.start?.includes('app:serve')) {\n return 'frontend';\n }\n if (pkg.scripts?.build?.includes('backend:bundle')) {\n return 'backend';\n }\n if (\n pkg.name?.includes('plugin-') &&\n pkg.name?.includes('-backend-module-')\n ) {\n return 'backend-plugin-module';\n }\n if (pkg.name?.includes('plugin-') && pkg.name?.includes('-module-')) {\n return 'frontend-plugin-module';\n }\n if (pkg.scripts?.start?.includes('plugin:serve')) {\n return 'frontend-plugin';\n }\n if (pkg.scripts?.start?.includes('backend:dev')) {\n return 'backend-plugin';\n }\n\n const mainEntry = pkg.publishConfig?.main || pkg.main;\n const moduleEntry = pkg.publishConfig?.module || pkg.module;\n const typesEntry = pkg.publishConfig?.types || pkg.types;\n if (typesEntry) {\n if (mainEntry && moduleEntry) {\n return 'common-library';\n }\n if (moduleEntry || mainEntry?.endsWith('.esm.js')) {\n return 'web-library';\n }\n if (mainEntry) {\n return 'node-library';\n }\n } else if (mainEntry) {\n return 'cli';\n }\n\n return undefined;\n }\n}\n"],"names":["z"],"mappings":"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"}
{
"name": "@backstage/cli-node",
"version": "0.0.0-nightly-20260316031711",
"version": "0.0.0-nightly-20260317031259",
"description": "Node.js library for Backstage CLIs",

@@ -23,3 +23,4 @@ "backstage": {

"files": [
"dist"
"dist",
"config"
],

@@ -35,3 +36,3 @@ "scripts": {

"dependencies": {
"@backstage/cli-common": "0.0.0-nightly-20260316031711",
"@backstage/cli-common": "0.0.0-nightly-20260317031259",
"@backstage/errors": "1.2.7",

@@ -42,3 +43,6 @@ "@backstage/types": "1.2.2",

"@yarnpkg/parsers": "^3.0.0",
"chalk": "^4.0.0",
"commander": "^12.0.0",
"fs-extra": "^11.2.0",
"pirates": "^4.0.6",
"semver": "^7.5.3",

@@ -49,7 +53,15 @@ "yaml": "^2.0.0",

"devDependencies": {
"@backstage/backend-test-utils": "0.0.0-nightly-20260316031711",
"@backstage/cli": "0.0.0-nightly-20260316031711",
"@backstage/test-utils": "0.0.0-nightly-20260316031711",
"@backstage/backend-test-utils": "0.0.0-nightly-20260317031259",
"@backstage/cli": "0.0.0-nightly-20260317031259",
"@backstage/test-utils": "0.0.0-nightly-20260317031259",
"@types/yarnpkg__lockfile": "^1.1.4"
},
"peerDependencies": {
"@swc/core": "^1.15.6"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
}
},
"typesVersions": {

@@ -56,0 +68,0 @@ "*": {