cli/new: read template content during preparation phase
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user