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:
@@ -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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
+111
-116
@@ -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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
+39
-40
@@ -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}`);
|
||||
}
|
||||
|
||||
+7
-8
@@ -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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
+7
-8
@@ -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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user