diff --git a/packages/cli/src/commands/new/new.ts b/packages/cli/src/commands/new/new.ts index 1cc59a98d7..273aba8957 100644 --- a/packages/cli/src/commands/new/new.ts +++ b/packages/cli/src/commands/new/new.ts @@ -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 { const options: Record = {}; @@ -58,142 +58,3 @@ function parseOptions(optionStrings: string[]): Record { 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(); - } -}; diff --git a/packages/cli/src/commands/new/TemporaryDirectoryManager.ts b/packages/cli/src/lib/new/TemporaryDirectoryManager.ts similarity index 100% rename from packages/cli/src/commands/new/TemporaryDirectoryManager.ts rename to packages/cli/src/lib/new/TemporaryDirectoryManager.ts diff --git a/packages/cli/src/lib/new/createNewPackage.ts b/packages/cli/src/lib/new/createNewPackage.ts new file mode 100644 index 0000000000..56d089277b --- /dev/null +++ b/packages/cli/src/lib/new/createNewPackage.ts @@ -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(); + } +}