cli/new: read template content during preparation phase

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-02-08 16:28:18 +01:00
parent 0244176c98
commit eec35e9c90
4 changed files with 106 additions and 92 deletions
@@ -17,7 +17,6 @@
import fs from 'fs-extra';
import chalk from 'chalk';
import handlebars from 'handlebars';
import recursive from 'recursive-readdir';
import {
basename,
dirname,
@@ -37,6 +36,8 @@ import { paths } from '../../paths';
import { Task } from '../../tasks';
import { Lockfile } from '../../versioning';
import { createPackageVersionProvider } from '../../version';
import { PortableTemplate, PortableTemplateInput } from '../types';
import { ForwardedError } from '@backstage/errors';
const helpers = {
camelCase,
@@ -61,15 +62,11 @@ export interface CreateContext {
}
export async function executePluginPackageTemplate(
template: PortableTemplate,
input: PortableTemplateInput,
ctx: CreateContext,
options: {
templateValues: Record<string, string>;
templateDir: string;
targetDir: string;
values: Record<string, unknown>;
},
) {
const { targetDir, templateDir, values } = options;
): Promise<{ targetDir: string }> {
const targetDir = paths.resolveTargetRoot(input.packageParams.packagePath);
let lockfile: Lockfile | undefined;
try {
@@ -96,10 +93,9 @@ export async function executePluginPackageTemplate(
Task.section('Executing Template');
await templatingTask(
templateDir,
options.templateValues,
tempDir,
values,
template,
input,
createPackageVersionProvider(lockfile),
ctx.isMonoRepo,
);
@@ -121,72 +117,66 @@ export async function executePluginPackageTemplate(
});
ctx.markAsModified();
return { targetDir };
}
export async function templatingTask(
templateDir: string,
templateValues: Record<string, string>,
destinationDir: string,
context: any,
template: PortableTemplate,
input: PortableTemplateInput,
versionProvider: (name: string, versionHint?: string) => string,
isMonoRepo: boolean,
) {
const files = await recursive(templateDir).catch(error => {
throw new Error(`Failed to read template directory: ${error.message}`);
});
const templatedValues = Object.fromEntries(
Object.entries(templateValues).map(([name, tmpl]) => {
return [name, handlebars.compile(tmpl)(context, { helpers })];
Object.entries(template.templateValues).map(([name, tmpl]) => {
return [name, handlebars.compile(tmpl)(input.params, { helpers })];
}),
);
for (const file of files) {
const destinationFile = file.replace(templateDir, destinationDir);
await fs.ensureDir(dirname(destinationFile));
for (const file of template.files) {
if (isMonoRepo && file.path === 'tsconfig.json') {
continue;
}
if (file.endsWith('.hbs')) {
await Task.forItem('templating', basename(file), async () => {
const destination = destinationFile.replace(/\.hbs$/, '');
const destPath = resolvePath(destinationDir, file.path);
await fs.ensureDir(dirname(destPath));
const template = await fs.readFile(file);
const compiled = handlebars.compile(template.toString(), {
strict: true,
});
const contents = compiled(
{ name: basename(destination), ...context, ...templatedValues },
{
helpers: {
versionQuery(name: string, versionHint: string | unknown) {
return versionProvider(
name,
typeof versionHint === 'string' ? versionHint : undefined,
);
if (file.syntax === 'handlebars') {
await Task.forItem(
file.syntax ? 'templating' : 'copying',
file.path,
async () => {
let content = file.content;
if (file.syntax === 'handlebars') {
const compiled = handlebars.compile(file.content, {
strict: true,
});
content = compiled(
{ name: basename(destPath), ...input.params, ...templatedValues },
{
helpers: {
versionQuery(name: string, versionHint: string | unknown) {
return versionProvider(
name,
typeof versionHint === 'string' ? versionHint : undefined,
);
},
...helpers,
},
},
...helpers,
},
},
);
);
}
await fs.writeFile(destination, contents).catch(error => {
throw new Error(
`Failed to create file: ${destination}: ${error.message}`,
);
});
});
} else {
if (isMonoRepo && file.match('tsconfig.json')) {
continue;
}
await Task.forItem('copying', basename(file), async () => {
await fs.copyFile(file, destinationFile).catch(error => {
const destination = destinationFile;
throw new Error(
`Failed to copy file to ${destination} : ${error.message}`,
);
});
});
await fs.writeFile(destPath, content).catch(error => {
throw new ForwardedError(
`Failed to copy file to ${destPath}`,
error,
);
});
},
);
}
}
}
@@ -16,16 +16,16 @@
import { isMonoRepo } from '@backstage/cli-node';
import { assertError } from '@backstage/errors';
import { paths } from '../../paths';
import { Task } from '../../tasks';
import { addCodeownersEntry } from '../../codeowners';
import { Task } from '../../tasks';
import {
PortableTemplate,
PortableTemplateConfig,
PortableTemplateInput,
} from '../types';
import { TemporaryDirectoryManager } from './TemporaryDirectoryManager';
import { runAdditionalActions } from './additionalActions';
import { executePluginPackageTemplate } from './executePluginPackageTemplate';
import { TemporaryDirectoryManager } from './TemporaryDirectoryManager';
import { PortableTemplateConfig, PortableTemplateInput } from '../types';
import { PortableTemplate } from '../types';
type ExecuteNewTemplateOptions = {
config: PortableTemplateConfig;
@@ -40,25 +40,15 @@ export async function executePortableTemplate(
const tmpDirManager = TemporaryDirectoryManager.create();
const targetDir = paths.resolveTargetRoot(input.packageParams.packagePath);
let modified = false;
try {
await executePluginPackageTemplate(
{
isMonoRepo: await isMonoRepo(),
createTemporaryDirectory: tmpDirManager.createDir,
markAsModified() {
modified = true;
},
const { targetDir } = await executePluginPackageTemplate(template, input, {
isMonoRepo: await isMonoRepo(),
createTemporaryDirectory: tmpDirManager.createDir,
markAsModified() {
modified = true;
},
{
targetDir,
templateDir: template.templatePath,
templateValues: template.templateValues,
values: input.params,
},
);
});
if (template.additionalActions?.length) {
await runAdditionalActions(template, input);
@@ -16,11 +16,16 @@
import { z } from 'zod';
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import recursiveReaddir from 'recursive-readdir';
import { resolve as resolvePath, relative as relativePath } from 'path';
import { dirname } from 'node:path';
import { parse as parseYaml } from 'yaml';
import { paths } from '../../paths';
import { PortableTemplatePointer, TEMPLATE_ROLES } from '../types';
import {
PortableTemplateFile,
PortableTemplatePointer,
TEMPLATE_ROLES,
} from '../types';
import { PortableTemplate } from '../types';
import { ForwardedError } from '@backstage/errors';
import { fromZodError } from 'zod-validation-error';
@@ -74,8 +79,31 @@ export async function loadPortableTemplate({
const { template, templateValues = {}, ...templateData } = parsed.data;
const templatePath = resolvePath(dirname(target), template);
if (!fs.existsSync(templatePath)) {
throw new Error(`Failed to load template contents from '${templatePath}'`);
const filePaths = await recursiveReaddir(templatePath).catch(error => {
throw new ForwardedError(
`Failed to load template contents from '${templatePath}'`,
error,
);
});
const files = new Array<PortableTemplateFile>();
for (const filePath of filePaths) {
const path = relativePath(templatePath, filePath);
const content = await fs.readFile(filePath, 'utf-8').catch(error => {
throw new ForwardedError(
`Failed to load file contents from '${path}'`,
error,
);
});
if (path.endsWith('.hbs')) {
files.push({ path: path.slice(0, -4), content, syntax: 'handlebars' });
} else {
files.push({ path, content });
}
}
return { id, templatePath, templateValues, ...templateData };
return { id, templateValues, ...templateData, files };
}
+7 -1
View File
@@ -58,14 +58,20 @@ export const TEMPLATE_ROLES = [
export type PortableTemplateRole = (typeof TEMPLATE_ROLES)[number];
export type PortableTemplateFile = {
path: string;
content: string;
syntax?: 'handlebars';
};
export type PortableTemplate = {
id: string;
description?: string;
templatePath: string;
targetPath: string;
role: PortableTemplateRole;
prompts?: PortableTemplatePrompt[];
additionalActions?: string[];
files: PortableTemplateFile[];
templateValues: Record<string, string>;
};