cli/new: split out templating

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-02-08 20:57:53 +01:00
parent 82b1d4d6b1
commit 5ff71a719e
2 changed files with 108 additions and 58 deletions
@@ -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<string, string>,
values: PortableTemplateParams,
): Record<string, string> {
return Object.fromEntries(
Object.entries(record).map(([key, value]) => [
key,
this.template(value, values),
]),
);
}
}
@@ -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 => {