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:
@@ -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`.
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user