Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -143,75 +143,90 @@ function visitComponentMetadata(
switch (node.name.text) {
case 'templateUrl':
// Only analyze string literals
if (
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I couldn't figure out where this logic is tested so I didn't add any tests. I can add some if somebody can point me in the right direction.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm okay, it looks like I can't write a test there, because the change hasn't landed in a release yet so writing a component with the new properties results in a compilation error.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually even if this compiled, it wouldn't be testing the behavior we're looking for, because the transformer only runs on JIT code. As far as I can tell, these tests are running AOT compilation.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually even if this compiled, it wouldn't be testing the behavior we're looking for, because the transformer only runs on JIT code. As far as I can tell, these tests are running AOT compilation.

You can switch between AOT and JIT by providing the aot as an option in

Let's land it for now, and follow up with a unit test in a separate PR once the changes in the FW are released on NPM.

!ts.isStringLiteral(node.initializer) &&
!ts.isNoSubstitutionTemplateLiteral(node.initializer)
) {
if (!ts.isStringLiteralLike(node.initializer)) {
return node;
}

const url = node.initializer.text;
if (!url) {
return node;
}

return nodeFactory.updatePropertyAssignment(
node,
nodeFactory.createIdentifier('template'),
createResourceImport(
nodeFactory,
generateJitFileUri(url, 'template'),
resourceImportDeclarations,
),
);
return node.initializer.text.length === 0
? node
: nodeFactory.updatePropertyAssignment(
node,
nodeFactory.createIdentifier('template'),
createResourceImport(
nodeFactory,
generateJitFileUri(node.initializer.text, 'template'),
resourceImportDeclarations,
),
);
case 'styles':
if (!ts.isArrayLiteralExpression(node.initializer)) {
return node;
if (ts.isStringLiteralLike(node.initializer)) {
styleReplacements.unshift(
createResourceImport(
nodeFactory,
generateJitInlineUri(node.initializer.text, 'style'),
resourceImportDeclarations,
),
);

return undefined;
}

const inlineStyles = ts.visitNodes(node.initializer.elements, (node) => {
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
return node;
}
if (ts.isArrayLiteralExpression(node.initializer)) {
const inlineStyles = ts.visitNodes(node.initializer.elements, (node) => {
if (!ts.isStringLiteralLike(node)) {
return node;
}

return node.text.length === 0
? undefined // An empty inline style is equivalent to not having a style element
: createResourceImport(
nodeFactory,
generateJitInlineUri(node.text, 'style'),
resourceImportDeclarations,
);
}) as ts.NodeArray<ts.Expression>;

// Inline styles should be placed first
styleReplacements.unshift(...inlineStyles);

// The inline styles will be added afterwards in combination with any external styles
return undefined;
}

const contents = node.text;
if (!contents) {
// An empty inline style is equivalent to not having a style element
return undefined;
}
return node;

return createResourceImport(
nodeFactory,
generateJitInlineUri(contents, 'style'),
resourceImportDeclarations,
case 'styleUrl':
if (ts.isStringLiteralLike(node.initializer)) {
styleReplacements.push(
createResourceImport(
nodeFactory,
generateJitFileUri(node.initializer.text, 'style'),
resourceImportDeclarations,
),
);
}) as ts.NodeArray<ts.Expression>;

// Inline styles should be placed first
styleReplacements.unshift(...inlineStyles);
return undefined;
}

return node;

// The inline styles will be added afterwards in combination with any external styles
return undefined;
case 'styleUrls':
if (!ts.isArrayLiteralExpression(node.initializer)) {
return node;
}

const externalStyles = ts.visitNodes(node.initializer.elements, (node) => {
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
return node;
}

const url = node.text;
if (!url) {
if (!ts.isStringLiteralLike(node)) {
return node;
}

return createResourceImport(
nodeFactory,
generateJitFileUri(url, 'style'),
resourceImportDeclarations,
);
return node.text
? createResourceImport(
nodeFactory,
generateJitFileUri(node.text, 'style'),
resourceImportDeclarations,
)
: undefined;
}) as ts.NodeArray<ts.Expression>;

// External styles are applied after any inline styles
Expand Down
96 changes: 63 additions & 33 deletions packages/ngtools/webpack/src/transformers/replace_resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,42 +180,37 @@ function visitComponentMetadata(
importName,
);
case 'styles':
case 'styleUrl':
case 'styleUrls':
if (!ts.isArrayLiteralExpression(node.initializer)) {
const isInlineStyle = name === 'styles';
let styles: Iterable<ts.Expression>;

if (ts.isStringLiteralLike(node.initializer)) {
styles = [
transformInlineStyleLiteral(
node.initializer,
nodeFactory,
isInlineStyle,
inlineStyleFileExtension,
resourceImportDeclarations,
moduleKind,
) as ts.StringLiteralLike,
];
} else if (ts.isArrayLiteralExpression(node.initializer)) {
styles = ts.visitNodes(node.initializer.elements, (node) =>
transformInlineStyleLiteral(
node,
nodeFactory,
isInlineStyle,
inlineStyleFileExtension,
resourceImportDeclarations,
moduleKind,
),
) as ts.NodeArray<ts.Expression>;
} else {
return node;
}

const isInlineStyle = name === 'styles';
const styles = ts.visitNodes(node.initializer.elements, (node) => {
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
return node;
}

let url;
if (isInlineStyle) {
if (inlineStyleFileExtension) {
const data = Buffer.from(node.text).toString('base64');
const containingFile = node.getSourceFile().fileName;
// app.component.ts.css?ngResource!=!@ngtools/webpack/src/loaders/inline-resource.js?data=...!app.component.ts
url =
`${containingFile}.${inlineStyleFileExtension}?${NG_COMPONENT_RESOURCE_QUERY}` +
`!=!${InlineAngularResourceLoaderPath}?data=${encodeURIComponent(
data,
)}!${containingFile}`;
} else {
return nodeFactory.createStringLiteral(node.text);
}
} else {
url = getResourceUrl(node);
}

if (!url) {
return node;
}

return createResourceImport(nodeFactory, url, resourceImportDeclarations, moduleKind);
}) as ts.NodeArray<ts.Expression>;

// Styles should be placed first
if (isInlineStyle) {
styleReplacements.unshift(...styles);
Expand All @@ -229,9 +224,44 @@ function visitComponentMetadata(
}
}

function transformInlineStyleLiteral(
node: ts.Node,
nodeFactory: ts.NodeFactory,
isInlineStyle: boolean,
inlineStyleFileExtension: string | undefined,
resourceImportDeclarations: ts.ImportDeclaration[],
moduleKind: ts.ModuleKind,
) {
if (!ts.isStringLiteralLike(node)) {
return node;
}

if (!isInlineStyle) {
const url = getResourceUrl(node);

return url
? createResourceImport(nodeFactory, url, resourceImportDeclarations, moduleKind)
: node;
}

if (!inlineStyleFileExtension) {
return nodeFactory.createStringLiteral(node.text);
}

const data = Buffer.from(node.text).toString('base64');
const containingFile = node.getSourceFile().fileName;

// app.component.ts.css?ngResource!=!@ngtools/webpack/src/loaders/inline-resource.js?data=...!app.component.ts
const url =
`${containingFile}.${inlineStyleFileExtension}?${NG_COMPONENT_RESOURCE_QUERY}` +
`!=!${InlineAngularResourceLoaderPath}?data=${encodeURIComponent(data)}!${containingFile}`;

return createResourceImport(nodeFactory, url, resourceImportDeclarations, moduleKind);
}

export function getResourceUrl(node: ts.Node): string | null {
// only analyze strings
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
if (!ts.isStringLiteralLike(node)) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,45 @@ describe('@ngtools/webpack transformers', () => {
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
});

it('should replace resources specified as string literals', () => {
const input = tags.stripIndent`
import { Component } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styles: 'h2 {font-size: 10px}',
styleUrl: './app.component.css'
})
export class AppComponent {
title = 'app';
}
`;
const output = tags.stripIndent`
import { __decorate } from "tslib";
import __NG_CLI_RESOURCE__0 from "./app.component.html?ngResource";
import __NG_CLI_RESOURCE__1 from "./app.component.css?ngResource";
import { Component } from '@angular/core';

let AppComponent = class AppComponent {
constructor() {
this.title = 'app';
}
};
AppComponent = __decorate([
Component({
selector: 'app-root',
template: __NG_CLI_RESOURCE__0,
styles: ["h2 {font-size: 10px}", __NG_CLI_RESOURCE__1]
})
], AppComponent);
export { AppComponent };
`;

const result = transform(input);
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
});

it('should not replace resources if not in Component decorator', () => {
const input = tags.stripIndent`
import { Component } from '@angular/core';
Expand Down