From 0abae8b3b179641e555adcc58f9edfbf3716abd3 Mon Sep 17 00:00:00 2001 From: Maruthan G Date: Sat, 28 Mar 2026 02:17:52 +0530 Subject: [PATCH] fix(@ngtools/webpack): emit diagnostic for unsupported conditional templateUrl expressions Fixes #27611 --- .../src/transformers/replace_resources.ts | 15 ++++++- .../transformers/replace_resources_spec.ts | 42 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/ngtools/webpack/src/transformers/replace_resources.ts b/packages/ngtools/webpack/src/transformers/replace_resources.ts index 9197ae3afd59..5283d1634130 100644 --- a/packages/ngtools/webpack/src/transformers/replace_resources.ts +++ b/packages/ngtools/webpack/src/transformers/replace_resources.ts @@ -41,6 +41,7 @@ export function replaceResources( resourceImportDeclarations, moduleKind, inlineStyleFileExtension, + node, ), ), ...(ts.getModifiers(node) ?? []), @@ -87,6 +88,7 @@ function visitDecorator( resourceImportDeclarations: ts.ImportDeclaration[], moduleKind?: ts.ModuleKind, inlineStyleFileExtension?: string, + classDeclaration?: ts.ClassDeclaration, ): ts.Decorator { if (!isComponentDecorator(node, typeChecker)) { return node; @@ -106,6 +108,8 @@ function visitDecorator( const objectExpression = args[0]; const styleReplacements: ts.Expression[] = []; + const className = classDeclaration?.name?.text ?? 'Unknown'; + // visit all properties let properties = ts.visitNodes(objectExpression.properties, (node) => ts.isObjectLiteralElementLike(node) @@ -116,6 +120,7 @@ function visitDecorator( resourceImportDeclarations, moduleKind, inlineStyleFileExtension, + className, ) : node, ) as ts.NodeArray; @@ -148,6 +153,7 @@ function visitComponentMetadata( resourceImportDeclarations: ts.ImportDeclaration[], moduleKind: ts.ModuleKind = ts.ModuleKind.ES2015, inlineStyleFileExtension?: string, + className?: string, ): ts.ObjectLiteralElementLike | undefined { if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name)) { return node; @@ -161,7 +167,14 @@ function visitComponentMetadata( case 'templateUrl': { const url = getResourceUrl(node.initializer); if (!url) { - return node; + const sourceFile = node.getSourceFile(); + const { line } = sourceFile.getLineAndCharacterOfPosition(node.initializer.getStart()); + + throw new Error( + `Component '${className}' in '${sourceFile.fileName}' contains a non-string literal ` + + `'templateUrl' value at line ${line + 1}. The 'templateUrl' property must be a ` + + `string literal. Expressions, variables, or other dynamic values are not supported.`, + ); } const importName = createResourceImport( diff --git a/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts b/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts index e0be8e4ebb03..5a140d04b183 100644 --- a/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts +++ b/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts @@ -469,4 +469,46 @@ describe('find_resources', () => { const result = transform(input, false); expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); }); + + it('should throw an error when templateUrl is a conditional expression', () => { + const input = tags.stripIndent` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + templateUrl: true === true + ? './app.component.html' + : './app.component.copy.html', + styleUrls: ['./app.component.css', './app.component.2.css'] + }) + export class AppComponent { + title = 'app'; + } + `; + + expect(() => transform(input)).toThrowError( + /Component 'AppComponent'.*contains a non-string literal 'templateUrl' value/, + ); + }); + + it('should throw an error when templateUrl is a variable reference', () => { + const input = tags.stripIndent` + import { Component } from '@angular/core'; + + const myTemplate = './app.component.html'; + + @Component({ + selector: 'app-root', + templateUrl: myTemplate, + styleUrls: ['./app.component.css'] + }) + export class AppComponent { + title = 'app'; + } + `; + + expect(() => transform(input)).toThrowError( + /Component 'AppComponent'.*contains a non-string literal 'templateUrl' value/, + ); + }); });