Skip to content

Commit cf748c4

Browse files
committed
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 angular#10567
1 parent 0b7dd0e commit cf748c4

File tree

6 files changed

+116
-17
lines changed

6 files changed

+116
-17
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
@@ -294,7 +294,6 @@ export function getCommonConfig(wco: WebpackConfigOptions) {
294294
},
295295
module: {
296296
rules: [
297-
{ test: /\.html$/, loader: 'raw-loader' },
298297
{
299298
test: /\.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)$/,
300299
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
@@ -843,7 +843,8 @@ export class AngularCompilerPlugin {
843843

844844
if (this._JitMode) {
845845
// Replace resources in JIT.
846-
this._transformers.push(replaceResources(isAppPath, getTypeChecker));
846+
this._transformers.push(
847+
replaceResources(isAppPath, getTypeChecker, this._options.directTemplateLoading));
847848
} else {
848849
// Remove unneeded angular decorators.
849850
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

+39-6
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe('@ngtools/webpack transformers', () => {
4545
AppComponent = tslib_1.__decorate([
4646
Component({
4747
selector: 'app-root',
48-
template: require("./app.component.html"),
48+
template: require("!raw-loader!./app.component.html"),
4949
styles: [require("./app.component.css"), require("./app.component.2.css")]
5050
})
5151
], AppComponent);
@@ -56,6 +56,39 @@ describe('@ngtools/webpack transformers', () => {
5656
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
5757
});
5858

59+
it('should should support svg as templates', () => {
60+
const input = tags.stripIndent`
61+
import { Component } from '@angular/core';
62+
63+
@Component({
64+
selector: 'app-root',
65+
templateUrl: './app.component.svg'
66+
})
67+
export class AppComponent {
68+
title = 'app';
69+
}
70+
`;
71+
const output = tags.stripIndent`
72+
import * as tslib_1 from "tslib";
73+
import { Component } from '@angular/core';
74+
let AppComponent = class AppComponent {
75+
constructor() {
76+
this.title = 'app';
77+
}
78+
};
79+
AppComponent = tslib_1.__decorate([
80+
Component({
81+
selector: 'app-root',
82+
template: require("!raw-loader!./app.component.svg")
83+
})
84+
], AppComponent);
85+
export { AppComponent };
86+
`;
87+
88+
const result = transform(input);
89+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
90+
});
91+
5992
it('should merge styleUrls with styles', () => {
6093
const input = tags.stripIndent`
6194
import { Component } from '@angular/core';
@@ -81,7 +114,7 @@ describe('@ngtools/webpack transformers', () => {
81114
AppComponent = tslib_1.__decorate([
82115
Component({
83116
selector: 'app-root',
84-
template: require("./app.component.html"),
117+
template: require("!raw-loader!./app.component.html"),
85118
styles: ["a { color: red }", require("./app.component.css")]
86119
})
87120
], AppComponent);
@@ -116,7 +149,7 @@ describe('@ngtools/webpack transformers', () => {
116149
AppComponent = tslib_1.__decorate([
117150
Component({
118151
selector: 'app-root',
119-
template: require("./app.component.html"),
152+
template: require("!raw-loader!./app.component.html"),
120153
styles: [require("./app.component.css"), require("./app.component.2.css")]
121154
})
122155
], AppComponent);
@@ -151,7 +184,7 @@ describe('@ngtools/webpack transformers', () => {
151184
AppComponent = tslib_1.__decorate([
152185
NgComponent({
153186
selector: 'app-root',
154-
template: require("./app.component.html"),
187+
template: require("!raw-loader!./app.component.html"),
155188
styles: [require("./app.component.css"), require("./app.component.2.css")]
156189
})
157190
], AppComponent);
@@ -190,7 +223,7 @@ describe('@ngtools/webpack transformers', () => {
190223
AppComponent = tslib_1.__decorate([
191224
ng.Component({
192225
selector: 'app-root',
193-
template: require("./app.component.html"),
226+
template: require("!raw-loader!./app.component.html"),
194227
styles: [require("./app.component.css"), require("./app.component.2.css")]
195228
})
196229
], AppComponent);
@@ -238,7 +271,7 @@ describe('@ngtools/webpack transformers', () => {
238271
AppComponent = tslib_1.__decorate([
239272
Component({
240273
selector: 'app-root',
241-
template: require("./app.component.html"),
274+
template: require("!raw-loader!./app.component.html"),
242275
styles: [require("./app.component.css")]
243276
})
244277
], AppComponent);

0 commit comments

Comments
 (0)