Skip to content

Commit dfb08b9

Browse files
oocxhansl
authored andcommitted
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
1 parent 1fcb9a0 commit dfb08b9

File tree

6 files changed

+156
-20
lines changed

6 files changed

+156
-20
lines changed

packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts

-1
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,6 @@ export function getCommonConfig(wco: WebpackConfigOptions) {
307307
},
308308
module: {
309309
rules: [
310-
{ test: /\.html$/, loader: 'raw-loader' },
311310
{
312311
test: /\.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)$/,
313312
loader: 'file-loader',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { runTargetSpec } from '@angular-devkit/architect/testing';
10+
import { join, normalize, virtualFs } from '@angular-devkit/core';
11+
import { tap } from 'rxjs/operators';
12+
import { browserTargetSpec, host, outputPath } from '../utils';
13+
14+
describe('Browser Builder allow svg', () => {
15+
16+
beforeEach(done => host.initialize().toPromise().then(done, done.fail));
17+
afterEach(done => host.restore().toPromise().then(done, done.fail));
18+
19+
it('works with aot',
20+
(done) => {
21+
22+
const svg = `
23+
<svg xmlns="http://www.w3.org/2000/svg">
24+
<text x="20" y="20" font-size="20" fill="red">Hello World</text>
25+
</svg>`;
26+
27+
host.writeMultipleFiles({
28+
'./src/app/app.component.svg': svg,
29+
'./src/app/app.component.ts': `
30+
import { Component } from '@angular/core';
31+
32+
@Component({
33+
selector: 'app-root',
34+
templateUrl: './app.component.svg',
35+
styleUrls: []
36+
})
37+
export class AppComponent {
38+
title = 'app';
39+
}
40+
`,
41+
});
42+
43+
const overrides = { aot: true };
44+
45+
runTargetSpec(host, browserTargetSpec, overrides).pipe(
46+
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
47+
tap(() => {
48+
const content = virtualFs.fileBufferToString(
49+
host.scopedSync().read(join(outputPath, 'main.js')),
50+
);
51+
52+
expect(content).toContain('":svg:svg"');
53+
expect(host.scopedSync().exists(normalize('dist/app.component.svg')))
54+
.toBe(false, 'should not copy app.component.svg to dist');
55+
}),
56+
).toPromise().then(done, done.fail);
57+
});
58+
59+
});

packages/ngtools/webpack/src/angular_compiler_plugin.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,8 @@ export class AngularCompilerPlugin {
863863

864864
if (this._JitMode) {
865865
// Replace resources in JIT.
866-
this._transformers.push(replaceResources(isAppPath, getTypeChecker));
866+
this._transformers.push(
867+
replaceResources(isAppPath, getTypeChecker, this._options.directTemplateLoading));
867868
} else {
868869
// Remove unneeded angular decorators.
869870
this._transformers.push(removeDecorators(isAppPath, getTypeChecker));

packages/ngtools/webpack/src/compiler_host.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,8 @@ export class WebpackCompilerHost implements ts.CompilerHost {
336336
}
337337

338338
readResource(fileName: string) {
339-
if (this.directTemplateLoading && fileName.endsWith('.html')) {
339+
if (this.directTemplateLoading &&
340+
(fileName.endsWith('.html') || fileName.endsWith('.svg'))) {
340341
return this.readFile(fileName);
341342
}
342343

packages/ngtools/webpack/src/transformers/replace_resources.ts

+14-8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as ts from 'typescript';
1010
export function replaceResources(
1111
shouldTransform: (fileName: string) => boolean,
1212
getTypeChecker: () => ts.TypeChecker,
13+
directTemplateLoading = false,
1314
): ts.TransformerFactory<ts.SourceFile> {
1415

1516
return (context: ts.TransformationContext) => {
@@ -19,7 +20,7 @@ export function replaceResources(
1920
if (ts.isClassDeclaration(node)) {
2021
node.decorators = ts.visitNodes(
2122
node.decorators,
22-
(node: ts.Decorator) => visitDecorator(node, typeChecker),
23+
(node: ts.Decorator) => visitDecorator(node, typeChecker, directTemplateLoading),
2324
);
2425
}
2526

@@ -34,7 +35,10 @@ export function replaceResources(
3435
};
3536
}
3637

37-
function visitDecorator(node: ts.Decorator, typeChecker: ts.TypeChecker): ts.Decorator {
38+
function visitDecorator(
39+
node: ts.Decorator,
40+
typeChecker: ts.TypeChecker,
41+
directTemplateLoading: boolean): ts.Decorator {
3842
if (!isComponentDecorator(node, typeChecker)) {
3943
return node;
4044
}
@@ -56,7 +60,8 @@ function visitDecorator(node: ts.Decorator, typeChecker: ts.TypeChecker): ts.Dec
5660
// visit all properties
5761
let properties = ts.visitNodes(
5862
objectExpression.properties,
59-
(node: ts.ObjectLiteralElementLike) => visitComponentMetadata(node, styleReplacements),
63+
(node: ts.ObjectLiteralElementLike) =>
64+
visitComponentMetadata(node, styleReplacements, directTemplateLoading),
6065
);
6166

6267
// replace properties with updated properties
@@ -83,6 +88,7 @@ function visitDecorator(node: ts.Decorator, typeChecker: ts.TypeChecker): ts.Dec
8388
function visitComponentMetadata(
8489
node: ts.ObjectLiteralElementLike,
8590
styleReplacements: ts.Expression[],
91+
directTemplateLoading: boolean,
8692
): ts.ObjectLiteralElementLike | undefined {
8793
if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name)) {
8894
return node;
@@ -98,7 +104,7 @@ function visitComponentMetadata(
98104
return ts.updatePropertyAssignment(
99105
node,
100106
ts.createIdentifier('template'),
101-
createRequireExpression(node.initializer),
107+
createRequireExpression(node.initializer, directTemplateLoading ? '!raw-loader!' : ''),
102108
);
103109

104110
case 'styles':
@@ -133,13 +139,13 @@ function visitComponentMetadata(
133139
}
134140
}
135141

136-
export function getResourceUrl(node: ts.Expression): string | null {
142+
export function getResourceUrl(node: ts.Expression, loader = ''): string | null {
137143
// only analyze strings
138144
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
139145
return null;
140146
}
141147

142-
return `${/^\.?\.\//.test(node.text) ? '' : './'}${node.text}`;
148+
return `${loader}${/^\.?\.\//.test(node.text) ? '' : './'}${node.text}`;
143149
}
144150

145151
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
155161
return false;
156162
}
157163

158-
function createRequireExpression(node: ts.Expression): ts.Expression {
159-
const url = getResourceUrl(node);
164+
function createRequireExpression(node: ts.Expression, loader = ''): ts.Expression {
165+
const url = getResourceUrl(node, loader);
160166
if (!url) {
161167
return node;
162168
}

packages/ngtools/webpack/src/transformers/replace_resources_spec.ts

+79-9
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit
99
import { createTypescriptContext, transformTypescript } from './ast_helpers';
1010
import { replaceResources } from './replace_resources';
1111

12-
function transform(input: string, shouldTransform = true) {
12+
function transform(input: string, shouldTransform = true, directTemplateLoading = true) {
1313
const { program } = createTypescriptContext(input);
1414
const getTypeChecker = () => program.getTypeChecker();
15-
const transformer = replaceResources(() => shouldTransform, getTypeChecker);
15+
const transformer = replaceResources(
16+
() => shouldTransform, getTypeChecker, directTemplateLoading);
1617

1718
return transformTypescript(input, [transformer]);
1819
}
@@ -45,7 +46,7 @@ describe('@ngtools/webpack transformers', () => {
4546
AppComponent = tslib_1.__decorate([
4647
Component({
4748
selector: 'app-root',
48-
template: require("./app.component.html"),
49+
template: require("!raw-loader!./app.component.html"),
4950
styles: [require("./app.component.css"), require("./app.component.2.css")]
5051
})
5152
], AppComponent);
@@ -56,6 +57,75 @@ describe('@ngtools/webpack transformers', () => {
5657
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
5758
});
5859

60+
it('should not replace resources when directTemplateLoading is false', () => {
61+
const input = tags.stripIndent`
62+
import { Component } from '@angular/core';
63+
64+
@Component({
65+
selector: 'app-root',
66+
templateUrl: './app.component.html',
67+
styleUrls: ['./app.component.css', './app.component.2.css']
68+
})
69+
export class AppComponent {
70+
title = 'app';
71+
}
72+
`;
73+
const output = tags.stripIndent`
74+
import * as tslib_1 from "tslib";
75+
import { Component } from '@angular/core';
76+
let AppComponent = class AppComponent {
77+
constructor() {
78+
this.title = 'app';
79+
}
80+
};
81+
AppComponent = tslib_1.__decorate([
82+
Component({
83+
selector: 'app-root',
84+
template: require("./app.component.html"),
85+
styles: [require("./app.component.css"), require("./app.component.2.css")]
86+
})
87+
], AppComponent);
88+
export { AppComponent };
89+
`;
90+
91+
const result = transform(input, true, false);
92+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
93+
});
94+
95+
96+
it('should should support svg as templates', () => {
97+
const input = tags.stripIndent`
98+
import { Component } from '@angular/core';
99+
100+
@Component({
101+
selector: 'app-root',
102+
templateUrl: './app.component.svg'
103+
})
104+
export class AppComponent {
105+
title = 'app';
106+
}
107+
`;
108+
const output = tags.stripIndent`
109+
import * as tslib_1 from "tslib";
110+
import { Component } from '@angular/core';
111+
let AppComponent = class AppComponent {
112+
constructor() {
113+
this.title = 'app';
114+
}
115+
};
116+
AppComponent = tslib_1.__decorate([
117+
Component({
118+
selector: 'app-root',
119+
template: require("!raw-loader!./app.component.svg")
120+
})
121+
], AppComponent);
122+
export { AppComponent };
123+
`;
124+
125+
const result = transform(input);
126+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
127+
});
128+
59129
it('should merge styleUrls with styles', () => {
60130
const input = tags.stripIndent`
61131
import { Component } from '@angular/core';
@@ -81,7 +151,7 @@ describe('@ngtools/webpack transformers', () => {
81151
AppComponent = tslib_1.__decorate([
82152
Component({
83153
selector: 'app-root',
84-
template: require("./app.component.html"),
154+
template: require("!raw-loader!./app.component.html"),
85155
styles: ["a { color: red }", require("./app.component.css")]
86156
})
87157
], AppComponent);
@@ -116,7 +186,7 @@ describe('@ngtools/webpack transformers', () => {
116186
AppComponent = tslib_1.__decorate([
117187
Component({
118188
selector: 'app-root',
119-
template: require("./app.component.html"),
189+
template: require("!raw-loader!./app.component.html"),
120190
styles: [require("./app.component.css"), require("./app.component.2.css")]
121191
})
122192
], AppComponent);
@@ -151,7 +221,7 @@ describe('@ngtools/webpack transformers', () => {
151221
AppComponent = tslib_1.__decorate([
152222
NgComponent({
153223
selector: 'app-root',
154-
template: require("./app.component.html"),
224+
template: require("!raw-loader!./app.component.html"),
155225
styles: [require("./app.component.css"), require("./app.component.2.css")]
156226
})
157227
], AppComponent);
@@ -160,7 +230,7 @@ describe('@ngtools/webpack transformers', () => {
160230

161231
const { program } = createTypescriptContext(input);
162232
const getTypeChecker = () => program.getTypeChecker();
163-
const transformer = replaceResources(() => true, getTypeChecker);
233+
const transformer = replaceResources(() => true, getTypeChecker, true);
164234
const result = transformTypescript(input, [transformer]);
165235

166236
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
@@ -190,7 +260,7 @@ describe('@ngtools/webpack transformers', () => {
190260
AppComponent = tslib_1.__decorate([
191261
ng.Component({
192262
selector: 'app-root',
193-
template: require("./app.component.html"),
263+
template: require("!raw-loader!./app.component.html"),
194264
styles: [require("./app.component.css"), require("./app.component.2.css")]
195265
})
196266
], AppComponent);
@@ -238,7 +308,7 @@ describe('@ngtools/webpack transformers', () => {
238308
AppComponent = tslib_1.__decorate([
239309
Component({
240310
selector: 'app-root',
241-
template: require("./app.component.html"),
311+
template: require("!raw-loader!./app.component.html"),
242312
styles: [require("./app.component.css")]
243313
})
244314
], AppComponent);

0 commit comments

Comments
 (0)