Split CLI modules into separate packages
Extract each CLI module from packages/cli/src/modules/ into its own package under packages/cli-module-*. This enables independent versioning and clearer dependency boundaries for each CLI capability. Module mapping: - auth → @backstage/cli-module-auth - build → @backstage/cli-module-build - config → @backstage/cli-module-config - create-github-app → @backstage/cli-module-create-github-app - info → @backstage/cli-module-info - lint → @backstage/cli-module-lint - maintenance → @backstage/cli-module-maintenance - migrate → @backstage/cli-module-migrate - new → @backstage/cli-module-new - test → @backstage/cli-module-test-jest - translations → @backstage/cli-module-translations Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Copyright 2025 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 { cli } from 'cleye';
|
||||
const { version: cliVersion } = require('../../../package.json') as {
|
||||
version: string;
|
||||
};
|
||||
import os from 'node:os';
|
||||
import { runOutput, targetPaths, findOwnPaths } from '@backstage/cli-common';
|
||||
import {
|
||||
BackstagePackageJson,
|
||||
Lockfile,
|
||||
PackageGraph,
|
||||
} from '@backstage/cli-node';
|
||||
import { minimatch } from 'minimatch';
|
||||
import fs from 'fs-extra';
|
||||
import type { CliCommandContext } from '@backstage/cli-node';
|
||||
|
||||
/**
|
||||
* Attempts to read package.json from node_modules for a given package name.
|
||||
* Returns undefined if the package.json cannot be read.
|
||||
*/
|
||||
function tryReadPackageJson(
|
||||
packageName: string,
|
||||
targetPath: string,
|
||||
): BackstagePackageJson | undefined {
|
||||
try {
|
||||
return require(require.resolve(`${packageName}/package.json`, {
|
||||
paths: [targetPath],
|
||||
}));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a package has a backstage field in its package.json
|
||||
*/
|
||||
function hasBackstageField(packageName: string, targetPath: string): boolean {
|
||||
const pkg = tryReadPackageJson(packageName, targetPath);
|
||||
return pkg?.backstage !== undefined;
|
||||
}
|
||||
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
const {
|
||||
flags: { include, format },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
booleanFlagNegation: true,
|
||||
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 */
|
||||
const isLocal = fs.existsSync(findOwnPaths(__dirname).resolve('./src'));
|
||||
|
||||
const backstageFile = targetPaths.resolveRoot('backstage.json');
|
||||
let backstageVersion = 'N/A';
|
||||
if (fs.existsSync(backstageFile)) {
|
||||
try {
|
||||
const backstageJson = await fs.readJSON(backstageFile);
|
||||
backstageVersion = backstageJson.version ?? 'N/A';
|
||||
} catch (error) {
|
||||
if (options.format !== 'json') {
|
||||
console.warn(
|
||||
'The "backstage.json" file is not in the expected format',
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build system info
|
||||
const systemInfo = {
|
||||
os: `${os.type} ${os.release} - ${os.platform}/${os.arch}`,
|
||||
node: process.version,
|
||||
yarn: yarnVersion,
|
||||
cli: { version: cliVersion, local: isLocal },
|
||||
backstage: backstageVersion,
|
||||
};
|
||||
|
||||
const lockfilePath = targetPaths.resolveRoot('yarn.lock');
|
||||
const lockfile = await Lockfile.load(lockfilePath);
|
||||
const targetPath = targetPaths.rootDir;
|
||||
|
||||
// Get workspace package names and their versions
|
||||
const workspacePackages = new Map<string, string>();
|
||||
try {
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
for (const pkg of packages) {
|
||||
workspacePackages.set(pkg.packageJson.name, pkg.packageJson.version);
|
||||
}
|
||||
} catch {
|
||||
// If we can't list workspace packages, continue without them
|
||||
}
|
||||
|
||||
// Collect all package names from lockfile
|
||||
const allPackages = [...lockfile.keys()];
|
||||
const includePatterns = options.include || [];
|
||||
|
||||
// Collect installed (non-local) packages
|
||||
const installedDeps = new Set<string>();
|
||||
// Collect local workspace packages
|
||||
const localDeps = new Set<string>();
|
||||
|
||||
// Process @backstage/* packages
|
||||
for (const pkg of allPackages) {
|
||||
if (pkg.startsWith('@backstage/')) {
|
||||
if (workspacePackages.has(pkg)) {
|
||||
localDeps.add(pkg);
|
||||
} else {
|
||||
installedDeps.add(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process packages matching --include patterns
|
||||
for (const pattern of includePatterns) {
|
||||
for (const pkg of allPackages) {
|
||||
if (minimatch(pkg, pattern)) {
|
||||
if (workspacePackages.has(pkg)) {
|
||||
localDeps.add(pkg);
|
||||
} else {
|
||||
installedDeps.add(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process packages with backstage field in their package.json
|
||||
for (const pkg of allPackages) {
|
||||
// Skip @backstage/* packages (already processed above)
|
||||
if (pkg.startsWith('@backstage/')) {
|
||||
continue;
|
||||
}
|
||||
if (workspacePackages.has(pkg)) {
|
||||
// Check if local package has backstage field
|
||||
if (hasBackstageField(pkg, targetPath)) {
|
||||
localDeps.add(pkg);
|
||||
}
|
||||
} else if (hasBackstageField(pkg, targetPath)) {
|
||||
installedDeps.add(pkg);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get versions for a package as an array of objects
|
||||
const getVersions = (dep: string): Array<{ version: string }> => {
|
||||
const entries = lockfile.get(dep);
|
||||
if (!entries) {
|
||||
return [{ version: 'unknown' }];
|
||||
}
|
||||
const versions = [...new Set(entries.map(i => i.version))];
|
||||
return versions.map(v => ({ version: v }));
|
||||
};
|
||||
|
||||
const sortedInstalled = [...installedDeps].sort();
|
||||
const sortedLocal = [...localDeps].sort();
|
||||
|
||||
// If format is json, output JSON to stdout
|
||||
if (options.format === 'json') {
|
||||
const output = {
|
||||
system: systemInfo,
|
||||
dependencies: Object.fromEntries(
|
||||
sortedInstalled.map(dep => [dep, getVersions(dep)]),
|
||||
),
|
||||
local: Object.fromEntries(
|
||||
sortedLocal.map(dep => [
|
||||
dep,
|
||||
[{ version: workspacePackages.get(dep) ?? 'unknown' }],
|
||||
]),
|
||||
),
|
||||
};
|
||||
|
||||
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Print to console
|
||||
console.log(`OS: ${systemInfo.os}`);
|
||||
console.log(`node: ${systemInfo.node}`);
|
||||
console.log(`yarn: ${systemInfo.yarn}`);
|
||||
console.log(`cli: ${cliVersion} (${isLocal ? 'local' : 'installed'})`);
|
||||
console.log(`backstage: ${backstageVersion}`);
|
||||
console.log();
|
||||
|
||||
// Print installed dependencies
|
||||
console.log('Dependencies:');
|
||||
if (sortedInstalled.length > 0) {
|
||||
const maxLength = Math.max(...sortedInstalled.map(d => d.length));
|
||||
for (const dep of sortedInstalled) {
|
||||
const versions = getVersions(dep)
|
||||
.map(v => v.version)
|
||||
.join(', ');
|
||||
console.log(` ${dep.padEnd(maxLength)} ${versions}`);
|
||||
}
|
||||
} else {
|
||||
console.log(' (no installed Backstage packages found)');
|
||||
}
|
||||
|
||||
// Print local workspace packages
|
||||
if (localDeps.size > 0) {
|
||||
console.log();
|
||||
console.log('Local:');
|
||||
const maxLength = Math.max(...sortedLocal.map(d => d.length));
|
||||
for (const dep of sortedLocal) {
|
||||
const version = workspacePackages.get(dep) ?? 'unknown';
|
||||
console.log(` ${dep.padEnd(maxLength)} ${version}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 { createCliModule } from '@backstage/cli-node';
|
||||
import packageJson from '../package.json';
|
||||
|
||||
export default createCliModule({
|
||||
packageJson,
|
||||
init: async reg => {
|
||||
reg.addCommand({
|
||||
path: ['info'],
|
||||
description: 'Show helpful information for debugging and reporting bugs',
|
||||
execute: { loader: () => import('./commands/info') },
|
||||
});
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user