cli/new: replace additionalActions with fixed installation per role
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user