Add copyWithoutTemplating and deprecate copyWithoutRender

Add `copyWithoutTemplating` to the the fetch template action input.
`copyWithoutTemplating` also accepts an array of glob patterns. Contents
of matched files or directories are copied without being processed, but
paths are subject to rendering.

Deprecate `copyWithoutRender` in favor of `copyWithoutTemplating`.

Signed-off-by: Mengnan Gong <namco1992@gmail.com>
This commit is contained in:
Mengnan Gong
2022-07-07 08:52:27 +08:00
committed by Namco
parent 106a0382af
commit ff316b86d8
4 changed files with 168 additions and 60 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Add `copyWithoutTemplating` to the fetch template action input. `copyWithoutTemplating` also accepts an array of glob patterns. Contents of matched files or directories are copied without being processed, but paths are subject to rendering.
Deprecate `copyWithoutRender` in favor of `copyWithoutTemplating`.
+1
View File
@@ -106,6 +106,7 @@ export function createFetchTemplateAction(options: {
values: any;
templateFileExtension?: string | boolean | undefined;
copyWithoutRender?: string[] | undefined;
copyWithoutTemplating?: string[] | undefined;
cookiecutterCompat?: boolean | undefined;
}>;
@@ -127,7 +127,22 @@ describe('fetch:template', () => {
action.handler(
mockContext({ copyWithoutRender: 'abc' as unknown as string[] }),
),
).rejects.toThrowError(/copyWithoutRender must be an array/i);
).rejects.toThrowError(
/copyWithoutRender\/copyWithoutTemplating must be an array/i,
);
});
it('throws if both copyWithoutRender and copyWithoutTemplating are used', async () => {
await expect(() =>
action.handler(
mockContext({
copyWithoutRender: 'abc' as unknown as string[],
copyWithoutTemplating: 'def' as unknown as string[],
}),
),
).rejects.toThrowError(
/copyWithoutRender and copyWithoutTemplating can not be used at the same time/i,
);
});
it('throws if copyWithoutRender is used with extension', async () => {
@@ -139,7 +154,7 @@ describe('fetch:template', () => {
}),
),
).rejects.toThrowError(
/input extension incompatible with copyWithoutRender and cookiecutterCompat/,
/input extension incompatible with copyWithoutRender\/copyWithoutTemplating and cookiecutterCompat/,
);
});
@@ -152,7 +167,7 @@ describe('fetch:template', () => {
}),
),
).rejects.toThrowError(
/input extension incompatible with copyWithoutRender and cookiecutterCompat/,
/input extension incompatible with copyWithoutRender\/copyWithoutTemplating and cookiecutterCompat/,
);
});
@@ -357,57 +372,105 @@ describe('fetch:template', () => {
).resolves.toBe(joinPath(workspacePath, 'target', 'a-binary-file.png'));
});
});
});
describe('copyWithoutRender', () => {
let context: ActionContext<FetchTemplateInput>;
describe('copyWithoutRender', () => {
let context: ActionContext<FetchTemplateInput>;
beforeEach(async () => {
context = mockContext({
values: {
name: 'test-project',
count: 1234,
},
copyWithoutRender: ['.unprocessed'],
});
beforeEach(async () => {
context = mockContext({
values: {
name: 'test-project',
count: 1234,
},
copyWithoutRender: ['.unprocessed'],
});
mockFetchContents.mockImplementation(({ outputPath }) => {
mockFs({
...realFiles,
[outputPath]: {
processed: {
'templated-content-${{ values.name }}.txt':
'${{ values.count }}',
},
'.unprocessed': {
'templated-content-${{ values.name }}.txt':
'${{ values.count }}',
},
mockFetchContents.mockImplementation(({ outputPath }) => {
mockFs({
...realFiles,
[outputPath]: {
processed: {
'templated-content-${{ values.name }}.txt': '${{ values.count }}',
},
});
return Promise.resolve();
'.unprocessed': {
'templated-content-${{ values.name }}.txt': '${{ values.count }}',
},
},
});
await action.handler(context);
return Promise.resolve();
});
it('ignores template syntax in files matched in copyWithoutRender', async () => {
await expect(
fs.readFile(
`${workspacePath}/target/.unprocessed/templated-content-\${{ values.name }}.txt`,
'utf-8',
),
).resolves.toEqual('${{ values.count }}');
await action.handler(context);
});
it('ignores template syntax in files matched in copyWithoutRender', async () => {
await expect(
fs.readFile(
`${workspacePath}/target/.unprocessed/templated-content-\${{ values.name }}.txt`,
'utf-8',
),
).resolves.toEqual('${{ values.count }}');
});
it('processes files not matched in copyWithoutRender', async () => {
await expect(
fs.readFile(
`${workspacePath}/target/processed/templated-content-test-project.txt`,
'utf-8',
),
).resolves.toEqual('1234');
});
});
describe('copyWithoutTemplating', () => {
let context: ActionContext<FetchTemplateInput>;
beforeEach(async () => {
context = mockContext({
values: {
name: 'test-project',
count: 1234,
},
copyWithoutTemplating: ['.unprocessed'],
});
it('processes files not matched in copyWithoutRender', async () => {
await expect(
fs.readFile(
`${workspacePath}/target/processed/templated-content-test-project.txt`,
'utf-8',
),
).resolves.toEqual('1234');
mockFetchContents.mockImplementation(({ outputPath }) => {
mockFs({
...realFiles,
[outputPath]: {
processed: {
'templated-content-${{ values.name }}.txt': '${{ values.count }}',
},
'.unprocessed': {
'templated-content-${{ values.name }}.txt': '${{ values.count }}',
},
},
});
return Promise.resolve();
});
await action.handler(context);
});
it('renders path template and ignores content template in files matched in copyWithoutTemplating', async () => {
await expect(
fs.readFile(
`${workspacePath}/target/.unprocessed/templated-content-test-project.txt`,
'utf-8',
),
).resolves.toEqual('${{ values.count }}');
});
it('processes files not matched in copyWithoutTemplating', async () => {
await expect(
fs.readFile(
`${workspacePath}/target/processed/templated-content-test-project.txt`,
'utf-8',
),
).resolves.toEqual('1234');
});
});
@@ -50,7 +50,11 @@ export function createFetchTemplateAction(options: {
templateFileExtension?: string | boolean;
// Cookiecutter compat options
/**
* @deprecated This field is deprecated in favor of copyWithoutTemplating.
*/
copyWithoutRender?: string[];
copyWithoutTemplating?: string[];
cookiecutterCompat?: boolean;
}>({
id: 'fetch:template',
@@ -79,7 +83,7 @@ export function createFetchTemplateAction(options: {
type: 'object',
},
copyWithoutRender: {
title: 'Copy Without Render',
title: '[Deprecated] Copy Without Render',
description:
'An array of glob patterns. Any files or directories which match are copied without being processed as templates.',
type: 'array',
@@ -87,6 +91,15 @@ export function createFetchTemplateAction(options: {
type: 'string',
},
},
copyWithoutTemplating: {
title: 'Copy Without Templating',
description:
'An array of glob patterns. Contents of matched files or directories are copied without being processed, but paths are subject to rendering.',
type: 'array',
items: {
type: 'string',
},
},
cookiecutterCompat: {
title: 'Cookiecutter compatibility mode',
description:
@@ -111,22 +124,37 @@ export function createFetchTemplateAction(options: {
const targetPath = ctx.input.targetPath ?? './';
const outputDir = resolveSafeChildPath(ctx.workspacePath, targetPath);
if (
ctx.input.copyWithoutRender &&
!Array.isArray(ctx.input.copyWithoutRender)
) {
if (ctx.input.copyWithoutRender && ctx.input.copyWithoutTemplating) {
throw new InputError(
'Fetch action input copyWithoutRender must be an Array',
'Fetch action input copyWithoutRender and copyWithoutTemplating can not be used at the same time',
);
}
let copyOnlyPatterns: string[] | undefined;
let renderFilename: boolean;
if (ctx.input.copyWithoutRender) {
ctx.logger.warn(
'[Deprecated] Please use copyWithoutTemplating instead.',
);
copyOnlyPatterns = ctx.input.copyWithoutRender;
renderFilename = false;
} else {
copyOnlyPatterns = ctx.input.copyWithoutTemplating;
renderFilename = true;
}
if (copyOnlyPatterns && !Array.isArray(copyOnlyPatterns)) {
throw new InputError(
'Fetch action input copyWithoutRender/copyWithoutTemplating must be an Array',
);
}
if (
ctx.input.templateFileExtension &&
(ctx.input.copyWithoutRender || ctx.input.cookiecutterCompat)
(copyOnlyPatterns || ctx.input.cookiecutterCompat)
) {
throw new InputError(
'Fetch action input extension incompatible with copyWithoutRender and cookiecutterCompat',
'Fetch action input extension incompatible with copyWithoutRender/copyWithoutTemplating and cookiecutterCompat',
);
}
@@ -161,7 +189,7 @@ export function createFetchTemplateAction(options: {
const nonTemplatedEntries = new Set(
(
await Promise.all(
(ctx.input.copyWithoutRender || []).map(pattern =>
(copyOnlyPatterns || []).map(pattern =>
globby(pattern, {
cwd: templateDir,
dot: true,
@@ -194,22 +222,31 @@ export function createFetchTemplateAction(options: {
});
for (const location of allEntriesInTemplate) {
let renderFilename: boolean;
let renderContents: boolean;
let localOutputPath = location;
if (extension) {
renderFilename = true;
renderContents = extname(localOutputPath) === extension;
if (renderContents) {
localOutputPath = localOutputPath.slice(0, -extension.length);
}
} else {
renderFilename = renderContents = !nonTemplatedEntries.has(location);
}
if (renderFilename) {
// 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 {
localOutputPath = renderContents
? renderTemplate(localOutputPath, context)
: localOutputPath;
}
}
if (containsSkippedContent(localOutputPath)) {