From 92f757d291ae7e89097c7f202ef96d641807b016 Mon Sep 17 00:00:00 2001 From: Yashvardhan Thanvi Date: Sat, 31 Oct 2026 09:53:17 +0530 Subject: [PATCH 1/2] fix(@angular-devkit/schematics): do not ignore TypeErrors thrown by content templates Since 18.1.0 (41ee8eac1), `applyContentTemplate` returns the entry unchanged for any `TypeError`, so that content which is not valid UTF-8 is left alone. Decoding and applying the template shared one `try`, so a `TypeError` thrown by the template itself, such as `<%= name.length %>` with `name` undefined, also left the file unrendered without an error. Decode in a separate `try` so that only decode failures are ignored. --- .../schematics/src/rules/template.ts | 14 ++++++++------ .../schematics/src/rules/template_spec.ts | 12 ++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/angular_devkit/schematics/src/rules/template.ts b/packages/angular_devkit/schematics/src/rules/template.ts index c01249cc235d..4cd14c9883d2 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,12 @@ export function applyContentTemplate(options: T): FileOperator { throw e; } + decodedContent = decodedContent.replace(/\r?\n/g, EOL); + + return { + path, + content: Buffer.from(templateImpl(decodedContent, {})(options)), + }; }; } diff --git a/packages/angular_devkit/schematics/src/rules/template_spec.ts b/packages/angular_devkit/schematics/src/rules/template_spec.ts index 1833d8e7f37c..72040c31eda7 100644 --- a/packages/angular_devkit/schematics/src/rules/template_spec.ts +++ b/packages/angular_devkit/schematics/src/rules/template_spec.ts @@ -171,6 +171,18 @@ 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); + }); }); describe('applyTemplateFiles', () => { From 2bc187e36f09394ab6bc415407a4f6954656cd5e Mon Sep 17 00:00:00 2001 From: Yashvardhan Thanvi Date: Sat, 31 Oct 2026 09:54:00 +0530 Subject: [PATCH 2/2] fix(@angular-devkit/schematics): include the template path in errors thrown by content templates Rethrow errors thrown while applying a content template, such as a `ReferenceError` from a misspelled variable or a `SyntaxError` inside `<% %>`, as `Failed to apply template "": ` and keep the original error as `cause`. The message did not say which template failed, and the ng CLI printed only `foobat is not defined`. Closes #13192 --- .../schematics/src/rules/template.ts | 14 +++++--- .../schematics/src/rules/template_spec.ts | 34 ++++++++++++++++++- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/angular_devkit/schematics/src/rules/template.ts b/packages/angular_devkit/schematics/src/rules/template.ts index 4cd14c9883d2..1c8417a336b2 100644 --- a/packages/angular_devkit/schematics/src/rules/template.ts +++ b/packages/angular_devkit/schematics/src/rules/template.ts @@ -71,10 +71,16 @@ export function applyContentTemplate(options: T): FileOperator { } decodedContent = decodedContent.replace(/\r?\n/g, EOL); - return { - path, - content: Buffer.from(templateImpl(decodedContent, {})(options)), - }; + 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 72040c31eda7..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'; @@ -183,6 +183,23 @@ describe('contentTemplate', () => { 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', () => { @@ -212,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 /, + ); + }); });