Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion packages/ngtools/webpack/src/transformers/replace_resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export function replaceResources(
resourceImportDeclarations,
moduleKind,
inlineStyleFileExtension,
node,
),
),
...(ts.getModifiers(node) ?? []),
Expand Down Expand Up @@ -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;
Expand All @@ -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)
Expand All @@ -116,6 +120,7 @@ function visitDecorator(
resourceImportDeclarations,
moduleKind,
inlineStyleFileExtension,
className,
)
: node,
) as ts.NodeArray<ts.ObjectLiteralElementLike>;
Expand Down Expand Up @@ -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;
Expand All @@ -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(
Expand Down Expand Up @@ -198,6 +211,22 @@ function visitComponentMetadata(
) as ts.StringLiteralLike,
];
} else if (ts.isArrayLiteralExpression(node.initializer)) {
if (!isInlineStyle) {
// Validate each element is a string literal for styleUrls
for (const element of node.initializer.elements) {
if (!ts.isStringLiteralLike(element)) {
const sourceFile = node.getSourceFile();
const { line } = sourceFile.getLineAndCharacterOfPosition(element.getStart());

throw new Error(
`Component '${className}' in '${sourceFile.fileName}' contains a non-string` +
` literal '${name}' value at line ${line + 1}. The '${name}' property must` +
` contain string literals. Expressions, variables, or other dynamic values` +
` are not supported.`,
);
}
}
}
styles = ts.visitNodes(node.initializer.elements, (node) =>
transformInlineStyleLiteral(
node,
Expand All @@ -208,6 +237,16 @@ function visitComponentMetadata(
moduleKind,
),
) as ts.NodeArray<ts.Expression>;
} else if (!isInlineStyle) {
// styleUrl or styleUrls with a non-string, non-array initializer
const sourceFile = node.getSourceFile();
const { line } = sourceFile.getLineAndCharacterOfPosition(node.initializer.getStart());

throw new Error(
`Component '${className}' in '${sourceFile.fileName}' contains a non-string literal` +
` '${name}' value at line ${line + 1}. The '${name}' property must be a` +
` string literal. Expressions, variables, or other dynamic values are not supported.`,
);
} else {
return node;
}
Expand Down
122 changes: 122 additions & 0 deletions packages/ngtools/webpack/src/transformers/replace_resources_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,4 +469,126 @@ 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/,
);
});

it('should throw an error when styleUrls contains a conditional expression', () => {
const input = tags.stripIndent`
import { Component } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: [true === true ? './app.component.css' : './app.component.dark.css']
})
export class AppComponent {
title = 'app';
}
`;

expect(() => transform(input)).toThrowError(
/Component 'AppComponent'.*contains a non-string literal 'styleUrls' value/,
);
});

it('should throw an error when styleUrls contains a variable reference', () => {
const input = tags.stripIndent`
import { Component } from '@angular/core';

const myStyle = './app.component.css';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: [myStyle]
})
export class AppComponent {
title = 'app';
}
`;

expect(() => transform(input)).toThrowError(
/Component 'AppComponent'.*contains a non-string literal 'styleUrls' value/,
);
});

it('should throw an error when styleUrl is a conditional expression', () => {
const input = tags.stripIndent`
import { Component } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrl: true === true ? './app.component.css' : './app.component.dark.css'
})
export class AppComponent {
title = 'app';
}
`;

expect(() => transform(input)).toThrowError(
/Component 'AppComponent'.*contains a non-string literal 'styleUrl' value/,
);
});

it('should throw an error when styleUrl is a variable reference', () => {
const input = tags.stripIndent`
import { Component } from '@angular/core';

const myStyle = './app.component.css';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrl: myStyle
})
export class AppComponent {
title = 'app';
}
`;

expect(() => transform(input)).toThrowError(
/Component 'AppComponent'.*contains a non-string literal 'styleUrl' value/,
);
});
});
Loading