diff --git a/packages/cli/src/lib/new/execution/executePluginPackageTemplate.ts b/packages/cli/src/lib/new/execution/executePluginPackageTemplate.ts index 8acf02c593..a6edb2eb3b 100644 --- a/packages/cli/src/lib/new/execution/executePluginPackageTemplate.ts +++ b/packages/cli/src/lib/new/execution/executePluginPackageTemplate.ts @@ -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; - templateDir: string; - targetDir: string; - values: Record; - }, -) { - 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, 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, + ); + }); + }, + ); } } } diff --git a/packages/cli/src/lib/new/execution/executePortableTemplate.ts b/packages/cli/src/lib/new/execution/executePortableTemplate.ts index 3bf91207eb..17261fe5b9 100644 --- a/packages/cli/src/lib/new/execution/executePortableTemplate.ts +++ b/packages/cli/src/lib/new/execution/executePortableTemplate.ts @@ -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); diff --git a/packages/cli/src/lib/new/preparation/loadPortableTemplate.ts b/packages/cli/src/lib/new/preparation/loadPortableTemplate.ts index dd0653a1c1..1976e3b895 100644 --- a/packages/cli/src/lib/new/preparation/loadPortableTemplate.ts +++ b/packages/cli/src/lib/new/preparation/loadPortableTemplate.ts @@ -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(); + + 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 }; } diff --git a/packages/cli/src/lib/new/types.ts b/packages/cli/src/lib/new/types.ts index 82382b602d..b55ae23d28 100644 --- a/packages/cli/src/lib/new/types.ts +++ b/packages/cli/src/lib/new/types.ts @@ -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; };