cli/new: replace additionalActions with fixed installation per role

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-02-10 00:10:47 +01:00
parent b15ecadc9a
commit 100681e427
9 changed files with 149 additions and 208 deletions
@@ -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<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
},
) {
try {
const pkgJson = await fs.readJson(path);
const normalize = (obj: Record<string, string>) => {
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 = `<Route path="/${roleParams.pluginId}" element={<${extensionName} />} />`;
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');
}
});
}
}
@@ -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);
@@ -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 = `<Route path="/${roleParams.pluginId}" element={<${extensionName} />} />`;
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');
}
});
}
@@ -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();
-1
View File
@@ -73,7 +73,6 @@ export type PortableTemplate = {
id: string;
description?: string;
role: PortableTemplateRole;
additionalActions?: string[];
files: PortableTemplateFile[];
templateValues: Record<string, string>;
};
@@ -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 ) }}'
@@ -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'
@@ -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'
@@ -5,6 +5,3 @@ params:
pluginId: scaffolder
templateValues:
moduleVar: '{{ camelCase pluginId }}Module{{ upperFirst ( camelCase moduleId ) }}'
additionalActions:
- install-backend
- add-backend