scaffolder: add cookiecutter compat mode to fetch:template

Signed-off-by: Mike Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
Mike Lewis
2021-07-05 17:30:55 +01:00
committed by Himanshu Mishra
parent 9fba3e22b1
commit a8ded61923
2 changed files with 106 additions and 21 deletions
@@ -218,4 +218,58 @@ describe('fetch:template', () => {
});
});
});
describe('cookiecutter compatibility mode', () => {
let context: ActionContext<FetchTemplateInput>;
beforeEach(async () => {
context = mockContext({
values: {
name: 'test-project',
count: 1234,
itemList: ['first', 'second', 'third'],
},
cookieCutterCompat: true,
});
mockFetchContents.mockImplementation(({ outputPath }) => {
mockFs({
[outputPath]: {
'{{ cookiecutter.name }}.txt': 'static content',
subdir: {
'templated-content.txt':
'{{ cookiecutter.name }}: {{ cookiecutter.count }}',
},
'{{ cookiecutter.name }}.json':
'{{ cookiecutter.itemList | jsonify }}',
},
});
return Promise.resolve();
});
await action.handler(context);
});
it('copies files with cookiecutter-style templated names successfully', async () => {
await expect(
fs.readFile(`${workspacePath}/target/test-project.txt`, 'utf-8'),
).resolves.toEqual('static content');
});
it('copies files with cookiecutter-style templated content successfully', async () => {
await expect(
fs.readFile(
`${workspacePath}/target/subdir/templated-content.txt`,
'utf-8',
),
).resolves.toEqual('test-project: 1234');
});
it('includes the jsonify filter', async () => {
await expect(
fs.readFile(`${workspacePath}/target/test-project.json`, 'utf-8'),
).resolves.toEqual('["first","second","third"]');
});
});
});
@@ -24,11 +24,25 @@ import globby from 'globby';
import nunjucks from 'nunjucks';
import fs from 'fs-extra';
/*
* Maximise compatibility with Jinja (and therefore cookiecutter)
* using nunjucks jinja compat mode. Since this method mutates
* the global nunjucks instance, we can't enable this per-template,
* or only for templates with cookiecutter compat enabled, so the
* next best option is to explicitly enable it globally and allow
* folks to rely on jinja compatibility behaviour in fetch:template
* templates if they wish.
*
* cf. https://mozilla.github.io/nunjucks/api.html#installjinjacompat
*/
nunjucks.installJinjaCompat();
export type FetchTemplateInput = {
url: string;
targetPath?: string;
values: any;
copyWithoutRender?: string[];
cookieCutterCompat?: boolean;
};
export function createFetchTemplateAction(options: {
@@ -72,6 +86,13 @@ export function createFetchTemplateAction(options: {
type: 'string',
},
},
cookieCutterCompat: {
title: 'Cookiecutter compatibility mode',
// TODO(mtlewis): documentation for cookiecutter compat mode
description:
'Enable features to maximise compatibility with templates built for fetch:cookiecutter',
type: 'boolean',
},
// TODO(mtlewis/orkohunter): do we need to replicate the template extensions support
// from fetch:cookiecutter?
},
@@ -109,8 +130,6 @@ export function createFetchTemplateAction(options: {
outputPath: templateDir,
});
// at this point the templateDir contains the unprocessed contents of the skeleton directory
ctx.logger.info(
'Fetched template, beginning templating process with values',
ctx.input.values,
@@ -138,39 +157,51 @@ export function createFetchTemplateAction(options: {
).flat(),
);
// Nice for Cookiecutter compat
//
// TODO(mtlewis/orkohunter): parameterize all jinja2/cookiecutter compat
// TODO(mtlewis): introduce "cookiecutter" prefix for input variables when
// compat is enabled?
nunjucks.installJinjaCompat();
ctx.logger.info(allEntriesInTemplate);
// Create a templater
const templater = nunjucks.configure({
// TODO(mtlewis/orkohunter): Document Why we are changing the literals? Not here, but on scaffolder docs. ADR?
tags: {
variableStart: '${{',
variableEnd: '}}',
},
...(ctx.input.cookieCutterCompat
? {}
: {
tags: {
variableStart: '${{',
variableEnd: '}}',
},
}),
// We don't want this builtin auto-escaping, since uses HTML escape sequences
// like `&quot;` - the correct way to escape strings in our case depends on
// the file type.
autoescape: false,
});
// TODO(mtlewis/orkohunter) Evaluate whether this behavior is still appropriate when using nunjucks.
// As of now jsonify seems to be the most reliable way to do escaping,
// but is there a builtin filter to do this inside nunjucks
// (other than `autoescape` inside `configure` which escapes strings as HTML, which isn't right.).
templater.addFilter('jsonify', s => JSON.stringify(s));
if (ctx.input.cookieCutterCompat) {
// The "jsonify" filter built into cookiecutter is common
// in fetch:cookiecutter templates, so when compat mode
// is enabled we alias the "dump" filter from nunjucks as
// jsonify. Dump accepts an optional `spaces` parameter
// which enables indented output, but when this parameter
// is not supplied it works identically to jsonify.
//
// cf. https://cookiecutter.readthedocs.io/en/latest/advanced/template_extensions.html?highlight=jsonify#jsonify-extension
// cf. https://mozilla.github.io/nunjucks/templating.html#dump
templater.addFilter('jsonify', templater.getFilter('dump'));
}
// 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 parameters = ctx.input.cookieCutterCompat
? { cookiecutter: ctx.input.values }
: ctx.input.values;
for (const location of allEntriesInTemplate) {
const isTemplated = !nonTemplatedEntries.has(location);
const outputPath = resolvePath(
outputDir,
isTemplated
? templater.renderString(location, ctx.input.values)
: location,
isTemplated ? templater.renderString(location, parameters) : location,
);
if (location.endsWith('/')) {
@@ -184,7 +215,7 @@ export function createFetchTemplateAction(options: {
await fs.outputFile(
outputPath,
isTemplated
? templater.renderString(inputFileContents, ctx.input.values)
? templater.renderString(inputFileContents, parameters)
: inputFileContents,
);
}