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 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage-cli-module-config
|
||||
title: '@backstage/cli-module-config'
|
||||
description: CLI module for Backstage CLI
|
||||
spec:
|
||||
lifecycle: experimental
|
||||
type: backstage-cli-module
|
||||
owner: tooling-maintainers
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@backstage/cli-module-config",
|
||||
"version": "0.1.0",
|
||||
"description": "CLI module for Backstage CLI",
|
||||
"backstage": {
|
||||
"role": "cli-module"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "packages/cli-module-config"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/cli-common": "workspace:^",
|
||||
"@backstage/cli-node": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/config-loader": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"@types/json-schema": "^7.0.6",
|
||||
"chalk": "^4.0.0",
|
||||
"cleye": "^2.3.0",
|
||||
"json-schema": "^0.4.0",
|
||||
"react-dev-utils": "^12.0.0-next.60",
|
||||
"yaml": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2021 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';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { mergeConfigSchemas } from '@backstage/config-loader';
|
||||
import { JSONSchema7 as JSONSchema } from 'json-schema';
|
||||
import openBrowser from 'react-dev-utils/openBrowser';
|
||||
import chalk from 'chalk';
|
||||
import { loadCliConfig } from '../lib/config';
|
||||
import type { CliCommandContext } from '@backstage/cli-node';
|
||||
|
||||
const DOCS_URL = 'https://config.backstage.io';
|
||||
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
const {
|
||||
flags: { package: pkg },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
booleanFlagNegation: true,
|
||||
flags: {
|
||||
package: {
|
||||
type: String,
|
||||
description:
|
||||
'Only include the schema that applies to the given package',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const { schema: appSchemas } = await loadCliConfig({
|
||||
args: [],
|
||||
fromPackage: pkg,
|
||||
mockEnv: true,
|
||||
});
|
||||
|
||||
const schema = mergeConfigSchemas(
|
||||
(appSchemas.serialize().schemas as JsonObject[]).map(
|
||||
_ => _.value as JSONSchema,
|
||||
),
|
||||
);
|
||||
|
||||
const url = `${DOCS_URL}#schema=${JSON.stringify(schema)}`;
|
||||
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
'Opening configuration reference documentation in your browser...',
|
||||
),
|
||||
);
|
||||
console.log(` ${chalk.cyan(url)}`);
|
||||
console.log();
|
||||
|
||||
const opened = openBrowser(url);
|
||||
|
||||
if (!opened) {
|
||||
console.log(
|
||||
chalk.yellow('⚠️ WARNING: Unable to open browser automatically.'),
|
||||
);
|
||||
console.log(chalk.yellow('Please open the URL manually in your browser.'));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2020 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';
|
||||
import { stringify as stringifyYaml } from 'yaml';
|
||||
import { AppConfig, ConfigReader } from '@backstage/config';
|
||||
import { loadCliConfig } from '../lib/config';
|
||||
import { ConfigSchema, ConfigVisibility } from '@backstage/config-loader';
|
||||
import type { CliCommandContext } from '@backstage/cli-node';
|
||||
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
const {
|
||||
flags: { config, lax, frontend, withSecrets, format, package: pkg },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
booleanFlagNegation: true,
|
||||
flags: {
|
||||
package: { type: String, description: 'Package to print config for' },
|
||||
lax: {
|
||||
type: Boolean,
|
||||
description: 'Do not require environment variables to be set',
|
||||
},
|
||||
frontend: { type: Boolean, description: 'Only print frontend config' },
|
||||
withSecrets: {
|
||||
type: Boolean,
|
||||
description: 'Include secrets in the output',
|
||||
},
|
||||
format: { type: String, description: 'Output format (yaml or json)' },
|
||||
config: {
|
||||
type: [String],
|
||||
description: 'Config files to load instead of app-config.yaml',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const { schema, appConfigs } = await loadCliConfig({
|
||||
args: config,
|
||||
fromPackage: pkg,
|
||||
mockEnv: lax,
|
||||
fullVisibility: !frontend,
|
||||
});
|
||||
const visibility = getVisibilityOption(frontend, withSecrets);
|
||||
const data = serializeConfigData(appConfigs, schema, visibility);
|
||||
|
||||
if (format === 'json') {
|
||||
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
||||
} else {
|
||||
process.stdout.write(`${stringifyYaml(data)}\n`);
|
||||
}
|
||||
};
|
||||
|
||||
function getVisibilityOption(
|
||||
frontend: boolean | undefined,
|
||||
withSecrets: boolean | undefined,
|
||||
): ConfigVisibility {
|
||||
if (frontend && withSecrets) {
|
||||
throw new Error('Not allowed to combine frontend and secret config');
|
||||
}
|
||||
if (frontend) {
|
||||
return 'frontend';
|
||||
} else if (withSecrets) {
|
||||
return 'secret';
|
||||
}
|
||||
return 'backend';
|
||||
}
|
||||
|
||||
function serializeConfigData(
|
||||
appConfigs: AppConfig[],
|
||||
schema: ConfigSchema,
|
||||
visibility: ConfigVisibility,
|
||||
) {
|
||||
if (visibility === 'frontend') {
|
||||
const frontendConfigs = schema.process(appConfigs, {
|
||||
visibility: ['frontend'],
|
||||
});
|
||||
return ConfigReader.fromConfigs(frontendConfigs).get();
|
||||
} else if (visibility === 'secret') {
|
||||
return ConfigReader.fromConfigs(appConfigs).get();
|
||||
}
|
||||
|
||||
const sanitizedConfigs = schema.process(appConfigs, {
|
||||
valueTransform: (value, context) =>
|
||||
context.visibility === 'secret' ? '<secret>' : value,
|
||||
});
|
||||
|
||||
return ConfigReader.fromConfigs(sanitizedConfigs).get();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2021 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';
|
||||
import { JSONSchema7 as JSONSchema } from 'json-schema';
|
||||
import { stringify as stringifyYaml } from 'yaml';
|
||||
import { loadCliConfig } from '../lib/config';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { mergeConfigSchemas } from '@backstage/config-loader';
|
||||
import type { CliCommandContext } from '@backstage/cli-node';
|
||||
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
const {
|
||||
flags: { merge, format, package: pkg },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
booleanFlagNegation: true,
|
||||
flags: {
|
||||
package: { type: String, description: 'Package to print schema for' },
|
||||
format: { type: String, description: 'Output format (yaml or json)' },
|
||||
merge: {
|
||||
type: Boolean,
|
||||
description: 'Merge all schemas into a single schema',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const { schema } = await loadCliConfig({
|
||||
args: [],
|
||||
fromPackage: pkg,
|
||||
mockEnv: true,
|
||||
});
|
||||
|
||||
let configSchema: JsonObject | JSONSchema;
|
||||
if (merge) {
|
||||
configSchema = mergeConfigSchemas(
|
||||
(schema.serialize().schemas as JsonObject[]).map(
|
||||
_ => _.value as JSONSchema,
|
||||
),
|
||||
);
|
||||
configSchema.title = 'Application Configuration Schema';
|
||||
configSchema.description =
|
||||
'This is the schema describing the structure of the app-config.yaml configuration file.';
|
||||
} else {
|
||||
configSchema = schema.serialize();
|
||||
}
|
||||
|
||||
if (format === 'json') {
|
||||
process.stdout.write(`${JSON.stringify(configSchema, null, 2)}\n`);
|
||||
} else {
|
||||
process.stdout.write(`${stringifyYaml(configSchema)}\n`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2020 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';
|
||||
import { loadCliConfig } from '../lib/config';
|
||||
import type { CliCommandContext } from '@backstage/cli-node';
|
||||
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
const {
|
||||
flags: { config, lax, frontend, deprecated, strict, package: pkg },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
booleanFlagNegation: true,
|
||||
flags: {
|
||||
package: {
|
||||
type: String,
|
||||
description: 'Package to validate config for',
|
||||
},
|
||||
lax: {
|
||||
type: Boolean,
|
||||
description: 'Do not require environment variables to be set',
|
||||
},
|
||||
frontend: {
|
||||
type: Boolean,
|
||||
description: 'Only validate frontend config',
|
||||
},
|
||||
deprecated: { type: Boolean, description: 'Output deprecated keys' },
|
||||
strict: { type: Boolean, description: 'Enable strict validation' },
|
||||
config: {
|
||||
type: [String],
|
||||
description: 'Config files to load instead of app-config.yaml',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
await loadCliConfig({
|
||||
args: config,
|
||||
fromPackage: pkg,
|
||||
mockEnv: lax,
|
||||
fullVisibility: !frontend,
|
||||
withDeprecatedKeys: deprecated,
|
||||
strict,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 const configOption = [
|
||||
'--config <path>',
|
||||
'Config files to load instead of app-config.yaml',
|
||||
(opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]),
|
||||
Array<string>(),
|
||||
] as const;
|
||||
|
||||
export default createCliModule({
|
||||
packageJson,
|
||||
init: async reg => {
|
||||
reg.addCommand({
|
||||
path: ['config:docs'],
|
||||
description: 'Browse the configuration reference documentation',
|
||||
execute: { loader: () => import('./commands/docs') },
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['config', 'docs'],
|
||||
description: 'Browse the configuration reference documentation',
|
||||
execute: { loader: () => import('./commands/docs') },
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['config:print'],
|
||||
description: 'Print the app configuration for the current package',
|
||||
execute: { loader: () => import('./commands/print') },
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['config:check'],
|
||||
description:
|
||||
'Validate that the given configuration loads and matches schema',
|
||||
execute: { loader: () => import('./commands/validate') },
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['config:schema'],
|
||||
description: 'Print the JSON schema for the given configuration',
|
||||
execute: { loader: () => import('./commands/schema') },
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['config', 'schema'],
|
||||
description: 'Print the JSON schema for the given configuration',
|
||||
execute: { loader: () => import('./commands/schema') },
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2020 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 { ConfigSources, loadConfigSchema } from '@backstage/config-loader';
|
||||
import { AppConfig, ConfigReader } from '@backstage/config';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
|
||||
import { getPackages } from '@manypkg/get-packages';
|
||||
import { PackageGraph } from '@backstage/cli-node';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
|
||||
type Options = {
|
||||
args: string[];
|
||||
targetDir?: string;
|
||||
fromPackage?: string;
|
||||
mockEnv?: boolean;
|
||||
withDeprecatedKeys?: boolean;
|
||||
fullVisibility?: boolean;
|
||||
strict?: boolean;
|
||||
};
|
||||
|
||||
export async function loadCliConfig(options: Options) {
|
||||
const targetDir = options.targetDir ?? targetPaths.dir;
|
||||
|
||||
// Consider all packages in the monorepo when loading in config
|
||||
const { packages } = await getPackages(targetDir);
|
||||
|
||||
let localPackageNames;
|
||||
if (options.fromPackage) {
|
||||
if (packages.length) {
|
||||
const graph = PackageGraph.fromPackages(packages);
|
||||
localPackageNames = Array.from(
|
||||
graph.collectPackageNames([options.fromPackage], node => {
|
||||
// Workaround for Backstage main repo only, since the CLI has some artificial devDependencies
|
||||
if (node.name === '@backstage/cli') {
|
||||
return undefined;
|
||||
}
|
||||
return node.localDependencies.keys();
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// No packages: it means that it's not a monorepo (e.g. standalone plugin)
|
||||
localPackageNames = [options.fromPackage];
|
||||
}
|
||||
} else {
|
||||
localPackageNames = packages.map(p => p.packageJson.name);
|
||||
}
|
||||
|
||||
const schema = await loadConfigSchema({
|
||||
dependencies: localPackageNames,
|
||||
// Include the package.json in the project root if it exists
|
||||
packagePaths: [targetPaths.resolveRoot('package.json')],
|
||||
noUndeclaredProperties: options.strict,
|
||||
});
|
||||
|
||||
const source = ConfigSources.default({
|
||||
allowMissingDefaultConfig: true,
|
||||
substitutionFunc: options.mockEnv
|
||||
? async name => process.env[name] || 'x'
|
||||
: undefined,
|
||||
rootDir: targetPaths.rootDir,
|
||||
argv: options.args.flatMap(t => ['--config', resolvePath(targetDir, t)]),
|
||||
});
|
||||
|
||||
const appConfigs = await new Promise<AppConfig[]>((resolve, reject) => {
|
||||
async function readConfig() {
|
||||
let loaded = false;
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
for await (const { configs } of source.readConfigData({
|
||||
signal: abortController.signal,
|
||||
})) {
|
||||
resolve(configs);
|
||||
loaded = true;
|
||||
abortController.abort();
|
||||
}
|
||||
} catch (error) {
|
||||
if (!loaded) {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
readConfig();
|
||||
});
|
||||
|
||||
const configurationLoadedMessage = appConfigs.length
|
||||
? `Loaded config from ${appConfigs.map(c => c.context).join(', ')}`
|
||||
: `No configuration files found, running without config`;
|
||||
|
||||
// printing to stderr to not clobber stdout in case the cli command
|
||||
// outputs structured data (e.g. as config:schema does)
|
||||
process.stderr.write(`${configurationLoadedMessage}\n`);
|
||||
|
||||
try {
|
||||
const frontendAppConfigs = schema.process(appConfigs, {
|
||||
visibility: options.fullVisibility
|
||||
? ['frontend', 'backend', 'secret']
|
||||
: ['frontend'],
|
||||
withDeprecatedKeys: options.withDeprecatedKeys,
|
||||
ignoreSchemaErrors: !options.strict,
|
||||
});
|
||||
const frontendConfig = ConfigReader.fromConfigs(frontendAppConfigs);
|
||||
|
||||
const fullConfig = ConfigReader.fromConfigs(appConfigs);
|
||||
|
||||
return {
|
||||
schema,
|
||||
appConfigs,
|
||||
frontendConfig,
|
||||
frontendAppConfigs,
|
||||
fullConfig,
|
||||
};
|
||||
} catch (error) {
|
||||
const maybeSchemaError = error as Error & { messages?: string[] };
|
||||
if (maybeSchemaError.messages) {
|
||||
const messages = maybeSchemaError.messages.join('\n ');
|
||||
throw new Error(`Configuration does not match schema\n\n ${messages}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user