Skip to content

Commit 4cfd44d

Browse files
committed
feat(@ngtools/webpack) support of single file component with custom types
1 parent 28e66f3 commit 4cfd44d

File tree

5 files changed

+232
-35
lines changed

5 files changed

+232
-35
lines changed

packages/@angular/cli/lib/config/schema.json

+4
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@
197197
"description": "Name and corresponding file for environment config.",
198198
"type": "object",
199199
"additionalProperties": true
200+
},
201+
"defaultStyleType": {
202+
"description": "Default file type for inline styles.",
203+
"type": "string"
200204
}
201205
},
202206
"additionalProperties": false

packages/@angular/cli/models/webpack-configs/typescript.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ function _createAotPlugin(wco: WebpackConfigOptions, options: any) {
9595
// If we don't explicitely list excludes, it will default to `['**/*.spec.ts']`.
9696
exclude: [],
9797
include: options.include,
98+
99+
defaultStyleType: appConfig.defaultStyleType
98100
}, options);
99101
return new AngularCompilerPlugin(pluginOptions);
100102
} else {
@@ -108,7 +110,9 @@ function _createAotPlugin(wco: WebpackConfigOptions, options: any) {
108110
hostReplacementPaths,
109111
sourceMap: buildOptions.sourcemaps,
110112
// If we don't explicitely list excludes, it will default to `['**/*.spec.ts']`.
111-
exclude: []
113+
exclude: [],
114+
115+
defaultStyleType: appConfig.defaultStyleType
112116
}, options);
113117
return new AotPlugin(pluginOptions);
114118
}

packages/@ngtools/webpack/src/angular_compiler_plugin.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ export interface AngularCompilerPluginOptions {
6969
missingTranslation?: string;
7070
platform?: PLATFORM;
7171

72+
defaultTemplateType?: string;
73+
defaultStyleType?: string;
74+
7275
// Use tsconfig to include path globs.
7376
exclude?: string | string[];
7477
include?: string[];
@@ -278,7 +281,12 @@ export class AngularCompilerPlugin implements Tapable {
278281
}
279282

280283
// Create the webpack compiler host.
281-
this._compilerHost = new WebpackCompilerHost(this._compilerOptions, this._basePath);
284+
this._compilerHost = new WebpackCompilerHost(
285+
this._compilerOptions,
286+
this._basePath,
287+
this._options.defaultTemplateType,
288+
this._options.defaultStyleType
289+
);
282290
this._compilerHost.enableCaching();
283291

284292
// Create and set a new WebpackResourceLoader.

packages/@ngtools/webpack/src/compiler_host.ts

+205-32
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import * as ts from 'typescript';
22
import {basename, dirname, join, sep} from 'path';
33
import * as fs from 'fs';
44
import {WebpackResourceLoader} from './resource_loader';
5+
import {TypeScriptFileRefactor} from './refactor';
6+
const MagicString = require('magic-string');
57

68

79
export interface OnErrorFn {
@@ -11,6 +13,49 @@ export interface OnErrorFn {
1113

1214
const dev = Math.floor(Math.random() * 10000);
1315

16+
// partial copy of TypeScriptFileRefactor
17+
class InlineResourceRefactor {
18+
private _sourceString: string;
19+
private _changed = false;
20+
21+
constructor(content: string, private _sourceFile: ts.SourceFile) {
22+
this._sourceString = new MagicString(content);
23+
}
24+
25+
getResourcesNodes() {
26+
return this.findAstNodes(this._sourceFile, ts.SyntaxKind.ObjectLiteralExpression, true)
27+
.map(node => this.findAstNodes(node, ts.SyntaxKind.PropertyAssignment))
28+
.filter(node => !!node)
29+
.reduce((prev, curr: ts.PropertyAssignment[]) => prev.concat(curr
30+
.filter(node =>
31+
node.name.kind == ts.SyntaxKind.Identifier ||
32+
node.name.kind == ts.SyntaxKind.StringLiteral
33+
)
34+
), [] ) as ts.PropertyAssignment[];
35+
}
36+
37+
getResourceContentAndType(_content: string, defaultType: string) {
38+
let type = defaultType;
39+
const content = _content
40+
.replace(/!(\w*)!/, (_, _type) => {
41+
type = _type;
42+
return '';
43+
});
44+
return {content, type};
45+
}
46+
47+
get hasChanged() {
48+
return this._changed;
49+
}
50+
51+
getNewContent() {
52+
return this._sourceString.toString();
53+
}
54+
55+
findAstNodes = TypeScriptFileRefactor.prototype.findAstNodes;
56+
replaceNode = TypeScriptFileRefactor.prototype.replaceNode;
57+
58+
}
1459

1560
export class VirtualStats implements fs.Stats {
1661
protected _ctime = new Date();
@@ -61,6 +106,8 @@ export class VirtualDirStats extends VirtualStats {
61106

62107
export class VirtualFileStats extends VirtualStats {
63108
private _sourceFile: ts.SourceFile | null;
109+
private _resources: string[] = [];
110+
64111
constructor(_fileName: string, private _content: string) {
65112
super(_fileName);
66113
}
@@ -71,21 +118,19 @@ export class VirtualFileStats extends VirtualStats {
71118
this._mtime = new Date();
72119
this._sourceFile = null;
73120
}
74-
setSourceFile(sourceFile: ts.SourceFile) {
121+
set sourceFile(sourceFile: ts.SourceFile) {
75122
this._sourceFile = sourceFile;
76123
}
77-
getSourceFile(languageVersion: ts.ScriptTarget, setParentNodes: boolean) {
78-
if (!this._sourceFile) {
79-
this._sourceFile = ts.createSourceFile(
80-
this._path,
81-
this._content,
82-
languageVersion,
83-
setParentNodes);
84-
}
85-
124+
get sourceFile() {
86125
return this._sourceFile;
87126
}
88127

128+
addResource(resourcePath: string) {
129+
this._resources.push(resourcePath);
130+
}
131+
132+
get resources(){ return this._resources; }
133+
89134
isFile() { return true; }
90135

91136
get size() { return this._content.length; }
@@ -107,7 +152,8 @@ export class WebpackCompilerHost implements ts.CompilerHost {
107152
private _cache = false;
108153
private _resourceLoader?: WebpackResourceLoader | undefined;
109154

110-
constructor(private _options: ts.CompilerOptions, basePath: string) {
155+
constructor(private _options: ts.CompilerOptions, basePath: string,
156+
private _defaultTemplateType = 'html', private _defaultStyleType = 'css') {
111157
this._setParentNodes = true;
112158
this._delegate = ts.createCompilerHost(this._options, this._setParentNodes);
113159
this._basePath = this._normalizePath(basePath);
@@ -128,7 +174,7 @@ export class WebpackCompilerHost implements ts.CompilerHost {
128174
}
129175
}
130176

131-
private _setFileContent(fileName: string, content: string) {
177+
private _setFileContent(fileName: string, content: string, resource?: boolean) {
132178
this._files[fileName] = new VirtualFileStats(fileName, content);
133179

134180
let p = dirname(fileName);
@@ -138,7 +184,10 @@ export class WebpackCompilerHost implements ts.CompilerHost {
138184
p = dirname(p);
139185
}
140186

141-
this._changedFiles[fileName] = true;
187+
// only ts files are expected on getChangedFiles()
188+
if (!resource) {
189+
this._changedFiles[fileName] = true;
190+
}
142191
}
143192

144193
get dirty() {
@@ -165,33 +214,82 @@ export class WebpackCompilerHost implements ts.CompilerHost {
165214

166215
invalidate(fileName: string): void {
167216
fileName = this.resolve(fileName);
168-
if (fileName in this._files) {
217+
const file = this._files[fileName];
218+
if (file != null) {
219+
file.resources
220+
.forEach(r => this.invalidate(r));
221+
169222
this._files[fileName] = null;
170-
this._changedFiles[fileName] = true;
223+
}
224+
if (fileName in this._changedFiles) {
225+
this._changedFiles[fileName] = true;
226+
}
227+
}
228+
229+
/**
230+
* Return the corresponding component path
231+
* or undefined if path isn't considered a resource
232+
*/
233+
private _getComponentPath(path: string) {
234+
const match = path.match(
235+
// match ngtemplate, ngstyles but not shim nor summaries
236+
/(.*)\.(?:ngtemplate|(?:ngstyles[\d]*))(?!.*(?:shim.ngstyle.ts|ngsummary.json)$).*$/
237+
);
238+
239+
if (match != null) {
240+
return match[1] + '.ts';
171241
}
172242
}
173243

174244
fileExists(fileName: string, delegate = true): boolean {
175245
fileName = this.resolve(fileName);
176-
return this._files[fileName] != null || (delegate && this._delegate.fileExists(fileName));
246+
if (this._files[fileName] != null) {
247+
return true;
248+
}
249+
250+
const componentPath = this._getComponentPath(fileName);
251+
if (componentPath != null) {
252+
return this._files[componentPath] == null &&
253+
this._readResource(fileName, componentPath) != null;
254+
} else {
255+
if (delegate) {
256+
return this._delegate.fileExists(fileName);
257+
}
258+
}
259+
260+
return false;
177261
}
178262

179263
readFile(fileName: string): string {
180264
fileName = this.resolve(fileName);
181265

182266
const stats = this._files[fileName];
183267
if (stats == null) {
268+
const componentPath = this._getComponentPath(fileName);
269+
if (componentPath != null) {
270+
return this._readResource(fileName, componentPath);
271+
}
272+
184273
const result = this._delegate.readFile(fileName);
185274
if (result !== undefined && this._cache) {
186275
this._setFileContent(fileName, result);
187-
return result;
188-
} else {
189-
return result;
190276
}
277+
278+
return result;
191279
}
192280
return stats.content;
193281
}
194282

283+
private _readResource(resourcePath: string, componentPath: string) {
284+
// Trigger source file build which will create and cache associated resources
285+
this.getSourceFile(componentPath);
286+
287+
const stats = this._files[resourcePath];
288+
if (stats != null) {
289+
return stats.content;
290+
}
291+
}
292+
195293
// Does not delegate, use with `fileExists/directoryExists()`.
196294
stat(path: string): VirtualStats {
197295
path = this.resolve(path);
@@ -228,24 +326,98 @@ export class WebpackCompilerHost implements ts.CompilerHost {
228326
return delegated.concat(subdirs);
229327
}
230328

231-
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, _onError?: OnErrorFn) {
329+
private _buildSourceFile(fileName: string, content: string, languageVersion: ts.ScriptTarget) {
330+
let sourceFile = ts.createSourceFile(fileName, content, languageVersion, this._setParentNodes);
331+
332+
const refactor = new InlineResourceRefactor(content, sourceFile);
333+
334+
const prefix = fileName.substring(0, fileName.lastIndexOf('.'));
335+
const resources: string[] = [];
336+
337+
refactor.getResourcesNodes()
338+
.forEach( (node: any) => {
339+
const name = node.name.text;
340+
341+
if (name === 'template') {
342+
const {content, type} = refactor.getResourceContentAndType(
343+
node.initializer.text,
344+
this._defaultTemplateType
345+
);
346+
const path = `${prefix}.ngtemplate.${type}`;
347+
348+
// always cache resources
349+
this._setFileContent(path, content, true);
350+
resources.push(path);
351+
352+
refactor.replaceNode(node, `templateUrl: './${basename(path)}'`);
353+
} else {
354+
if (name === 'styles') {
355+
const arr = <ts.ArrayLiteralExpression[]>
356+
refactor.findAstNodes(node, ts.SyntaxKind.ArrayLiteralExpression, false);
357+
358+
if (arr && arr.length > 0 && arr[0].elements.length > 0) {
359+
const styles = arr[0].elements
360+
.map( (element: any) => element.text)
361+
.map( (_content, idx) => {
362+
const {content, type} = refactor.getResourceContentAndType(
363+
_content,
364+
this._defaultStyleType
365+
);
366+
367+
return {path: `${prefix}.ngstyles${idx}.${type}`, content};
368+
});
369+
370+
styles.forEach(({path, content}) => {
371+
// always cache resources
372+
this._setFileContent(path, content, true);
373+
resources.push(path);
374+
});
375+
376+
const styleUrls = styles
377+
.map( ({path}) => `'./${basename(path)}'`)
378+
.join(',');
379+
380+
refactor.replaceNode(node, `styleUrls: [${styleUrls}]`);
381+
}
382+
}
383+
}
384+
});
385+
386+
if (refactor.hasChanged) {
387+
sourceFile = ts.createSourceFile(
388+
fileName, refactor.getNewContent(), languageVersion, this._setParentNodes
389+
);
390+
}
391+
392+
return {
393+
sourceFile,
394+
resources
395+
};
396+
}
397+
398+
getSourceFile(fileName: string, languageVersion = ts.ScriptTarget.Latest, _onError?: OnErrorFn) {
232399
fileName = this.resolve(fileName);
233400

234401
const stats = this._files[fileName];
235-
if (stats == null) {
236-
const content = this.readFile(fileName);
237-
238-
if (!this._cache) {
239-
return ts.createSourceFile(fileName, content, languageVersion, this._setParentNodes);
240-
} else if (!this._files[fileName]) {
241-
// If cache is turned on and the file exists, the readFile call will have populated stats.
242-
// Empty stats at this point mean the file doesn't exist at and so we should return
243-
// undefined.
244-
return undefined;
245-
}
402+
if (stats != null && stats.sourceFile != null) {
403+
return stats.sourceFile;
404+
}
405+
406+
const content = this.readFile(fileName);
407+
if (!content) {
408+
return;
409+
}
410+
411+
const {sourceFile, resources} = this._buildSourceFile(fileName, content, languageVersion);
412+
413+
if (this._cache) {
414+
const stats = this._files[fileName];
415+
stats.sourceFile = sourceFile;
416+
417+
resources.forEach(r => stats.addResource(r));
246418
}
247419

248-
return this._files[fileName]!.getSourceFile(languageVersion, this._setParentNodes);
420+
return sourceFile;
249421
}
250422

251423
getCancellationToken() {
@@ -288,6 +460,7 @@ export class WebpackCompilerHost implements ts.CompilerHost {
288460
this._resourceLoader = resourceLoader;
289461
}
290462

463+
// this function and resourceLoader is pretty new and seem unusued so I ignored it for the moment.
291464
readResource(fileName: string) {
292465
if (this._resourceLoader) {
293466
const denormalizedFileName = fileName.replace(/\//g, sep);

0 commit comments

Comments
 (0)