From 42d3ab0a4c678a03aedd9f3766398988b8821237 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Wed, 8 Sep 2021 16:25:46 +0200 Subject: [PATCH] fix: Add filename parameter for EJS templates This is required if we want to include partial templates --- src/util/templates/EjsTemplateEngine.ts | 9 +- src/util/templates/TemplateEngine.ts | 28 ++++-- .../util/templates/EjsTemplateEngine.test.ts | 1 + .../util/templates/TemplateEngine.test.ts | 87 +++++++++++++------ 4 files changed, 89 insertions(+), 36 deletions(-) diff --git a/src/util/templates/EjsTemplateEngine.ts b/src/util/templates/EjsTemplateEngine.ts index c1183dbfb..47ecea29e 100644 --- a/src/util/templates/EjsTemplateEngine.ts +++ b/src/util/templates/EjsTemplateEngine.ts @@ -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 = Dict> 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; public async render(contents: TCustom, template: Template): Promise; public async render(contents: TCustom, template?: Template): Promise { - 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); } } diff --git a/src/util/templates/TemplateEngine.ts b/src/util/templates/TemplateEngine.ts index fd166a6fe..095a3c631 100644 --- a/src/util/templates/TemplateEngine.ts +++ b/src/util/templates/TemplateEngine.ts @@ -36,20 +36,32 @@ export interface TemplateEngine = Dict> { } /* 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 { - // 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'); } diff --git a/test/unit/util/templates/EjsTemplateEngine.test.ts b/test/unit/util/templates/EjsTemplateEngine.test.ts index 216d34f11..a303122fc 100644 --- a/test/unit/util/templates/EjsTemplateEngine.test.ts +++ b/test/unit/util/templates/EjsTemplateEngine.test.ts @@ -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 => `${templateString}: <%= detail %>`), })); diff --git a/test/unit/util/templates/TemplateEngine.test.ts b/test/unit/util/templates/TemplateEngine.test.ts index 94eb5d881..897538f87 100644 --- a/test/unit/util/templates/TemplateEngine.test.ts +++ b/test/unit/util/templates/TemplateEngine.test.ts @@ -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 => { - const { data } = mockFs(resolveAssetPath('')); - Object.assign(data, { - 'template.xyz': '{{template}}', - other: { - 'template.xyz': '{{other}}', - }, + beforeEach(async(): Promise => { + 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 => { + expect(getTemplateFilePath()).toBeUndefined(); + }); + + it('returns the input if it was a filename.', async(): Promise => { + expect(getTemplateFilePath(templateFile)).toBe(resolveAssetPath(templateFile)); + }); + + it('returns undefined for options with a string template.', async(): Promise => { + expect(getTemplateFilePath({ templateString: 'abc' })).toBeUndefined(); + }); + + it('accepts options with a filename.', async(): Promise => { + expect(getTemplateFilePath({ templateFile })).toBe(resolveAssetPath(templateFile)); + }); + + it('accepts options with a filename and a path.', async(): Promise => { + expect(getTemplateFilePath({ templateFile, templatePath })).toBe(resolveAssetPath('other/template.xyz')); }); }); - it('returns the empty string when no template is provided.', async(): Promise => { - await expect(readTemplate()).resolves.toBe(''); - }); + describe('#readTemplate', (): void => { + const templateFile = 'template.xyz'; + const templatePath = 'other'; - it('accepts a filename.', async(): Promise => { - await expect(readTemplate(templateFile)).resolves.toBe('{{template}}'); - }); + beforeEach(async(): Promise => { + const { data } = mockFs(resolveAssetPath('')); + Object.assign(data, { + 'template.xyz': '{{template}}', + other: { + 'template.xyz': '{{other}}', + }, + }); + }); - it('accepts options with a string template.', async(): Promise => { - await expect(readTemplate({ templateString: 'abc' })).resolves.toBe('abc'); - }); + it('returns the empty string when no template is provided.', async(): Promise => { + await expect(readTemplate()).resolves.toBe(''); + }); - it('accepts options with a filename.', async(): Promise => { - await expect(readTemplate({ templateFile })).resolves.toBe('{{template}}'); - }); + it('accepts a filename.', async(): Promise => { + await expect(readTemplate(templateFile)).resolves.toBe('{{template}}'); + }); - it('accepts options with a filename and a path.', async(): Promise => { - await expect(readTemplate({ templateFile, templatePath })).resolves.toBe('{{other}}'); + it('accepts options with a string template.', async(): Promise => { + await expect(readTemplate({ templateString: 'abc' })).resolves.toBe('abc'); + }); + + it('accepts options with a filename.', async(): Promise => { + await expect(readTemplate({ templateFile })).resolves.toBe('{{template}}'); + }); + + it('accepts options with a filename and a path.', async(): Promise => { + await expect(readTemplate({ templateFile, templatePath })).resolves.toBe('{{other}}'); + }); }); });