From 100681e427aa1864a87501546885d74c1fd6b9ef Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 10 Feb 2025 00:10:47 +0100 Subject: [PATCH] cli/new: replace additionalActions with fixed installation per role Signed-off-by: Patrik Oldsberg --- .../lib/new/execution/additionalActions.ts | 190 ------------------ .../new/execution/executePortableTemplate.ts | 6 +- .../lib/new/execution/installNewPackage.ts | 147 ++++++++++++++ .../new/preparation/loadPortableTemplate.ts | 1 - packages/cli/src/lib/new/types.ts | 1 - .../cli/templates/default-backend-module.yaml | 3 - .../cli/templates/default-backend-plugin.yaml | 3 - packages/cli/templates/default-plugin.yaml | 3 - packages/cli/templates/scaffolder-module.yaml | 3 - 9 files changed, 149 insertions(+), 208 deletions(-) delete mode 100644 packages/cli/src/lib/new/execution/additionalActions.ts create mode 100644 packages/cli/src/lib/new/execution/installNewPackage.ts diff --git a/packages/cli/src/lib/new/execution/additionalActions.ts b/packages/cli/src/lib/new/execution/additionalActions.ts deleted file mode 100644 index dbeb40f7bc..0000000000 --- a/packages/cli/src/lib/new/execution/additionalActions.ts +++ /dev/null @@ -1,190 +0,0 @@ -/* - * 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 fs from 'fs-extra'; -import upperFirst from 'lodash/upperFirst'; -import camelCase from 'lodash/camelCase'; -import { paths } from '../../paths'; -import { Task } from '../../tasks'; -import { PortableTemplate, PortableTemplateInput } from '../types'; - -export async function runAdditionalActions( - template: PortableTemplate, - input: PortableTemplateInput, -) { - if (!template.additionalActions) { - return; - } - for (const action of template.additionalActions) { - switch (action) { - case 'install-frontend': - await installFrontend(input); - break; - case 'add-frontend-legacy': - await addFrontendLegacy(input); - break; - case 'install-backend': - await installBackend(input); - break; - case 'add-backend': - await addBackend(input); - break; - default: - throw new Error(`${action} is not a valid additional action`); - } - } -} - -async function addPackageDependency( - path: string, - options: { - dependencies?: Record; - devDependencies?: Record; - peerDependencies?: Record; - }, -) { - try { - const pkgJson = await fs.readJson(path); - - const normalize = (obj: Record) => { - if (Object.keys(obj).length === 0) { - return undefined; - } - return Object.fromEntries( - Object.keys(obj) - .sort() - .map(key => [key, obj[key]]), - ); - }; - - pkgJson.dependencies = normalize({ - ...pkgJson.dependencies, - ...options.dependencies, - }); - pkgJson.devDependencies = normalize({ - ...pkgJson.devDependencies, - ...options.devDependencies, - }); - pkgJson.peerDependencies = normalize({ - ...pkgJson.peerDependencies, - ...options.peerDependencies, - }); - - await fs.writeJson(path, pkgJson, { spaces: 2 }); - } catch (error) { - throw new Error(`Failed to add package dependencies, ${error}`); - } -} - -async function installFrontend(input: PortableTemplateInput) { - if (await fs.pathExists(paths.resolveTargetRoot('packages/app'))) { - await Task.forItem('app', 'adding dependency', async () => { - await addPackageDependency( - paths.resolveTargetRoot('packages/app/package.json'), - { - dependencies: { - [input.packageName]: `workspace:^`, - }, - }, - ); - }); - } -} - -async function addFrontendLegacy(input: PortableTemplateInput) { - const { roleParams } = input; - if (roleParams.role !== 'frontend-plugin') { - throw new Error( - 'add-frontend-legacy can only be used for frontend plugins', - ); - } - await Task.forItem('app', 'adding import', async () => { - const pluginsFilePath = paths.resolveTargetRoot('packages/app/src/App.tsx'); - if (!(await fs.pathExists(pluginsFilePath))) { - return; - } - - const content = await fs.readFile(pluginsFilePath, 'utf8'); - const revLines = content.split('\n').reverse(); - - const lastImportIndex = revLines.findIndex(line => - line.match(/ from ("|').*("|')/), - ); - const lastRouteIndex = revLines.findIndex(line => - line.match(/<\/FlatRoutes/), - ); - - if (lastImportIndex !== -1 && lastRouteIndex !== -1) { - const extensionName = upperFirst(`${camelCase(roleParams.pluginId)}Page`); - const importLine = `import { ${extensionName} } from '${input.packageName}';`; - if (!content.includes(importLine)) { - revLines.splice(lastImportIndex, 0, importLine); - } - - const componentLine = `} />`; - if (!content.includes(componentLine)) { - const [indentation] = revLines[lastRouteIndex + 1].match(/^\s*/) ?? []; - revLines.splice(lastRouteIndex + 1, 0, indentation + componentLine); - } - - const newContent = revLines.reverse().join('\n'); - await fs.writeFile(pluginsFilePath, newContent, 'utf8'); - } - }); -} - -async function installBackend(input: PortableTemplateInput) { - if (await fs.pathExists(paths.resolveTargetRoot('packages/backend'))) { - await Task.forItem('backend', 'adding dependency', async () => { - await addPackageDependency( - paths.resolveTargetRoot('packages/backend/package.json'), - { - dependencies: { - [input.packageName]: `workspace:^`, - }, - }, - ); - }); - } -} - -async function addBackend(input: PortableTemplateInput) { - if (await fs.pathExists(paths.resolveTargetRoot('packages/backend'))) { - await Task.forItem('backend', `adding ${input.packageName}`, async () => { - const backendFilePath = paths.resolveTargetRoot( - 'packages/backend/src/index.ts', - ); - if (!(await fs.pathExists(backendFilePath))) { - return; - } - - const content = await fs.readFile(backendFilePath, 'utf8'); - const lines = content.split('\n'); - const backendAddLine = `backend.add(import('${input.packageName}'));`; - - const backendStartIndex = lines.findIndex(line => - line.match(/backend.start/), - ); - - if (backendStartIndex !== -1) { - const [indentation] = lines[backendStartIndex].match(/^\s*/)!; - lines.splice(backendStartIndex, 0, `${indentation}${backendAddLine}`); - - const newContent = lines.join('\n'); - await fs.writeFile(backendFilePath, newContent, 'utf8'); - } - }); - } -} diff --git a/packages/cli/src/lib/new/execution/executePortableTemplate.ts b/packages/cli/src/lib/new/execution/executePortableTemplate.ts index 746c299586..4300bd54bc 100644 --- a/packages/cli/src/lib/new/execution/executePortableTemplate.ts +++ b/packages/cli/src/lib/new/execution/executePortableTemplate.ts @@ -22,7 +22,7 @@ import { PortableTemplateConfig, PortableTemplateInput, } from '../types'; -import { runAdditionalActions } from './additionalActions'; +import { installNewPackage } from './installNewPackage'; import { writeTemplateContents } from './writeTemplateContents'; type ExecuteNewTemplateOptions = { @@ -42,9 +42,7 @@ export async function executePortableTemplate( modified = true; - if (template.additionalActions?.length) { - await runAdditionalActions(template, input); - } + await installNewPackage(input); if (input.builtInParams.owner) { await addCodeownersEntry(targetDir, input.builtInParams.owner); diff --git a/packages/cli/src/lib/new/execution/installNewPackage.ts b/packages/cli/src/lib/new/execution/installNewPackage.ts new file mode 100644 index 0000000000..6ed60541c1 --- /dev/null +++ b/packages/cli/src/lib/new/execution/installNewPackage.ts @@ -0,0 +1,147 @@ +/* + * 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 fs from 'fs-extra'; +import upperFirst from 'lodash/upperFirst'; +import camelCase from 'lodash/camelCase'; +import { paths } from '../../paths'; +import { Task } from '../../tasks'; +import { PortableTemplateInput } from '../types'; + +export async function installNewPackage(input: PortableTemplateInput) { + switch (input.roleParams.role) { + case 'web-library': + case 'node-library': + case 'common-library': + case 'plugin-web-library': + case 'plugin-node-library': + case 'plugin-common-library': + return; // No installation action needed for library packages + case 'frontend-plugin': + await addDependency(input, 'package/app/package.json'); + await tryAddFrontendLegacy(input); + return; + case 'frontend-plugin-module': + await addDependency(input, 'package/app/package.json'); + return; + case 'backend-plugin': + await addDependency(input, 'package/backend/package.json'); + await tryAddBackend(input); + return; + case 'backend-plugin-module': + await addDependency(input, 'package/backend/package.json'); + await tryAddBackend(input); + return; + default: + throw new Error( + `Unsupported role ${(input.roleParams as { role: string }).role}`, + ); + } +} + +async function addDependency(input: PortableTemplateInput, path: string) { + const pkgJsonPath = paths.resolveTargetRoot(path); + + const pkgJson = await fs.readJson(pkgJsonPath).catch(error => { + if (error.code === 'ENOENT') { + return undefined; + } + throw error; + }); + if (!pkgJson) { + return; + } + + try { + pkgJson.dependencies = { + ...pkgJson.dependencies, + [input.packageName]: `workspace:^`, + }; + + await fs.writeJson(path, pkgJson, { spaces: 2 }); + } catch (error) { + throw new Error(`Failed to add package dependencies, ${error}`); + } +} + +async function tryAddFrontendLegacy(input: PortableTemplateInput) { + const { roleParams } = input; + if (roleParams.role !== 'frontend-plugin') { + throw new Error( + 'add-frontend-legacy can only be used for frontend plugins', + ); + } + + const appDefinitionPath = paths.resolveTargetRoot('packages/app/src/App.tsx'); + if (!(await fs.pathExists(appDefinitionPath))) { + return; + } + + await Task.forItem('app', 'adding import', async () => { + const content = await fs.readFile(appDefinitionPath, 'utf8'); + const revLines = content.split('\n').reverse(); + + const lastImportIndex = revLines.findIndex(line => + line.match(/ from ("|').*("|')/), + ); + const lastRouteIndex = revLines.findIndex(line => + line.match(/<\/FlatRoutes/), + ); + + if (lastImportIndex !== -1 && lastRouteIndex !== -1) { + const extensionName = upperFirst(`${camelCase(roleParams.pluginId)}Page`); + const importLine = `import { ${extensionName} } from '${input.packageName}';`; + if (!content.includes(importLine)) { + revLines.splice(lastImportIndex, 0, importLine); + } + + const componentLine = `} />`; + if (!content.includes(componentLine)) { + const [indentation] = revLines[lastRouteIndex + 1].match(/^\s*/) ?? []; + revLines.splice(lastRouteIndex + 1, 0, indentation + componentLine); + } + + const newContent = revLines.reverse().join('\n'); + await fs.writeFile(appDefinitionPath, newContent, 'utf8'); + } + }); +} + +async function tryAddBackend(input: PortableTemplateInput) { + const backendIndexPath = paths.resolveTargetRoot( + 'packages/backend/src/index.ts', + ); + if (!(await fs.pathExists(backendIndexPath))) { + return; + } + + await Task.forItem('backend', `adding ${input.packageName}`, async () => { + const content = await fs.readFile(backendIndexPath, 'utf8'); + const lines = content.split('\n'); + const backendAddLine = `backend.add(import('${input.packageName}'));`; + + const backendStartIndex = lines.findIndex(line => + line.match(/backend.start/), + ); + + if (backendStartIndex !== -1) { + const [indentation] = lines[backendStartIndex].match(/^\s*/)!; + lines.splice(backendStartIndex, 0, `${indentation}${backendAddLine}`); + + const newContent = lines.join('\n'); + await fs.writeFile(backendIndexPath, newContent, 'utf8'); + } + }); +} diff --git a/packages/cli/src/lib/new/preparation/loadPortableTemplate.ts b/packages/cli/src/lib/new/preparation/loadPortableTemplate.ts index 2b179e9459..05ba267271 100644 --- a/packages/cli/src/lib/new/preparation/loadPortableTemplate.ts +++ b/packages/cli/src/lib/new/preparation/loadPortableTemplate.ts @@ -35,7 +35,6 @@ const templateDefinitionSchema = z description: z.string().optional(), template: z.string(), role: z.enum(TEMPLATE_ROLES), - additionalActions: z.array(z.string()).optional(), templateValues: z.record(z.string()).optional(), }) .strict(); diff --git a/packages/cli/src/lib/new/types.ts b/packages/cli/src/lib/new/types.ts index b460dd842c..cb940120fe 100644 --- a/packages/cli/src/lib/new/types.ts +++ b/packages/cli/src/lib/new/types.ts @@ -73,7 +73,6 @@ export type PortableTemplate = { id: string; description?: string; role: PortableTemplateRole; - additionalActions?: string[]; files: PortableTemplateFile[]; templateValues: Record; }; diff --git a/packages/cli/templates/default-backend-module.yaml b/packages/cli/templates/default-backend-module.yaml index 81cfd49a13..c3c3817510 100644 --- a/packages/cli/templates/default-backend-module.yaml +++ b/packages/cli/templates/default-backend-module.yaml @@ -1,8 +1,5 @@ description: A new backend module that extends an existing backend plugin with additional features template: ./default-backend-module role: backend-plugin-module -additionalActions: - - install-backend - - add-backend templateValues: moduleVar: '{{ camelCase pluginId }}Module{{ upperFirst ( camelCase moduleId ) }}' diff --git a/packages/cli/templates/default-backend-plugin.yaml b/packages/cli/templates/default-backend-plugin.yaml index b02cbc7a81..3c6107cdb0 100644 --- a/packages/cli/templates/default-backend-plugin.yaml +++ b/packages/cli/templates/default-backend-plugin.yaml @@ -1,8 +1,5 @@ description: A new backend plugin template: ./default-backend-plugin role: backend-plugin -additionalActions: - - install-backend - - add-backend templateValues: pluginVar: '{{ camelCase pluginId }}Plugin' diff --git a/packages/cli/templates/default-plugin.yaml b/packages/cli/templates/default-plugin.yaml index 1bc42eb1da..64a2e25988 100644 --- a/packages/cli/templates/default-plugin.yaml +++ b/packages/cli/templates/default-plugin.yaml @@ -1,9 +1,6 @@ description: A new frontend plugin template: ./default-plugin role: frontend-plugin -additionalActions: - - install-frontend - - add-frontend-legacy templateValues: pluginVar: '{{ camelCase pluginId }}Plugin' extensionName: '{{ upperFirst ( camelCase pluginId ) }}Page' diff --git a/packages/cli/templates/scaffolder-module.yaml b/packages/cli/templates/scaffolder-module.yaml index 5a773759f3..a9ad7ddf13 100644 --- a/packages/cli/templates/scaffolder-module.yaml +++ b/packages/cli/templates/scaffolder-module.yaml @@ -5,6 +5,3 @@ params: pluginId: scaffolder templateValues: moduleVar: '{{ camelCase pluginId }}Module{{ upperFirst ( camelCase moduleId ) }}' -additionalActions: - - install-backend - - add-backend