cli/new: split out templating
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user