From 17f0cd5233d6bac4fac29acd66f4445d4f0cba8d Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 11 Sep 2023 14:14:09 +0200 Subject: [PATCH 1/2] fix(@ngtools/webpack): account for styles specified as string literals and styleUrl An upcoming change in Angular will allow `style` specified as strings, in addition to a new `styleUrl` property. These changes update the Webpack transform to support the change. --- .../src/transformers/replace_resources.ts | 96 ++++++++++++------- .../transformers/replace_resources_spec.ts | 39 ++++++++ 2 files changed, 102 insertions(+), 33 deletions(-) diff --git a/packages/ngtools/webpack/src/transformers/replace_resources.ts b/packages/ngtools/webpack/src/transformers/replace_resources.ts index 9bebf13f64c6..2a6adf29c07e 100644 --- a/packages/ngtools/webpack/src/transformers/replace_resources.ts +++ b/packages/ngtools/webpack/src/transformers/replace_resources.ts @@ -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; + + 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; + } 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; - // Styles should be placed first if (isInlineStyle) { styleReplacements.unshift(...styles); @@ -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; } diff --git a/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts b/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts index c244419e6e4e..ee9b9c65e720 100644 --- a/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts +++ b/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts @@ -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'; From 734a46e99ff94d5fffd93f223e4b2b7646790f78 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 11 Sep 2023 14:14:37 +0200 Subject: [PATCH 2/2] fix(@angular-devkit/build-angular): account for styles specified as string literals and styleUrl An upcoming change in Angular will allow `style` specified as strings, in addition to a new `styleUrl` property. These changes update the JIT resource transform to support the change. --- .../angular/jit-resource-transformer.ts | 113 ++++++++++-------- 1 file changed, 64 insertions(+), 49 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/angular/jit-resource-transformer.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/angular/jit-resource-transformer.ts index a065a1ac432e..41b012b9e6f0 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/angular/jit-resource-transformer.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/angular/jit-resource-transformer.ts @@ -143,75 +143,90 @@ function visitComponentMetadata( switch (node.name.text) { case 'templateUrl': // Only analyze string literals - if ( - !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; + + // 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; - // 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; // External styles are applied after any inline styles