cli/new: refactor templater to make it stateful

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-02-08 21:09:11 +01:00
parent ece4911b9e
commit 451205b246
2 changed files with 41 additions and 39 deletions
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import handlebars, { HelperDeclareSpec } from 'handlebars';
import handlebars from 'handlebars';
import { PortableTemplateParams } from '../types';
import camelCase from 'lodash/camelCase';
import kebabCase from 'lodash/kebabCase';
@@ -39,12 +39,13 @@ const builtInHelpers = {
lowerFirst,
};
type CreateTemplaterOptions = {
helpers?: HelperDeclareSpec;
type CreatePortableTemplaterOptions = {
values?: PortableTemplateParams;
templatedValues?: Record<string, string>;
};
export class PortableTemplater {
static async create() {
static async create(options: CreatePortableTemplaterOptions = {}) {
let lockfile: Lockfile | undefined;
try {
lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock'));
@@ -54,8 +55,8 @@ export class PortableTemplater {
const versionProvider = createPackageVersionProvider(lockfile);
return new PortableTemplater({
helpers: {
const templater = new PortableTemplater(
{
versionQuery(name: string, versionHint: string | unknown) {
return versionProvider(
name,
@@ -63,36 +64,44 @@ export class PortableTemplater {
);
},
},
});
options.values ?? {},
);
if (options.templatedValues) {
templater.appendTemplatedValues(options.templatedValues);
}
return templater;
}
readonly #templater: typeof handlebars;
#values: PortableTemplateParams;
private constructor(options: CreateTemplaterOptions = {}) {
private constructor(
helpers: handlebars.HelperDeclareSpec,
values: PortableTemplateParams,
) {
this.#templater = handlebars.create();
this.#templater.registerHelper(builtInHelpers);
if (options.helpers) {
this.#templater.registerHelper(options.helpers);
if (helpers) {
this.#templater.registerHelper(helpers);
}
this.#values = values;
}
template(content: string, values: PortableTemplateParams): string {
template(content: string): string {
return this.#templater.compile(content, {
strict: true,
})(values);
})(this.#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),
]),
appendTemplatedValues(record: Record<string, string>): void {
const newValues = Object.fromEntries(
Object.entries(record).map(([key, value]) => [key, this.template(value)]),
);
this.#values = { ...this.#values, ...newValues };
}
}
@@ -49,12 +49,11 @@ export async function executePluginPackageTemplate(
try {
const tempDir = await tmpDirManager.createDir('backstage-create');
const isMonoRepo = await getIsMonoRepo();
const templater = await PortableTemplater.create();
const templatedValues = templater.templateRecord(
template.templateValues,
input.params,
);
const templater = await PortableTemplater.create({
values: input.params,
templatedValues: template.templateValues,
});
for (const file of template.files) {
if (isMonoRepo && file.path === 'tsconfig.json') {
@@ -64,20 +63,14 @@ export async function executePluginPackageTemplate(
const destPath = resolvePath(tempDir, file.path);
await fs.ensureDir(dirname(destPath));
if (file.syntax === 'handlebars') {
let content = file.content;
const content =
file.syntax === 'handlebars'
? templater.template(file.content)
: file.content;
if (file.syntax === 'handlebars') {
content = templater.template(file.content, {
...input.params,
...templatedValues,
});
}
await fs.writeFile(destPath, content).catch(error => {
throw new ForwardedError(`Failed to copy file to ${destPath}`, error);
});
}
await fs.writeFile(destPath, content).catch(error => {
throw new ForwardedError(`Failed to copy file to ${destPath}`, error);
});
}
// Format package.json if it exists