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;
}
@@ -28,6 +28,17 @@
],
"devDependencies": {
"@backstage/cli": "^{{version '@backstage/cli'}}",
"@backstage/cli-module-auth": "^{{version '@backstage/cli-module-auth'}}",
"@backstage/cli-module-build": "^{{version '@backstage/cli-module-build'}}",
"@backstage/cli-module-config": "^{{version '@backstage/cli-module-config'}}",
"@backstage/cli-module-create-github-app": "^{{version '@backstage/cli-module-create-github-app'}}",
"@backstage/cli-module-info": "^{{version '@backstage/cli-module-info'}}",
"@backstage/cli-module-lint": "^{{version '@backstage/cli-module-lint'}}",
"@backstage/cli-module-maintenance": "^{{version '@backstage/cli-module-maintenance'}}",
"@backstage/cli-module-migrate": "^{{version '@backstage/cli-module-migrate'}}",
"@backstage/cli-module-new": "^{{version '@backstage/cli-module-new'}}",
"@backstage/cli-module-test-jest": "^{{version '@backstage/cli-module-test-jest'}}",
"@backstage/cli-module-translations": "^{{version '@backstage/cli-module-translations'}}",
"@backstage/e2e-test-utils": "^{{version '@backstage/e2e-test-utils'}}",
"@jest/environment-jsdom-abstract": "^30.0.0",
"@playwright/test": "^1.32.3",
@@ -50,6 +50,17 @@
],
"devDependencies": {
"@backstage/cli": "^{{version '@backstage/cli'}}",
"@backstage/cli-module-auth": "^{{version '@backstage/cli-module-auth'}}",
"@backstage/cli-module-build": "^{{version '@backstage/cli-module-build'}}",
"@backstage/cli-module-config": "^{{version '@backstage/cli-module-config'}}",
"@backstage/cli-module-create-github-app": "^{{version '@backstage/cli-module-create-github-app'}}",
"@backstage/cli-module-info": "^{{version '@backstage/cli-module-info'}}",
"@backstage/cli-module-lint": "^{{version '@backstage/cli-module-lint'}}",
"@backstage/cli-module-maintenance": "^{{version '@backstage/cli-module-maintenance'}}",
"@backstage/cli-module-migrate": "^{{version '@backstage/cli-module-migrate'}}",
"@backstage/cli-module-new": "^{{version '@backstage/cli-module-new'}}",
"@backstage/cli-module-test-jest": "^{{version '@backstage/cli-module-test-jest'}}",
"@backstage/cli-module-translations": "^{{version '@backstage/cli-module-translations'}}",
"@backstage/e2e-test-utils": "^{{version '@backstage/e2e-test-utils'}}",
"@jest/environment-jsdom-abstract": "^30.0.0",
"@playwright/test": "^1.32.3",