cli/new: refactor config reading

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-02-07 12:02:37 +01:00
parent 509fdccb96
commit 3f8252f343
10 changed files with 192 additions and 81 deletions
+2 -4
View File
@@ -7,13 +7,11 @@
"url": "https://github.com/backstage/backstage"
},
"backstage": {
"cli": {
"defaults": true,
"new": {
"globals": {
"scope": "backstage",
"private": false
},
"templates": []
}
}
},
"workspaces": {
+2 -1
View File
@@ -160,7 +160,8 @@
"yargs": "^16.2.0",
"yml-loader": "^2.1.0",
"yn": "^4.0.0",
"zod": "^3.22.4"
"zod": "^3.22.4",
"zod-validation-error": "^3.4.0"
},
"devDependencies": {
"@backstage/backend-plugin-api": "workspace:^",
@@ -0,0 +1,69 @@
/*
* 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 fs from 'fs-extra';
import { paths } from '../../paths';
import { defaultTemplates } from '../defaultTemplates';
import { NewConfig } from './types';
import { z } from 'zod';
import { fromZodError } from 'zod-validation-error';
import { ForwardedError } from '@backstage/errors';
const pkgJsonWithNewConfigSchema = z.object({
backstage: z
.object({
new: z
.object({
templates: z
.array(
z
.object({
id: z.string(),
target: z.string(),
})
.strict(),
)
.optional(),
globals: z
.record(z.union([z.string(), z.number(), z.boolean()]))
.optional(),
})
.strict()
.optional(),
})
.optional(),
});
export async function loadNewConfig(): Promise<NewConfig> {
const pkgPath = paths.resolveTargetRoot('package.json');
const pkgJson = await fs.readJson(pkgPath);
const parsed = pkgJsonWithNewConfigSchema.safeParse(pkgJson);
if (!parsed.success) {
throw new ForwardedError(
`Failed to load templating configuration from '${pkgPath}'`,
fromZodError(parsed.error),
);
}
const newConfig = parsed.data.backstage?.new;
return {
isUsingDefaultTemplates: !newConfig?.templates,
templatePointers: newConfig?.templates ?? defaultTemplates,
globals: newConfig?.globals ?? {},
};
}
+39
View File
@@ -0,0 +1,39 @@
/*
* 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.
*/
export type NewConfig = {
/**
* The pointers to templates that can be used.
*/
templatePointers: NewTemplatePointer[];
/**
* Whether the default set of templates are being used or not.
*/
isUsingDefaultTemplates: boolean;
/**
* Templating globals that should apply to all templates.
*/
globals: {
[KName in string]?: number | string | boolean;
};
};
export type NewTemplatePointer = {
id: string;
target: string;
};
+8 -11
View File
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import camelCase from 'lodash/camelCase';
import upperFirst from 'lodash/upperFirst';
import { isMonoRepo } from '@backstage/cli-node';
@@ -24,16 +23,13 @@ import { paths } from '../paths';
import { Task } from '../tasks';
import { addCodeownersEntry, getCodeownersFilePath } from '../codeowners';
import {
readCliConfig,
templateSelector,
verifyTemplate,
} from './templateSelector';
import { templateSelector, verifyTemplate } from './templateSelector';
import { promptOptions } from './prompts';
import { populateOptions, createDirName, resolvePackageName } from './utils';
import { runAdditionalActions } from './additionalActions';
import { executePluginPackageTemplate } from './executeTemplate';
import { TemporaryDirectoryManager } from './TemporaryDirectoryManager';
import { loadNewConfig } from './config/loadNewConfig';
export type CreateNewPackageOptions = {
preselectedTemplateId?: string;
@@ -48,12 +44,13 @@ export type CreateNewPackageOptions = {
};
export async function createNewPackage(options: CreateNewPackageOptions) {
const pkgJson = await fs.readJson(paths.resolveTargetRoot('package.json'));
const cliConfig = pkgJson.backstage?.cli;
const newConfig = await loadNewConfig();
const { templates, globals } = readCliConfig(cliConfig);
const template = verifyTemplate(
await templateSelector(templates, options.preselectedTemplateId),
await templateSelector(
newConfig.templatePointers,
options.preselectedTemplateId,
),
);
const codeOwnersFilePath = await getCodeownersFilePath(paths.targetRoot);
@@ -74,7 +71,7 @@ export async function createNewPackage(options: CreateNewPackageOptions) {
typeof prompt === 'string' ? prompt : prompt.id,
),
) ?? [],
globals,
globals: newConfig.globals,
codeOwnersFilePath,
});
const answers = { ...prefilledAnswers, ...promptAnswers };
@@ -0,0 +1,68 @@
/*
* 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.
*/
export const defaultTemplates = [
{
id: 'backend-module',
target: require.resolve(
'@backstage/cli/templates/default-backend-module.yaml',
),
},
{
id: 'backend-plugin',
target: require.resolve(
'@backstage/cli/templates/default-backend-plugin.yaml',
),
},
{
id: 'plugin-common',
target: require.resolve(
'@backstage/cli/templates/default-common-plugin-package.yaml',
),
},
{
id: 'plugin-node',
target: require.resolve(
'@backstage/cli/templates/default-node-plugin-package.yaml',
),
},
{
id: 'frontend-plugin',
target: require.resolve('@backstage/cli/templates/default-plugin.yaml'),
},
{
id: 'plugin-react',
target: require.resolve(
'@backstage/cli/templates/default-react-plugin-package.yaml',
),
},
{
id: 'node-library',
target: require.resolve(
'@backstage/cli/templates/node-library-package.yaml',
),
},
{
id: 'scaffolder-module',
target: require.resolve('@backstage/cli/templates/scaffolder-module.yaml'),
},
{
id: 'web-library',
target: require.resolve(
'@backstage/cli/templates/web-library-package.yaml',
),
},
];
+1 -1
View File
@@ -95,7 +95,7 @@ export async function promptOptions({
codeOwnersFilePath,
}: {
prompts: ConfigurablePrompt[];
globals: Record<string, string>;
globals: { [name in string]?: string | boolean | number };
codeOwnersFilePath: string | undefined;
}): Promise<Record<string, string>> {
const answers = await inquirer.prompt(
+2 -24
View File
@@ -19,31 +19,9 @@ import { parse } from 'yaml';
import fs from 'fs-extra';
import { paths } from '../paths';
import { Template, TemplateLocation, CliConfig } from './types';
import { Template, TemplateLocation } from './types';
import defaultTemplates from '../../../templates/all-default-templates';
export function readCliConfig(cliConfig: CliConfig) {
let templates: TemplateLocation[] = [];
if (!cliConfig || cliConfig?.defaults) {
templates = defaultTemplates;
}
const cliTemplates = cliConfig?.templates;
if (cliTemplates?.length) {
cliTemplates.forEach((template: TemplateLocation) => {
templates.push({
id: template.id,
target: template.target,
});
});
}
return {
templates,
globals: { ...cliConfig?.globals },
};
}
import { defaultTemplates } from './defaultTemplates';
export async function templateSelector(
templates: TemplateLocation[],
@@ -1,40 +0,0 @@
import { paths } from '../src/lib/paths';
export default [
{
id: 'backend-module',
target: paths.resolveTargetRoot('node_modules/@backstage/cli/templates/default-backend-module.yaml'),
},
{
id: 'backend-plugin',
target: paths.resolveTargetRoot('node_modules/@backstage/cli/templates/default-backend-plugin.yaml'),
},
{
id: 'plugin-common',
target: paths.resolveTargetRoot('node_modules/@backstage/cli/templates/default-common-plugin-package.yaml'),
},
{
id: 'plugin-node',
target: paths.resolveTargetRoot('node_modules/@backstage/cli/templates/default-node-plugin-package.yaml'),
},
{
id: 'frontend-plugin', // changed from 'plugin'
target: paths.resolveTargetRoot('node_modules/@backstage/cli/templates/default-plugin.yaml'),
},
{
id: 'plugin-react',
target: paths.resolveTargetRoot('node_modules/@backstage/cli/templates/default-react-plugin-package.yaml'),
},
{
id: 'node-library',
target: paths.resolveTargetRoot('node_modules/@backstage/cli/templates/node-library-package.yaml'),
},
{
id: 'scaffolder-module',
target: paths.resolveTargetRoot('node_modules/@backstage/cli/templates/scaffolder-module.yaml'),
},
{
id: 'web-library',
target: paths.resolveTargetRoot('node_modules/@backstage/cli/templates/web-library-package.yaml'),
},
];
+1
View File
@@ -4051,6 +4051,7 @@ __metadata:
yml-loader: ^2.1.0
yn: ^4.0.0
zod: ^3.22.4
zod-validation-error: ^3.4.0
peerDependencies:
"@rspack/core": ^1.0.10
"@rspack/dev-server": ^1.0.9