diff --git a/packages/angular_devkit/schematics/src/rules/template.ts b/packages/angular_devkit/schematics/src/rules/template.ts index c01249cc235d..1c8417a336b2 100644 --- a/packages/angular_devkit/schematics/src/rules/template.ts +++ b/packages/angular_devkit/schematics/src/rules/template.ts @@ -54,13 +54,9 @@ export function applyContentTemplate(options: T): FileOperator { return (entry: FileEntry) => { const { path, content } = entry; + let decodedContent: string; try { - const decodedContent = decoder.decode(content).replace(/\r?\n/g, EOL); - - return { - path, - content: Buffer.from(templateImpl(decodedContent, {})(options)), - }; + decodedContent = decoder.decode(content); } catch (e) { // The second part should not be needed. But Jest does not support instanceof correctly. // See: https://github.com/jestjs/jest/issues/2549 @@ -73,6 +69,18 @@ export function applyContentTemplate(options: T): FileOperator { throw e; } + decodedContent = decodedContent.replace(/\r?\n/g, EOL); + + try { + return { + path, + content: Buffer.from(templateImpl(decodedContent, {})(options)), + }; + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + + throw new Error(`Failed to apply content template to "${path}": ${message}`, { cause: e }); + } }; } diff --git a/packages/angular_devkit/schematics/src/rules/template_spec.ts b/packages/angular_devkit/schematics/src/rules/template_spec.ts index 1833d8e7f37c..e46d523ea032 100644 --- a/packages/angular_devkit/schematics/src/rules/template_spec.ts +++ b/packages/angular_devkit/schematics/src/rules/template_spec.ts @@ -8,7 +8,7 @@ /* eslint-disable import/no-extraneous-dependencies */ import { normalize } from '@angular-devkit/core'; -import { of as observableOf } from 'rxjs'; +import { lastValueFrom, of as observableOf } from 'rxjs'; import { UnitTestTree } from '../../testing'; import { SchematicContext } from '../engine/interface'; import { HostTree } from '../tree/host-tree'; @@ -171,6 +171,35 @@ describe('contentTemplate', () => { expect(_applyContentTemplate('a<%= \'a\' + "b" %>b', {})).toBe('aabb'); expect(_applyContentTemplate('a<%= "\\n" + "b" %>b', {})).toBe('a\nbb'); }); + + it('does not ignore type errors thrown by a template', () => { + expect(() => _applyContentTemplate('a<%= value.length %>b', { value: undefined })).toThrowError( + /Cannot read properties of undefined/, + ); + }); + + it('leaves content that is not valid UTF-8 unchanged', () => { + const entry = { path: normalize('/a/b/image.png'), content: Buffer.from([0xff, 0xfe, 0xfd]) }; + + expect(applyContentTemplate({})(entry)).toBe(entry); + }); + + it('names the template in errors thrown while applying it', () => { + const operator = applyContentTemplate({ foobar: 1 }); + + expect(() => operator(_entry('/a/b/file.ts', 'a<%= foobat %>b'))).toThrowError( + 'Failed to apply content template to "/a/b/file.ts": foobat is not defined', + ); + expect(() => operator(_entry('/a/b/file.ts', 'a<%= foobar unintended %>b'))).toThrowError( + /^Failed to apply content template to "\/a\/b\/file\.ts": Unexpected identifier/, + ); + }); + + it('keeps the original template error as the cause', () => { + expect(() => + applyContentTemplate({})(_entry('/a/b/file.ts', 'a<%= foobat %>b')), + ).toThrowMatching((e) => e.cause instanceof ReferenceError); + }); }); describe('applyTemplateFiles', () => { @@ -200,4 +229,19 @@ describe('applyTemplateFiles', () => { ]); expect(tree.readContent('/a/b/file3')).toBe('hello 1 world'); }); + + it('names the template file in errors', async () => { + const tree = new UnitTestTree(new HostTree()); + tree.create('a/b/file__a__.template', 'hello <%= a.missing.length %> world'); + + const context: SchematicContext = { + strategy: MergeStrategy.Default, + } as SchematicContext; + + await expectAsync( + lastValueFrom(callRule(applyTemplates({ a: 'foo' }), observableOf(tree), context)), + ).toBeRejectedWithError( + /^Failed to apply content template to "\/a\/b\/file__a__\.template": Cannot read /, + ); + }); });