From c06ea4704e3c7d8e881601a408e4e915f1baa0ee Mon Sep 17 00:00:00 2001 From: Mathias Raacke Date: Sun, 17 Feb 2019 18:34:54 +0100 Subject: [PATCH] feat(@ngtools/webpack): allow .svg files as templates With directTemplateLoading enabled, components can now use .svg files as templates. For AOT builds, the Angular compiler host now reads .svg files directly when reading component templates. For JIT builds, replaceResources creates a require call that directly uses raw-loader instead of using the loader provided by the current webpack configuration. Closes #10567 --- .../models/webpack-configs/common.ts | 1 - .../test/browser/svg_spec_large.ts | 59 +++++++++++++ .../webpack/src/angular_compiler_plugin.ts | 3 +- packages/ngtools/webpack/src/compiler_host.ts | 3 +- .../src/transformers/replace_resources.ts | 22 +++-- .../transformers/replace_resources_spec.ts | 88 +++++++++++++++++-- 6 files changed, 156 insertions(+), 20 deletions(-) create mode 100644 packages/angular_devkit/build_angular/test/browser/svg_spec_large.ts diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts index 59d5c3152f60..d1bb13e18604 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts @@ -307,7 +307,6 @@ export function getCommonConfig(wco: WebpackConfigOptions) { }, module: { rules: [ - { test: /\.html$/, loader: 'raw-loader' }, { test: /\.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)$/, loader: 'file-loader', diff --git a/packages/angular_devkit/build_angular/test/browser/svg_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/svg_spec_large.ts new file mode 100644 index 000000000000..44756101654d --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/svg_spec_large.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { runTargetSpec } from '@angular-devkit/architect/testing'; +import { join, normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { browserTargetSpec, host, outputPath } from '../utils'; + +describe('Browser Builder allow svg', () => { + + beforeEach(done => host.initialize().toPromise().then(done, done.fail)); + afterEach(done => host.restore().toPromise().then(done, done.fail)); + + it('works with aot', + (done) => { + + const svg = ` + + Hello World + `; + + host.writeMultipleFiles({ + './src/app/app.component.svg': svg, + './src/app/app.component.ts': ` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + templateUrl: './app.component.svg', + styleUrls: [] + }) + export class AppComponent { + title = 'app'; + } + `, + }); + + const overrides = { aot: true }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const content = virtualFs.fileBufferToString( + host.scopedSync().read(join(outputPath, 'main.js')), + ); + + expect(content).toContain('":svg:svg"'); + expect(host.scopedSync().exists(normalize('dist/app.component.svg'))) + .toBe(false, 'should not copy app.component.svg to dist'); + }), + ).toPromise().then(done, done.fail); + }); + +}); diff --git a/packages/ngtools/webpack/src/angular_compiler_plugin.ts b/packages/ngtools/webpack/src/angular_compiler_plugin.ts index 06a751ed635f..e42eccdd6c83 100644 --- a/packages/ngtools/webpack/src/angular_compiler_plugin.ts +++ b/packages/ngtools/webpack/src/angular_compiler_plugin.ts @@ -863,7 +863,8 @@ export class AngularCompilerPlugin { if (this._JitMode) { // Replace resources in JIT. - this._transformers.push(replaceResources(isAppPath, getTypeChecker)); + this._transformers.push( + replaceResources(isAppPath, getTypeChecker, this._options.directTemplateLoading)); } else { // Remove unneeded angular decorators. this._transformers.push(removeDecorators(isAppPath, getTypeChecker)); diff --git a/packages/ngtools/webpack/src/compiler_host.ts b/packages/ngtools/webpack/src/compiler_host.ts index 873d2fb93e4a..aac65c453ef0 100644 --- a/packages/ngtools/webpack/src/compiler_host.ts +++ b/packages/ngtools/webpack/src/compiler_host.ts @@ -336,7 +336,8 @@ export class WebpackCompilerHost implements ts.CompilerHost { } readResource(fileName: string) { - if (this.directTemplateLoading && fileName.endsWith('.html')) { + if (this.directTemplateLoading && + (fileName.endsWith('.html') || fileName.endsWith('.svg'))) { return this.readFile(fileName); } diff --git a/packages/ngtools/webpack/src/transformers/replace_resources.ts b/packages/ngtools/webpack/src/transformers/replace_resources.ts index 22428e15e262..077a0532d760 100644 --- a/packages/ngtools/webpack/src/transformers/replace_resources.ts +++ b/packages/ngtools/webpack/src/transformers/replace_resources.ts @@ -10,6 +10,7 @@ import * as ts from 'typescript'; export function replaceResources( shouldTransform: (fileName: string) => boolean, getTypeChecker: () => ts.TypeChecker, + directTemplateLoading = false, ): ts.TransformerFactory { return (context: ts.TransformationContext) => { @@ -19,7 +20,7 @@ export function replaceResources( if (ts.isClassDeclaration(node)) { node.decorators = ts.visitNodes( node.decorators, - (node: ts.Decorator) => visitDecorator(node, typeChecker), + (node: ts.Decorator) => visitDecorator(node, typeChecker, directTemplateLoading), ); } @@ -34,7 +35,10 @@ export function replaceResources( }; } -function visitDecorator(node: ts.Decorator, typeChecker: ts.TypeChecker): ts.Decorator { +function visitDecorator( + node: ts.Decorator, + typeChecker: ts.TypeChecker, + directTemplateLoading: boolean): ts.Decorator { if (!isComponentDecorator(node, typeChecker)) { return node; } @@ -56,7 +60,8 @@ function visitDecorator(node: ts.Decorator, typeChecker: ts.TypeChecker): ts.Dec // visit all properties let properties = ts.visitNodes( objectExpression.properties, - (node: ts.ObjectLiteralElementLike) => visitComponentMetadata(node, styleReplacements), + (node: ts.ObjectLiteralElementLike) => + visitComponentMetadata(node, styleReplacements, directTemplateLoading), ); // replace properties with updated properties @@ -83,6 +88,7 @@ function visitDecorator(node: ts.Decorator, typeChecker: ts.TypeChecker): ts.Dec function visitComponentMetadata( node: ts.ObjectLiteralElementLike, styleReplacements: ts.Expression[], + directTemplateLoading: boolean, ): ts.ObjectLiteralElementLike | undefined { if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name)) { return node; @@ -98,7 +104,7 @@ function visitComponentMetadata( return ts.updatePropertyAssignment( node, ts.createIdentifier('template'), - createRequireExpression(node.initializer), + createRequireExpression(node.initializer, directTemplateLoading ? '!raw-loader!' : ''), ); case 'styles': @@ -133,13 +139,13 @@ function visitComponentMetadata( } } -export function getResourceUrl(node: ts.Expression): string | null { +export function getResourceUrl(node: ts.Expression, loader = ''): string | null { // only analyze strings if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) { return null; } - return `${/^\.?\.\//.test(node.text) ? '' : './'}${node.text}`; + return `${loader}${/^\.?\.\//.test(node.text) ? '' : './'}${node.text}`; } function isComponentDecorator(node: ts.Node, typeChecker: ts.TypeChecker): node is ts.Decorator { @@ -155,8 +161,8 @@ function isComponentDecorator(node: ts.Node, typeChecker: ts.TypeChecker): node return false; } -function createRequireExpression(node: ts.Expression): ts.Expression { - const url = getResourceUrl(node); +function createRequireExpression(node: ts.Expression, loader = ''): ts.Expression { + const url = getResourceUrl(node, loader); if (!url) { return node; } diff --git a/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts b/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts index 0ba09eecb385..d385c767e7f2 100644 --- a/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts +++ b/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts @@ -9,10 +9,11 @@ import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit import { createTypescriptContext, transformTypescript } from './ast_helpers'; import { replaceResources } from './replace_resources'; -function transform(input: string, shouldTransform = true) { +function transform(input: string, shouldTransform = true, directTemplateLoading = true) { const { program } = createTypescriptContext(input); const getTypeChecker = () => program.getTypeChecker(); - const transformer = replaceResources(() => shouldTransform, getTypeChecker); + const transformer = replaceResources( + () => shouldTransform, getTypeChecker, directTemplateLoading); return transformTypescript(input, [transformer]); } @@ -45,7 +46,7 @@ describe('@ngtools/webpack transformers', () => { AppComponent = tslib_1.__decorate([ Component({ selector: 'app-root', - template: require("./app.component.html"), + template: require("!raw-loader!./app.component.html"), styles: [require("./app.component.css"), require("./app.component.2.css")] }) ], AppComponent); @@ -56,6 +57,75 @@ describe('@ngtools/webpack transformers', () => { expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); }); + it('should not replace resources when directTemplateLoading is false', () => { + const input = tags.stripIndent` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css', './app.component.2.css'] + }) + export class AppComponent { + title = 'app'; + } + `; + const output = tags.stripIndent` + import * as tslib_1 from "tslib"; + import { Component } from '@angular/core'; + let AppComponent = class AppComponent { + constructor() { + this.title = 'app'; + } + }; + AppComponent = tslib_1.__decorate([ + Component({ + selector: 'app-root', + template: require("./app.component.html"), + styles: [require("./app.component.css"), require("./app.component.2.css")] + }) + ], AppComponent); + export { AppComponent }; + `; + + const result = transform(input, true, false); + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + + it('should should support svg as templates', () => { + const input = tags.stripIndent` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + templateUrl: './app.component.svg' + }) + export class AppComponent { + title = 'app'; + } + `; + const output = tags.stripIndent` + import * as tslib_1 from "tslib"; + import { Component } from '@angular/core'; + let AppComponent = class AppComponent { + constructor() { + this.title = 'app'; + } + }; + AppComponent = tslib_1.__decorate([ + Component({ + selector: 'app-root', + template: require("!raw-loader!./app.component.svg") + }) + ], AppComponent); + export { AppComponent }; + `; + + const result = transform(input); + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + it('should merge styleUrls with styles', () => { const input = tags.stripIndent` import { Component } from '@angular/core'; @@ -81,7 +151,7 @@ describe('@ngtools/webpack transformers', () => { AppComponent = tslib_1.__decorate([ Component({ selector: 'app-root', - template: require("./app.component.html"), + template: require("!raw-loader!./app.component.html"), styles: ["a { color: red }", require("./app.component.css")] }) ], AppComponent); @@ -116,7 +186,7 @@ describe('@ngtools/webpack transformers', () => { AppComponent = tslib_1.__decorate([ Component({ selector: 'app-root', - template: require("./app.component.html"), + template: require("!raw-loader!./app.component.html"), styles: [require("./app.component.css"), require("./app.component.2.css")] }) ], AppComponent); @@ -151,7 +221,7 @@ describe('@ngtools/webpack transformers', () => { AppComponent = tslib_1.__decorate([ NgComponent({ selector: 'app-root', - template: require("./app.component.html"), + template: require("!raw-loader!./app.component.html"), styles: [require("./app.component.css"), require("./app.component.2.css")] }) ], AppComponent); @@ -160,7 +230,7 @@ describe('@ngtools/webpack transformers', () => { const { program } = createTypescriptContext(input); const getTypeChecker = () => program.getTypeChecker(); - const transformer = replaceResources(() => true, getTypeChecker); + const transformer = replaceResources(() => true, getTypeChecker, true); const result = transformTypescript(input, [transformer]); expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); @@ -190,7 +260,7 @@ describe('@ngtools/webpack transformers', () => { AppComponent = tslib_1.__decorate([ ng.Component({ selector: 'app-root', - template: require("./app.component.html"), + template: require("!raw-loader!./app.component.html"), styles: [require("./app.component.css"), require("./app.component.2.css")] }) ], AppComponent); @@ -238,7 +308,7 @@ describe('@ngtools/webpack transformers', () => { AppComponent = tslib_1.__decorate([ Component({ selector: 'app-root', - template: require("./app.component.html"), + template: require("!raw-loader!./app.component.html"), styles: [require("./app.component.css")] }) ], AppComponent);