diff --git a/packages/cli/src/lib/new/execution/PortableTemplater.ts b/packages/cli/src/lib/new/execution/PortableTemplater.ts new file mode 100644 index 0000000000..bce0d211af --- /dev/null +++ b/packages/cli/src/lib/new/execution/PortableTemplater.ts @@ -0,0 +1,98 @@ +/* + * Copyright 2025 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 handlebars, { HelperDeclareSpec } from 'handlebars'; +import { PortableTemplateParams } from '../types'; +import camelCase from 'lodash/camelCase'; +import kebabCase from 'lodash/kebabCase'; +import lowerCase from 'lodash/lowerCase'; +import snakeCase from 'lodash/snakeCase'; +import startCase from 'lodash/startCase'; +import upperCase from 'lodash/upperCase'; +import upperFirst from 'lodash/upperFirst'; +import lowerFirst from 'lodash/lowerFirst'; +import { Lockfile } from '../../versioning'; +import { paths } from '../../paths'; +import { createPackageVersionProvider } from '../../version'; + +const builtInHelpers = { + camelCase, + kebabCase, + lowerCase, + snakeCase, + startCase, + upperCase, + upperFirst, + lowerFirst, +}; + +type CreateTemplaterOptions = { + helpers?: HelperDeclareSpec; +}; + +export class PortableTemplater { + static async create() { + let lockfile: Lockfile | undefined; + try { + lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + } catch { + /* ignored */ + } + + const versionProvider = createPackageVersionProvider(lockfile); + + return new PortableTemplater({ + helpers: { + versionQuery(name: string, versionHint: string | unknown) { + return versionProvider( + name, + typeof versionHint === 'string' ? versionHint : undefined, + ); + }, + }, + }); + } + + readonly #templater: typeof handlebars; + + private constructor(options: CreateTemplaterOptions = {}) { + this.#templater = handlebars.create(); + + this.#templater.registerHelper(builtInHelpers); + + if (options.helpers) { + this.#templater.registerHelper(options.helpers); + } + } + + template(content: string, values: PortableTemplateParams): string { + return this.#templater.compile(content, { + strict: true, + })(values); + } + + templateRecord( + record: Record, + values: PortableTemplateParams, + ): Record { + return Object.fromEntries( + Object.entries(record).map(([key, value]) => [ + key, + this.template(value, values), + ]), + ); + } +} diff --git a/packages/cli/src/lib/new/execution/executePluginPackageTemplate.ts b/packages/cli/src/lib/new/execution/executePluginPackageTemplate.ts index 7edb1c6b20..1ea1547d2c 100644 --- a/packages/cli/src/lib/new/execution/executePluginPackageTemplate.ts +++ b/packages/cli/src/lib/new/execution/executePluginPackageTemplate.ts @@ -16,41 +16,19 @@ import fs from 'fs-extra'; import chalk from 'chalk'; -import handlebars from 'handlebars'; import { - basename, dirname, resolve as resolvePath, relative as relativePath, } from 'path'; -import camelCase from 'lodash/camelCase'; -import kebabCase from 'lodash/kebabCase'; -import lowerCase from 'lodash/lowerCase'; -import snakeCase from 'lodash/snakeCase'; -import startCase from 'lodash/startCase'; -import upperCase from 'lodash/upperCase'; -import upperFirst from 'lodash/upperFirst'; -import lowerFirst from 'lodash/lowerFirst'; 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'; import { TemporaryDirectoryManager } from './TemporaryDirectoryManager'; import { isMonoRepo } from '@backstage/cli-node'; - -const helpers = { - camelCase, - kebabCase, - lowerCase, - snakeCase, - startCase, - upperCase, - upperFirst, - lowerFirst, -}; +import { PortableTemplater } from './PortableTemplater'; export async function executePluginPackageTemplate( template: PortableTemplate, @@ -58,13 +36,6 @@ export async function executePluginPackageTemplate( ): Promise<{ targetDir: string }> { const targetDir = paths.resolveTargetRoot(input.packageParams.packagePath); - let lockfile: Lockfile | undefined; - try { - lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); - } catch { - /* ignored */ - } - Task.section('Checking Prerequisites'); const shortPluginDir = relativePath(paths.targetRoot, targetDir); await Task.forItem('availability', shortPluginDir, async () => { @@ -85,13 +56,7 @@ export async function executePluginPackageTemplate( }); Task.section('Executing Template'); - await templatingTask( - tempDir, - template, - input, - createPackageVersionProvider(lockfile), - await isMonoRepo(), - ); + await templatingTask(tempDir, template, input, await isMonoRepo()); // Format package.json if it exists const pkgJsonPath = resolvePath(tempDir, 'package.json'); @@ -119,13 +84,13 @@ export async function templatingTask( destinationDir: string, template: PortableTemplate, input: PortableTemplateInput, - versionProvider: (name: string, versionHint?: string) => string, inMonoRepo: boolean, ) { - const templatedValues = Object.fromEntries( - Object.entries(template.templateValues).map(([name, tmpl]) => { - return [name, handlebars.compile(tmpl)(input.params, { helpers })]; - }), + const templater = await PortableTemplater.create(); + + const templatedValues = templater.templateRecord( + template.templateValues, + input.params, ); for (const file of template.files) { @@ -144,23 +109,10 @@ export async function templatingTask( let content = file.content; if (file.syntax === 'handlebars') { - const compiled = handlebars.compile(file.content, { - strict: true, + content = templater.template(file.content, { + ...input.params, + ...templatedValues, }); - content = compiled( - { name: basename(destPath), ...input.params, ...templatedValues }, - { - helpers: { - versionQuery(name: string, versionHint: string | unknown) { - return versionProvider( - name, - typeof versionHint === 'string' ? versionHint : undefined, - ); - }, - ...helpers, - }, - }, - ); } await fs.writeFile(destPath, content).catch(error => {