chore: refactor a little bit to keep the typings from the original context

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
benjdlambert
2025-04-29 14:56:44 +02:00
parent 587cb050a5
commit 8c9851f13f
6 changed files with 201 additions and 205 deletions
@@ -131,24 +131,26 @@ export function createFetchTemplateAction(options: {
},
},
supportsDryRun: true,
handler: createTemplateActionHandler({
resolveTemplate: async ctx => {
ctx.logger.info('Fetching template content from remote URL');
handler: ctx =>
createTemplateActionHandler({
ctx,
resolveTemplate: async () => {
ctx.logger.info('Fetching template content from remote URL');
const workDir = await ctx.createTemporaryDirectory();
const templateDir = resolveSafeChildPath(workDir, 'template');
const workDir = await ctx.createTemporaryDirectory();
const templateDir = resolveSafeChildPath(workDir, 'template');
await fetchContents({
baseUrl: ctx.templateInfo?.baseUrl,
fetchUrl: ctx.input.url,
outputPath: templateDir,
token: ctx.input.token,
...options,
});
await fetchContents({
baseUrl: ctx.templateInfo?.baseUrl,
fetchUrl: ctx.input.url,
outputPath: templateDir,
token: ctx.input.token,
...options,
});
return templateDir;
},
...options,
}),
return templateDir;
},
...options,
}),
});
}
@@ -21,7 +21,6 @@ import { InputError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import {
ActionContext,
TemplateActionOptions,
TemplateFilter,
TemplateGlobal,
} from '@backstage/plugin-scaffolder-node';
@@ -50,19 +49,21 @@ export type TemplateActionInput = {
lstripBlocks?: boolean;
};
export function createTemplateActionHandler<
I extends TemplateActionInput = TemplateActionInput,
export async function createTemplateActionHandler<
I extends TemplateActionInput,
>(options: {
resolveTemplate: (ctx: ActionContext<I, any, any>) => Promise<string>;
ctx: ActionContext<I, any, any>;
resolveTemplate: () => Promise<string>;
integrations: ScmIntegrations;
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
}): TemplateActionOptions<I>['handler'] {
}) {
const {
resolveTemplate,
integrations,
additionalTemplateFilters,
additionalTemplateGlobals: templateGlobals,
ctx,
} = options;
const templateFilters = {
@@ -70,134 +71,128 @@ export function createTemplateActionHandler<
...additionalTemplateFilters,
};
return async (ctx: ActionContext<I, any, any>) => {
const { outputDir, copyOnlyPatterns, renderFilename, extension } =
resolveTemplateActionSettings(ctx);
const { outputDir, copyOnlyPatterns, renderFilename, extension } =
resolveTemplateActionSettings(ctx);
const templateDir = await resolveTemplate(ctx);
const templateDir = await resolveTemplate();
if (isChildPath(templateDir, outputDir)) {
throw new InputError('targetPath must not be within template path');
}
if (isChildPath(templateDir, outputDir)) {
throw new InputError('targetPath must not be within template path');
}
ctx.logger.info('Listing files and directories in template');
const allEntriesInTemplate = await globby(`**/*`, {
ctx.logger.info('Listing files and directories in template');
const allEntriesInTemplate = await globby(`**/*`, {
cwd: templateDir,
dot: true,
onlyFiles: false,
markDirectories: true,
followSymbolicLinks: false,
});
const nonTemplatedEntries = new Set(
await globby(copyOnlyPatterns || [], {
cwd: templateDir,
dot: true,
onlyFiles: false,
markDirectories: true,
followSymbolicLinks: false,
});
}),
);
const nonTemplatedEntries = new Set(
await globby(copyOnlyPatterns || [], {
cwd: templateDir,
dot: true,
onlyFiles: false,
markDirectories: true,
followSymbolicLinks: false,
}),
);
// Cookiecutter prefixes all parameters in templates with
// `cookiecutter.`. To replicate this, we wrap our parameters
// in an object with a `cookiecutter` property when compat
// mode is enabled.
const { cookiecutterCompat, values } = ctx.input;
const context = {
[cookiecutterCompat ? 'cookiecutter' : 'values']: values,
};
// Cookiecutter prefixes all parameters in templates with
// `cookiecutter.`. To replicate this, we wrap our parameters
// in an object with a `cookiecutter` property when compat
// mode is enabled.
const { cookiecutterCompat, values } = ctx.input;
const context = {
[cookiecutterCompat ? 'cookiecutter' : 'values']: values,
};
ctx.logger.info(
`Processing ${allEntriesInTemplate.length} template files/directories with input values`,
ctx.input.values,
);
ctx.logger.info(
`Processing ${allEntriesInTemplate.length} template files/directories with input values`,
ctx.input.values,
);
const renderTemplate = await SecureTemplater.loadRenderer({
cookiecutterCompat: ctx.input.cookiecutterCompat,
templateFilters,
templateGlobals,
nunjucksConfigs: {
trimBlocks: ctx.input.trimBlocks,
lstripBlocks: ctx.input.lstripBlocks,
},
});
const renderTemplate = await SecureTemplater.loadRenderer({
cookiecutterCompat: ctx.input.cookiecutterCompat,
templateFilters,
templateGlobals,
nunjucksConfigs: {
trimBlocks: ctx.input.trimBlocks,
lstripBlocks: ctx.input.lstripBlocks,
},
});
for (const location of allEntriesInTemplate) {
let renderContents: boolean;
for (const location of allEntriesInTemplate) {
let renderContents: boolean;
let localOutputPath = location;
if (extension) {
renderContents = extname(localOutputPath) === extension;
if (renderContents) {
localOutputPath = localOutputPath.slice(0, -extension.length);
}
// extension is mutual exclusive with copyWithoutRender/copyWithoutTemplating,
// therefore the output path is always rendered.
let localOutputPath = location;
if (extension) {
renderContents = extname(localOutputPath) === extension;
if (renderContents) {
localOutputPath = localOutputPath.slice(0, -extension.length);
}
// extension is mutual exclusive with copyWithoutRender/copyWithoutTemplating,
// therefore the output path is always rendered.
localOutputPath = renderTemplate(localOutputPath, context);
} else {
renderContents = !nonTemplatedEntries.has(location);
// The logic here is a bit tangled because it depends on two variables.
// If renderFilename is true, which means copyWithoutTemplating is used,
// then the path is always rendered.
// If renderFilename is false, which means copyWithoutRender is used,
// then matched file/directory won't be processed, same as before.
if (renderFilename) {
localOutputPath = renderTemplate(localOutputPath, context);
} else {
renderContents = !nonTemplatedEntries.has(location);
// The logic here is a bit tangled because it depends on two variables.
// If renderFilename is true, which means copyWithoutTemplating is used,
// then the path is always rendered.
// If renderFilename is false, which means copyWithoutRender is used,
// then matched file/directory won't be processed, same as before.
if (renderFilename) {
localOutputPath = renderTemplate(localOutputPath, context);
} else {
localOutputPath = renderContents
? renderTemplate(localOutputPath, context)
: localOutputPath;
}
}
if (containsSkippedContent(localOutputPath)) {
continue;
}
const outputPath = resolveSafeChildPath(outputDir, localOutputPath);
if (fs.existsSync(outputPath) && !ctx.input.replace) {
continue;
}
if (!renderContents && !extension) {
ctx.logger.info(
`Copying file/directory ${location} without processing.`,
);
}
if (location.endsWith('/')) {
ctx.logger.info(
`Writing directory ${location} to template output path.`,
);
await fs.ensureDir(outputPath);
} else {
const inputFilePath = resolveSafeChildPath(templateDir, location);
const stats = await fs.promises.lstat(inputFilePath);
if (stats.isSymbolicLink() || (await isBinaryFile(inputFilePath))) {
ctx.logger.info(
`Copying file binary or symbolic link at ${location}, to template output path.`,
);
await fs.copy(inputFilePath, outputPath);
} else {
const statsObj = await fs.stat(inputFilePath);
ctx.logger.info(
`Writing file ${location} to template output path with mode ${statsObj.mode}.`,
);
const inputFileContents = await fs.readFile(inputFilePath, 'utf-8');
await fs.outputFile(
outputPath,
renderContents
? renderTemplate(inputFileContents, context)
: inputFileContents,
{ mode: statsObj.mode },
);
}
localOutputPath = renderContents
? renderTemplate(localOutputPath, context)
: localOutputPath;
}
}
ctx.logger.info(`Template result written to ${outputDir}`);
};
if (containsSkippedContent(localOutputPath)) {
continue;
}
const outputPath = resolveSafeChildPath(outputDir, localOutputPath);
if (fs.existsSync(outputPath) && !ctx.input.replace) {
continue;
}
if (!renderContents && !extension) {
ctx.logger.info(`Copying file/directory ${location} without processing.`);
}
if (location.endsWith('/')) {
ctx.logger.info(`Writing directory ${location} to template output path.`);
await fs.ensureDir(outputPath);
} else {
const inputFilePath = resolveSafeChildPath(templateDir, location);
const stats = await fs.promises.lstat(inputFilePath);
if (stats.isSymbolicLink() || (await isBinaryFile(inputFilePath))) {
ctx.logger.info(
`Copying file binary or symbolic link at ${location}, to template output path.`,
);
await fs.copy(inputFilePath, outputPath);
} else {
const statsObj = await fs.stat(inputFilePath);
ctx.logger.info(
`Writing file ${location} to template output path with mode ${statsObj.mode}.`,
);
const inputFileContents = await fs.readFile(inputFilePath, 'utf-8');
await fs.outputFile(
outputPath,
renderContents
? renderTemplate(inputFileContents, context)
: inputFileContents,
{ mode: statsObj.mode },
);
}
}
}
ctx.logger.info(`Template result written to ${outputDir}`);
}
function resolveTemplateActionSettings<I extends TemplateActionInput>(
@@ -95,24 +95,26 @@ export function createFetchTemplateFileAction(options: {
},
},
supportsDryRun: true,
handler: createTemplateFileActionHandler({
resolveTemplateFile: async ctx => {
ctx.logger.info('Fetching template file content from remote URL');
handler: ctx =>
createTemplateFileActionHandler({
ctx,
resolveTemplateFile: async () => {
ctx.logger.info('Fetching template file content from remote URL');
const workDir = await ctx.createTemporaryDirectory();
// Write to a tmp file, render the template, then copy to workspace.
const tmpFilePath = path.join(workDir, 'tmp');
const workDir = await ctx.createTemporaryDirectory();
// Write to a tmp file, render the template, then copy to workspace.
const tmpFilePath = path.join(workDir, 'tmp');
await fetchFile({
baseUrl: ctx.templateInfo?.baseUrl,
fetchUrl: ctx.input.url,
outputPath: tmpFilePath,
token: ctx.input.token,
...options,
});
return tmpFilePath;
},
...options,
}),
await fetchFile({
baseUrl: ctx.templateInfo?.baseUrl,
fetchUrl: ctx.input.url,
outputPath: tmpFilePath,
token: ctx.input.token,
...options,
});
return tmpFilePath;
},
...options,
}),
});
}
@@ -16,7 +16,6 @@
import { ScmIntegrations } from '@backstage/integration';
import {
ActionContext,
TemplateActionOptions,
TemplateFilter,
TemplateGlobal,
} from '@backstage/plugin-scaffolder-node';
@@ -36,19 +35,21 @@ export type TemplateFileActionInput = {
lstripBlocks?: boolean;
};
export function createTemplateFileActionHandler<
export async function createTemplateFileActionHandler<
I extends TemplateFileActionInput = TemplateFileActionInput,
>(options: {
resolveTemplateFile: (ctx: ActionContext<I, any, any>) => Promise<string>;
ctx: ActionContext<I, any, any>;
resolveTemplateFile: () => Promise<string>;
integrations: ScmIntegrations;
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
}): TemplateActionOptions<I>['handler'] {
}) {
const {
resolveTemplateFile,
integrations,
additionalTemplateFilters,
additionalTemplateGlobals: templateGlobals,
ctx,
} = options;
const templateFilters = {
@@ -56,45 +57,43 @@ export function createTemplateFileActionHandler<
...additionalTemplateFilters,
};
return async (ctx: ActionContext<I, any, any>) => {
const outputPath = resolveSafeChildPath(
ctx.workspacePath,
ctx.input.targetPath,
);
if (fs.existsSync(outputPath) && !ctx.input.replace) {
ctx.logger.info(
`File ${ctx.input.targetPath} already exists in workspace, not replacing.`,
);
return;
}
const filePath = await resolveTemplateFile(ctx);
const { cookiecutterCompat, values } = ctx.input;
const context = {
[cookiecutterCompat ? 'cookiecutter' : 'values']: values,
};
const outputPath = resolveSafeChildPath(
ctx.workspacePath,
ctx.input.targetPath,
);
if (fs.existsSync(outputPath) && !ctx.input.replace) {
ctx.logger.info(
`Processing template file with input values`,
ctx.input.values,
`File ${ctx.input.targetPath} already exists in workspace, not replacing.`,
);
return;
}
const filePath = await resolveTemplateFile();
const renderTemplate = await SecureTemplater.loadRenderer({
cookiecutterCompat,
templateFilters,
templateGlobals,
nunjucksConfigs: {
trimBlocks: ctx.input.trimBlocks,
lstripBlocks: ctx.input.lstripBlocks,
},
});
const contents = await fs.readFile(filePath, 'utf-8');
const result = renderTemplate(contents, context);
await fs.ensureDir(path.dirname(outputPath));
await fs.outputFile(outputPath, result);
ctx.logger.info(`Template file has been written to ${outputPath}`);
const { cookiecutterCompat, values } = ctx.input;
const context = {
[cookiecutterCompat ? 'cookiecutter' : 'values']: values,
};
ctx.logger.info(
`Processing template file with input values`,
ctx.input.values,
);
const renderTemplate = await SecureTemplater.loadRenderer({
cookiecutterCompat,
templateFilters,
templateGlobals,
nunjucksConfigs: {
trimBlocks: ctx.input.trimBlocks,
lstripBlocks: ctx.input.lstripBlocks,
},
});
const contents = await fs.readFile(filePath, 'utf-8');
const result = renderTemplate(contents, context);
await fs.ensureDir(path.dirname(outputPath));
await fs.outputFile(outputPath, result);
ctx.logger.info(`Template file has been written to ${outputPath}`);
}
@@ -90,13 +90,12 @@ export function createWorkspaceTemplateAction(options: {
},
},
supportsDryRun: true,
handler: createTemplateActionHandler({
resolveTemplate: async ctx =>
resolveSafeChildPath(
ctx.workspacePath,
(ctx.input as any as { sourcePath: string }).sourcePath,
),
...options,
}),
handler: ctx =>
createTemplateActionHandler({
ctx,
resolveTemplate: async () =>
resolveSafeChildPath(ctx.workspacePath, ctx.input.sourcePath),
...options,
}),
});
}
@@ -65,13 +65,12 @@ export function createWorkspaceTemplateFileAction(options: {
},
},
supportsDryRun: true,
handler: createTemplateFileActionHandler({
resolveTemplateFile: async ctx =>
resolveSafeChildPath(
ctx.workspacePath,
(ctx.input as any as { sourcePath: string }).sourcePath,
),
...options,
}),
handler: ctx =>
createTemplateFileActionHandler({
ctx,
resolveTemplateFile: async () =>
resolveSafeChildPath(ctx.workspacePath, ctx.input.sourcePath),
...options,
}),
});
}