feat: add --include to backstage-cli info
Signed-off-by: Renas <renash@spotify.com>
This commit is contained in:
@@ -149,6 +149,7 @@ Usage: <none>
|
||||
|
||||
Options:
|
||||
--help
|
||||
--include
|
||||
--version
|
||||
```
|
||||
|
||||
|
||||
@@ -19,9 +19,43 @@ import os from 'os';
|
||||
import { runOutput } from '@backstage/cli-common';
|
||||
import { paths } from '../../../lib/paths';
|
||||
import { Lockfile } from '../../../lib/versioning';
|
||||
import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node';
|
||||
import { minimatch } from 'minimatch';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
export default async () => {
|
||||
interface InfoOptions {
|
||||
include: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (options: InfoOptions) => {
|
||||
await new Promise(async () => {
|
||||
const yarnVersion = await runOutput(['yarn', '--version']);
|
||||
const isLocal = fs.existsSync(paths.resolveOwn('./src'));
|
||||
@@ -44,16 +78,104 @@ export default async () => {
|
||||
console.log(`cli: ${cliVersion} (${isLocal ? 'local' : 'installed'})`);
|
||||
console.log(`backstage: ${backstageVersion}`);
|
||||
console.log();
|
||||
console.log('Dependencies:');
|
||||
|
||||
const lockfilePath = paths.resolveTargetRoot('yarn.lock');
|
||||
const lockfile = await Lockfile.load(lockfilePath);
|
||||
const targetPath = paths.targetRoot;
|
||||
|
||||
const deps = [...lockfile.keys()].filter(n => n.startsWith('@backstage/'));
|
||||
const maxLength = Math.max(...deps.map(d => d.length));
|
||||
// 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
|
||||
}
|
||||
|
||||
for (const dep of deps) {
|
||||
const versions = new Set(lockfile.get(dep)!.map(i => i.version));
|
||||
console.log(` ${dep.padEnd(maxLength)} ${[...versions].join(', ')}`);
|
||||
// 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 version string for a package
|
||||
const getVersions = (dep: string): string => {
|
||||
const entries = lockfile.get(dep);
|
||||
if (!entries) {
|
||||
return 'unknown';
|
||||
}
|
||||
const versions = [...new Set(entries.map(i => i.version))];
|
||||
return versions.join(', ');
|
||||
};
|
||||
|
||||
// Print installed dependencies
|
||||
console.log('Dependencies:');
|
||||
const sortedInstalled = [...installedDeps].sort();
|
||||
if (sortedInstalled.length > 0) {
|
||||
const maxLength = Math.max(...sortedInstalled.map(d => d.length));
|
||||
for (const dep of sortedInstalled) {
|
||||
const versions = getVersions(dep);
|
||||
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 sortedLocal = [...localDeps].sort();
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -24,8 +24,19 @@ export default createCliPlugin({
|
||||
path: ['info'],
|
||||
description: 'Show helpful information for debugging and reporting bugs',
|
||||
execute: async ({ args }) => {
|
||||
yargs().parse(args);
|
||||
await lazy(() => import('./commands/info'), 'default')(args);
|
||||
const argv = await yargs()
|
||||
.options({
|
||||
include: {
|
||||
type: 'string',
|
||||
array: true,
|
||||
default: [],
|
||||
description:
|
||||
'Glob patterns for additional packages to include (e.g., @spotify/backstage*)',
|
||||
},
|
||||
})
|
||||
.help()
|
||||
.parse(args);
|
||||
await lazy(() => import('./commands/info'), 'default')(argv);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user