Add automatic discovery of CLI modules from project dependencies

The CLI now scans the project root's dependencies and devDependencies
for packages with the cli-module role, loading them automatically.
Falls back to the built-in set with a deprecation warning when no
modules are found. Updated create-app templates and the monorepo root
to include all CLI modules as explicit devDependencies.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-14 12:34:24 +01:00
parent 64a96d9d9f
commit d806b0cc9f
8 changed files with 191 additions and 22 deletions
+35 -11
View File
@@ -14,20 +14,44 @@
* limitations under the License.
*/
import chalk from 'chalk';
import { CliInitializer } from './wiring/CliInitializer';
import { discoverCliModules } from './wiring/discoverCliModules';
(async () => {
const initializer = new CliInitializer();
initializer.add(import('@backstage/cli-module-build'));
initializer.add(import('@backstage/cli-module-config'));
initializer.add(import('@backstage/cli-module-create-github-app'));
initializer.add(import('@backstage/cli-module-info'));
initializer.add(import('@backstage/cli-module-lint'));
initializer.add(import('@backstage/cli-module-maintenance'));
initializer.add(import('@backstage/cli-module-migrate'));
initializer.add(import('@backstage/cli-module-new'));
initializer.add(import('@backstage/cli-module-test-jest'));
initializer.add(import('@backstage/cli-module-translations'));
initializer.add(import('@backstage/cli-module-auth'));
const discoveredModules = discoverCliModules();
if (discoveredModules.length > 0) {
for (const moduleName of discoveredModules) {
initializer.add(import(moduleName));
}
} else {
// No CLI modules found in the project root; fall back to the built-in
// set while printing a deprecation warning.
console.error(
chalk.yellow(
`No CLI modules found in the project root dependencies. ` +
`Falling back to the built-in set of modules.\n` +
`This fallback will be removed in a future release. ` +
`Please add the CLI modules you need as devDependencies ` +
`in your root package.json.\n`,
),
);
initializer.add(import('@backstage/cli-module-build'));
initializer.add(import('@backstage/cli-module-config'));
initializer.add(import('@backstage/cli-module-create-github-app'));
initializer.add(import('@backstage/cli-module-info'));
initializer.add(import('@backstage/cli-module-lint'));
initializer.add(import('@backstage/cli-module-maintenance'));
initializer.add(import('@backstage/cli-module-migrate'));
initializer.add(import('@backstage/cli-module-new'));
initializer.add(import('@backstage/cli-module-test-jest'));
initializer.add(import('@backstage/cli-module-translations'));
initializer.add(import('@backstage/cli-module-auth'));
}
await initializer.run();
})();
@@ -0,0 +1,65 @@
/*
* 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 { targetPaths } from '@backstage/cli-common';
import { PackageRoles } from '@backstage/cli-node';
import fs from 'node:fs';
import { resolve as resolvePath } from 'node:path';
/**
* Scans the target project root's package.json for dependencies that are CLI
* modules (packages with `backstage.role === 'cli-module'`).
*
* Returns the names of discovered CLI module packages, or an empty array if
* none are found or the project root cannot be read.
*/
export function discoverCliModules(): string[] {
const rootDir = targetPaths.rootDir;
const pkgJsonPath = resolvePath(rootDir, 'package.json');
let projectPkg: {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
try {
projectPkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
} catch {
return [];
}
const allDeps = {
...projectPkg.dependencies,
...projectPkg.devDependencies,
};
const modules: string[] = [];
for (const depName of Object.keys(allDeps)) {
try {
const depPkgPath = require.resolve(`${depName}/package.json`, {
paths: [rootDir],
});
const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf8'));
if (PackageRoles.getRoleFromPackage(depPkg) === 'cli-module') {
modules.push(depName);
}
} catch {
// Skip packages that can't be resolved or read
}
}
return modules;
}