cli/commands/new: move implementation to lib
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -14,33 +14,33 @@
|
||||
* 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';
|
||||
import { assertError } from '@backstage/errors';
|
||||
import { createNewPackage } from '../../lib/new/createNewPackage';
|
||||
|
||||
import { paths } from '../../lib/paths';
|
||||
import { Task } from '../../lib/tasks';
|
||||
import {
|
||||
addCodeownersEntry,
|
||||
getCodeownersFilePath,
|
||||
} from '../../lib/codeowners';
|
||||
type ArgOptions = {
|
||||
option: string[];
|
||||
select?: string;
|
||||
private?: boolean;
|
||||
npmRegistry?: string;
|
||||
scope?: string;
|
||||
license?: string;
|
||||
baseVersion?: string;
|
||||
};
|
||||
|
||||
import {
|
||||
readCliConfig,
|
||||
templateSelector,
|
||||
verifyTemplate,
|
||||
} from '../../lib/new/templateSelector';
|
||||
import { promptOptions } from '../../lib/new/prompts';
|
||||
import {
|
||||
populateOptions,
|
||||
createDirName,
|
||||
resolvePackageName,
|
||||
} from '../../lib/new/utils';
|
||||
import { runAdditionalActions } from '../../lib/new/additionalActions';
|
||||
import { executePluginPackageTemplate } from '../../lib/new/executeTemplate';
|
||||
import { TemporaryDirectoryManager } from './TemporaryDirectoryManager';
|
||||
export default async (opts: ArgOptions) => {
|
||||
const {
|
||||
option: rawArgOptions,
|
||||
select: preselectedTemplateId,
|
||||
...globals
|
||||
} = opts;
|
||||
|
||||
const argOptions = parseOptions(rawArgOptions);
|
||||
|
||||
await createNewPackage({
|
||||
argOptions,
|
||||
preselectedTemplateId,
|
||||
globals,
|
||||
});
|
||||
};
|
||||
|
||||
function parseOptions(optionStrings: string[]): Record<string, string> {
|
||||
const options: Record<string, string> = {};
|
||||
@@ -58,142 +58,3 @@ function parseOptions(optionStrings: string[]): Record<string, string> {
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
type ArgOptions = {
|
||||
option: string[];
|
||||
select?: string;
|
||||
private?: boolean;
|
||||
npmRegistry?: string;
|
||||
scope?: string;
|
||||
license?: string;
|
||||
baseVersion?: string;
|
||||
};
|
||||
|
||||
export default async ({
|
||||
option: argOptions,
|
||||
select: preselectedTemplateId,
|
||||
...argGlobals
|
||||
}: ArgOptions) => {
|
||||
const pkgJson = await fs.readJson(paths.resolveTargetRoot('package.json'));
|
||||
const cliConfig = pkgJson.backstage?.cli;
|
||||
|
||||
const { templates, globals } = readCliConfig(cliConfig);
|
||||
const template = verifyTemplate(
|
||||
await templateSelector(templates, preselectedTemplateId),
|
||||
);
|
||||
|
||||
const codeOwnersFilePath = await getCodeownersFilePath(paths.targetRoot);
|
||||
|
||||
const legacyOpts = parseOptions(argOptions);
|
||||
|
||||
const prefilledAnswers = Object.fromEntries(
|
||||
(template.prompts ?? []).flatMap(prompt => {
|
||||
const id = typeof prompt === 'string' ? prompt : prompt.id;
|
||||
const answer = legacyOpts[id];
|
||||
return answer ? [[id, answer]] : [];
|
||||
}),
|
||||
);
|
||||
const promptAnswers = await promptOptions({
|
||||
prompts:
|
||||
template.prompts?.filter(
|
||||
prompt =>
|
||||
!Object.hasOwn(
|
||||
prefilledAnswers,
|
||||
typeof prompt === 'string' ? prompt : prompt.id,
|
||||
),
|
||||
) ?? [],
|
||||
globals,
|
||||
codeOwnersFilePath,
|
||||
});
|
||||
const answers = { ...prefilledAnswers, ...promptAnswers };
|
||||
const options = populateOptions({ ...answers, ...argGlobals }, template);
|
||||
|
||||
const tmpDirManager = TemporaryDirectoryManager.create();
|
||||
|
||||
const dirName = createDirName(template, options);
|
||||
const targetDir = paths.resolveTargetRoot(options.targetPath, dirName);
|
||||
|
||||
const packageName = resolvePackageName({
|
||||
baseName: dirName,
|
||||
scope: options.scope,
|
||||
plugin: template.plugin ?? true,
|
||||
});
|
||||
|
||||
const moduleVar =
|
||||
options.moduleId ??
|
||||
`${camelCase(options.id)}Module${camelCase(
|
||||
options.moduleId,
|
||||
)[0].toUpperCase()}${camelCase(options.moduleId).slice(1)}`; // used in default-backend-module template
|
||||
const extensionName = `${upperFirst(camelCase(options.id))}Page`; // used in default-plugin template
|
||||
const pluginVar = `${camelCase(options.id)}Plugin`; // used in default-backend-plugin and default-plugin template
|
||||
|
||||
let modified = false;
|
||||
try {
|
||||
await executePluginPackageTemplate(
|
||||
{
|
||||
isMonoRepo: await isMonoRepo(),
|
||||
createTemporaryDirectory: tmpDirManager.createDir,
|
||||
markAsModified() {
|
||||
modified = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
targetDir,
|
||||
templateDir: template.templatePath,
|
||||
values: {
|
||||
name: packageName,
|
||||
privatePackage: options.private,
|
||||
pluginVersion: options.baseVersion,
|
||||
moduleVar,
|
||||
extensionName,
|
||||
pluginVar,
|
||||
...options,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (template.additionalActions?.length) {
|
||||
await runAdditionalActions(template.additionalActions, {
|
||||
name: packageName,
|
||||
version: options.baseVersion,
|
||||
id: options.id, // for frontend legacy
|
||||
extensionName, // for frontend legacy
|
||||
});
|
||||
}
|
||||
|
||||
if (options.owner) {
|
||||
await addCodeownersEntry(targetDir, options.owner);
|
||||
}
|
||||
|
||||
await Task.forCommand('yarn install', {
|
||||
cwd: targetDir,
|
||||
optional: true,
|
||||
});
|
||||
await Task.forCommand('yarn lint --fix', {
|
||||
cwd: targetDir,
|
||||
optional: true,
|
||||
});
|
||||
|
||||
Task.log();
|
||||
Task.log(`🎉 Successfully created ${template.id}`);
|
||||
Task.log();
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
Task.error(error.message);
|
||||
|
||||
if (modified) {
|
||||
Task.log('It seems that something went wrong in the creation process 🤔');
|
||||
Task.log();
|
||||
Task.log(
|
||||
'We have left the changes that were made intact in case you want to',
|
||||
);
|
||||
Task.log(
|
||||
'continue manually, but you can also revert the changes and try again.',
|
||||
);
|
||||
|
||||
Task.error(`🔥 Failed to create ${template.id}!`);
|
||||
}
|
||||
} finally {
|
||||
tmpDirManager.cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* 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 camelCase from 'lodash/camelCase';
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import { isMonoRepo } from '@backstage/cli-node';
|
||||
import { assertError } from '@backstage/errors';
|
||||
|
||||
import { paths } from '../paths';
|
||||
import { Task } from '../tasks';
|
||||
import { addCodeownersEntry, getCodeownersFilePath } from '../codeowners';
|
||||
|
||||
import {
|
||||
readCliConfig,
|
||||
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';
|
||||
|
||||
export type CreateNewPackageOptions = {
|
||||
preselectedTemplateId?: string;
|
||||
globals: {
|
||||
private?: boolean;
|
||||
npmRegistry?: string;
|
||||
scope?: string;
|
||||
license?: string;
|
||||
baseVersion?: string;
|
||||
};
|
||||
argOptions: { [name in string]?: string };
|
||||
};
|
||||
|
||||
export async function createNewPackage(options: CreateNewPackageOptions) {
|
||||
const pkgJson = await fs.readJson(paths.resolveTargetRoot('package.json'));
|
||||
const cliConfig = pkgJson.backstage?.cli;
|
||||
|
||||
const { templates, globals } = readCliConfig(cliConfig);
|
||||
const template = verifyTemplate(
|
||||
await templateSelector(templates, options.preselectedTemplateId),
|
||||
);
|
||||
|
||||
const codeOwnersFilePath = await getCodeownersFilePath(paths.targetRoot);
|
||||
|
||||
const prefilledAnswers = Object.fromEntries(
|
||||
(template.prompts ?? []).flatMap(prompt => {
|
||||
const id = typeof prompt === 'string' ? prompt : prompt.id;
|
||||
const answer = options.argOptions[id];
|
||||
return answer ? [[id, answer]] : [];
|
||||
}),
|
||||
);
|
||||
const promptAnswers = await promptOptions({
|
||||
prompts:
|
||||
template.prompts?.filter(
|
||||
prompt =>
|
||||
!Object.hasOwn(
|
||||
prefilledAnswers,
|
||||
typeof prompt === 'string' ? prompt : prompt.id,
|
||||
),
|
||||
) ?? [],
|
||||
globals,
|
||||
codeOwnersFilePath,
|
||||
});
|
||||
const answers = { ...prefilledAnswers, ...promptAnswers };
|
||||
const params = populateOptions({ ...answers, ...options.globals }, template);
|
||||
|
||||
const tmpDirManager = TemporaryDirectoryManager.create();
|
||||
|
||||
const dirName = createDirName(template, params);
|
||||
const targetDir = paths.resolveTargetRoot(params.targetPath, dirName);
|
||||
|
||||
const packageName = resolvePackageName({
|
||||
baseName: dirName,
|
||||
scope: params.scope,
|
||||
plugin: template.plugin ?? true,
|
||||
});
|
||||
|
||||
const moduleVar =
|
||||
params.moduleId ??
|
||||
`${camelCase(params.id)}Module${camelCase(
|
||||
params.moduleId,
|
||||
)[0].toUpperCase()}${camelCase(params.moduleId).slice(1)}`; // used in default-backend-module template
|
||||
const extensionName = `${upperFirst(camelCase(params.id))}Page`; // used in default-plugin template
|
||||
const pluginVar = `${camelCase(params.id)}Plugin`; // used in default-backend-plugin and default-plugin template
|
||||
|
||||
let modified = false;
|
||||
try {
|
||||
await executePluginPackageTemplate(
|
||||
{
|
||||
isMonoRepo: await isMonoRepo(),
|
||||
createTemporaryDirectory: tmpDirManager.createDir,
|
||||
markAsModified() {
|
||||
modified = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
targetDir,
|
||||
templateDir: template.templatePath,
|
||||
values: {
|
||||
name: packageName,
|
||||
privatePackage: params.private,
|
||||
pluginVersion: params.baseVersion,
|
||||
moduleVar,
|
||||
extensionName,
|
||||
pluginVar,
|
||||
...params,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (template.additionalActions?.length) {
|
||||
await runAdditionalActions(template.additionalActions, {
|
||||
name: packageName,
|
||||
version: params.baseVersion,
|
||||
id: params.id, // for frontend legacy
|
||||
extensionName, // for frontend legacy
|
||||
});
|
||||
}
|
||||
|
||||
if (params.owner) {
|
||||
await addCodeownersEntry(targetDir, params.owner);
|
||||
}
|
||||
|
||||
await Task.forCommand('yarn install', {
|
||||
cwd: targetDir,
|
||||
optional: true,
|
||||
});
|
||||
await Task.forCommand('yarn lint --fix', {
|
||||
cwd: targetDir,
|
||||
optional: true,
|
||||
});
|
||||
|
||||
Task.log();
|
||||
Task.log(`🎉 Successfully created ${template.id}`);
|
||||
Task.log();
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
Task.error(error.message);
|
||||
|
||||
if (modified) {
|
||||
Task.log('It seems that something went wrong in the creation process 🤔');
|
||||
Task.log();
|
||||
Task.log(
|
||||
'We have left the changes that were made intact in case you want to',
|
||||
);
|
||||
Task.log(
|
||||
'continue manually, but you can also revert the changes and try again.',
|
||||
);
|
||||
|
||||
Task.error(`🔥 Failed to create ${template.id}!`);
|
||||
}
|
||||
} finally {
|
||||
tmpDirManager.cleanup();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user