fix: Add filename parameter for EJS templates

This is required if we want to include partial templates
This commit is contained in:
Joachim Van Herwegen
2021-09-08 16:25:46 +02:00
parent cfa70011f6
commit 42d3ab0a4c
4 changed files with 89 additions and 36 deletions

View File

@@ -3,7 +3,7 @@
import type { TemplateFunction } from 'ejs';
import { compile, render } from 'ejs';
import type { TemplateEngine, Template } from './TemplateEngine';
import { readTemplate } from './TemplateEngine';
import { getTemplateFilePath, readTemplate } from './TemplateEngine';
import Dict = NodeJS.Dict;
/**
@@ -16,13 +16,16 @@ export class EjsTemplateEngine<T extends Dict<any> = Dict<any>> implements Templ
* @param template - The default template @range {json}
*/
public constructor(template?: Template) {
// EJS requires the `filename` parameter to be able to include partial templates
const filename = getTemplateFilePath(template);
this.applyTemplate = readTemplate(template)
.then((templateString: string): TemplateFunction => compile(templateString));
.then((templateString: string): TemplateFunction => compile(templateString, { filename }));
}
public async render(contents: T): Promise<string>;
public async render<TCustom = T>(contents: TCustom, template: Template): Promise<string>;
public async render<TCustom = T>(contents: TCustom, template?: Template): Promise<string> {
return template ? render(await readTemplate(template), contents) : (await this.applyTemplate)(contents);
const options = { ...contents, filename: getTemplateFilePath(template) };
return template ? render(await readTemplate(template), options) : (await this.applyTemplate)(options);
}
}

View File

@@ -36,20 +36,32 @@ export interface TemplateEngine<T extends Dict<any> = Dict<any>> {
}
/* eslint-enable @typescript-eslint/method-signature-style */
/**
* Returns the absolute path to the template.
* Returns undefined if the input does not contain a file path.
*/
export function getTemplateFilePath(template?: Template): string | undefined {
// The template has been passed as a filename
if (typeof template === 'string') {
return getTemplateFilePath({ templateFile: template });
}
// The template has already been given as a string so no known path
if (!template || 'templateString' in template) {
return;
}
const { templateFile, templatePath } = template;
const fullTemplatePath = templatePath ? joinFilePath(templatePath, templateFile) : templateFile;
return resolveAssetPath(fullTemplatePath);
}
/**
* Reads the template and returns it as a string.
*/
export async function readTemplate(template: Template = { templateString: '' }): Promise<string> {
// The template has been passed as a filename
if (typeof template === 'string') {
return readTemplate({ templateFile: template });
}
// The template has already been given as a string
if ('templateString' in template) {
if (typeof template === 'object' && 'templateString' in template) {
return template.templateString;
}
// The template needs to be read from disk
const { templateFile, templatePath } = template;
const fullTemplatePath = templatePath ? joinFilePath(templatePath, templateFile) : templateFile;
return fsPromises.readFile(resolveAssetPath(fullTemplatePath), 'utf8');
return fsPromises.readFile(getTemplateFilePath(template)!, 'utf8');
}

View File

@@ -1,6 +1,7 @@
import { EjsTemplateEngine } from '../../../../src/util/templates/EjsTemplateEngine';
jest.mock('../../../../src/util/templates/TemplateEngine', (): any => ({
getTemplateFilePath: jest.fn((): string => `filename`),
readTemplate: jest.fn(async({ templateString }): Promise<string> => `${templateString}: <%= detail %>`),
}));

View File

@@ -1,40 +1,77 @@
import { resolveAssetPath } from '../../../../src/util/PathUtil';
import { readTemplate } from '../../../../src/util/templates/TemplateEngine';
import { getTemplateFilePath, readTemplate } from '../../../../src/util/templates/TemplateEngine';
import { mockFs } from '../../../util/Util';
jest.mock('fs');
describe('readTemplate', (): void => {
const templateFile = 'template.xyz';
const templatePath = 'other';
describe('TemplateEngine', (): void => {
describe('#getTemplateFilePath', (): void => {
const templateFile = 'template.xyz';
const templatePath = 'other';
beforeEach(async(): Promise<void> => {
const { data } = mockFs(resolveAssetPath(''));
Object.assign(data, {
'template.xyz': '{{template}}',
other: {
'template.xyz': '{{other}}',
},
beforeEach(async(): Promise<void> => {
const { data } = mockFs(resolveAssetPath(''));
Object.assign(data, {
'template.xyz': '{{template}}',
other: {
'template.xyz': '{{other}}',
},
});
});
it('returns the undefined when no template is provided.', async(): Promise<void> => {
expect(getTemplateFilePath()).toBeUndefined();
});
it('returns the input if it was a filename.', async(): Promise<void> => {
expect(getTemplateFilePath(templateFile)).toBe(resolveAssetPath(templateFile));
});
it('returns undefined for options with a string template.', async(): Promise<void> => {
expect(getTemplateFilePath({ templateString: 'abc' })).toBeUndefined();
});
it('accepts options with a filename.', async(): Promise<void> => {
expect(getTemplateFilePath({ templateFile })).toBe(resolveAssetPath(templateFile));
});
it('accepts options with a filename and a path.', async(): Promise<void> => {
expect(getTemplateFilePath({ templateFile, templatePath })).toBe(resolveAssetPath('other/template.xyz'));
});
});
it('returns the empty string when no template is provided.', async(): Promise<void> => {
await expect(readTemplate()).resolves.toBe('');
});
describe('#readTemplate', (): void => {
const templateFile = 'template.xyz';
const templatePath = 'other';
it('accepts a filename.', async(): Promise<void> => {
await expect(readTemplate(templateFile)).resolves.toBe('{{template}}');
});
beforeEach(async(): Promise<void> => {
const { data } = mockFs(resolveAssetPath(''));
Object.assign(data, {
'template.xyz': '{{template}}',
other: {
'template.xyz': '{{other}}',
},
});
});
it('accepts options with a string template.', async(): Promise<void> => {
await expect(readTemplate({ templateString: 'abc' })).resolves.toBe('abc');
});
it('returns the empty string when no template is provided.', async(): Promise<void> => {
await expect(readTemplate()).resolves.toBe('');
});
it('accepts options with a filename.', async(): Promise<void> => {
await expect(readTemplate({ templateFile })).resolves.toBe('{{template}}');
});
it('accepts a filename.', async(): Promise<void> => {
await expect(readTemplate(templateFile)).resolves.toBe('{{template}}');
});
it('accepts options with a filename and a path.', async(): Promise<void> => {
await expect(readTemplate({ templateFile, templatePath })).resolves.toBe('{{other}}');
it('accepts options with a string template.', async(): Promise<void> => {
await expect(readTemplate({ templateString: 'abc' })).resolves.toBe('abc');
});
it('accepts options with a filename.', async(): Promise<void> => {
await expect(readTemplate({ templateFile })).resolves.toBe('{{template}}');
});
it('accepts options with a filename and a path.', async(): Promise<void> => {
await expect(readTemplate({ templateFile, templatePath })).resolves.toBe('{{other}}');
});
});
});