Skip to content

Commit 744c559

Browse files
committed
feat(aot): creating files in a virtual fs.
In addition, reading and using AST on main.ts to figure out the entry module, if not specified.
1 parent b224395 commit 744c559

File tree

4 files changed

+282
-25
lines changed

4 files changed

+282
-25
lines changed

packages/angular-cli/models/webpack-build-typescript.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const getWebpackAotConfigPartial = function(projectRoot: string, appConfi
5555
new NgcWebpackPlugin({
5656
project: path.resolve(projectRoot, appConfig.root, appConfig.tsconfig),
5757
baseDir: path.resolve(projectRoot, ''),
58-
entryModule: path.join(projectRoot, appConfig.root, 'app/app.module#AppModule'),
58+
main: path.join(projectRoot, appConfig.root, appConfig.main),
5959
genDir: path.join(projectRoot, appConfig.outDir, 'ngfactory')
6060
}),
6161
]

packages/webpack/src/compiler_host.ts

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import * as ts from 'typescript';
2+
import {dirname} from 'path';
3+
import * as fs from 'fs';
4+
5+
6+
export interface OnErrorFn {
7+
(message: string): void;
8+
}
9+
10+
11+
const dev = Math.floor(Math.random() * 10000);
12+
13+
14+
export class VirtualStats implements fs.Stats {
15+
protected _ctime = new Date();
16+
protected _mtime = new Date();
17+
protected _atime = new Date();
18+
protected _btime = new Date();
19+
protected _dev = dev;
20+
protected _ino = Math.floor(Math.random() * 100000);
21+
protected _mode = parseInt('777', 8); // RWX for everyone.
22+
protected _uid = process.env['UID'] || 0;
23+
protected _gid = process.env['GID'] || 0;
24+
25+
constructor(private _path: string) {}
26+
27+
isFile() { return false; }
28+
isDirectory() { return false; }
29+
isBlockDevice() { return false; }
30+
isCharacterDevice() { return false; }
31+
isSymbolicLink() { return false; }
32+
isFIFO() { return false; }
33+
isSocket() { return false; }
34+
35+
get dev() { return this._dev; }
36+
get ino() { return this._ino; }
37+
get mode() { return this._mode; }
38+
get nlink() { return 1; } // Default to 1 hard link.
39+
get uid() { return this._uid; }
40+
get gid() { return this._gid; }
41+
get rdev() { return 0; }
42+
get size() { return 0; }
43+
get blksize() { return 512; }
44+
get blocks() { return Math.ceil(this.size / this.blksize); }
45+
get atime() { return this._atime; }
46+
get mtime() { return this._mtime; }
47+
get ctime() { return this._ctime; }
48+
get birthtime() { return this._btime; }
49+
}
50+
51+
export class VirtualDirStats extends VirtualStats {
52+
constructor(_fileName: string) {
53+
super(_fileName);
54+
}
55+
56+
isDirectory() { return true; }
57+
58+
get size() { return 1024; }
59+
}
60+
61+
export class VirtualFileStats extends VirtualStats {
62+
private _sourceFile: ts.SourceFile;
63+
constructor(_fileName: string, private _content: string) {
64+
super(_fileName);
65+
}
66+
67+
get content() { return this._content; }
68+
set content(v: string) {
69+
this._content = v;
70+
this._mtime = new Date();
71+
}
72+
getSourceFile(languageVersion: ts.ScriptTarget, setParentNodes: boolean) {
73+
if (!this._sourceFile) {
74+
this._sourceFile = ts.createSourceFile(
75+
this._path,
76+
this._content,
77+
languageVersion,
78+
setParentNodes);
79+
}
80+
81+
return this._sourceFile;
82+
}
83+
84+
isFile() { return true; }
85+
86+
get size() { return this._content.length; }
87+
}
88+
89+
90+
export class WebpackCompilerHost implements ts.CompilerHost {
91+
private _delegate: ts.CompilerHost;
92+
private _cachedFiles: {[path: string]: VirtualFileStats} = Object.create(null);
93+
private _directories: {[path: string]: VirtualDirStats} = Object.create(null);
94+
95+
constructor(private _options: ts.CompilerOptions, private _setParentNodes = true) {
96+
this._delegate = ts.createCompilerHost(this._options, this._setParentNodes);
97+
}
98+
99+
private _setFileContent(fileName: string, content: string) {
100+
this._cachedFiles[fileName] = new VirtualFileStats(fileName, content);
101+
102+
let p = dirname(fileName);
103+
while (p && !this._directories[p]) {
104+
this._directories[p] = new VirtualDirStats(p);
105+
p = dirname(p);
106+
}
107+
}
108+
109+
populateWebpackResolver(resolver: any) {
110+
const fs = resolver.fileSystem;
111+
112+
for (const fileName of Object.keys(this._cachedFiles)) {
113+
const stats = this._cachedFiles[fileName];
114+
fs._statStorage.data[fileName] = [null, stats];
115+
fs._readFileStorage.data[fileName] = [null, stats.content];
116+
}
117+
for (const path of Object.keys(this._directories)) {
118+
const stats = this._directories[path];
119+
fs._statStorage.data[path] = [null, stats];
120+
fs._readdirStorage.data[path] = [null, stats.content];
121+
}
122+
}
123+
124+
fileExists(fileName: string): boolean {
125+
return fileName in this._cachedFiles || this._delegate.fileExists(fileName);
126+
}
127+
128+
readFile(fileName: string): string {
129+
return (fileName in this._cachedFiles)
130+
? this._cachedFiles[fileName].content
131+
: this._delegate.readFile(fileName);
132+
}
133+
134+
directoryExists(directoryName: string): boolean {
135+
return (directoryName in this._directories) || this._delegate.directoryExists(directoryName);
136+
}
137+
138+
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: OnErrorFn) {
139+
if (!(fileName in this._cachedFiles)) {
140+
return this._delegate.getSourceFile(fileName, languageVersion, onError);
141+
}
142+
143+
return this._cachedFiles[fileName].getSourceFile(languageVersion, this._setParentNodes);
144+
}
145+
146+
getCancellationToken() {
147+
return this._delegate.getCancellationToken();
148+
}
149+
150+
getDefaultLibFileName(options: ts.CompilerOptions) {
151+
return this._delegate.getDefaultLibFileName(options);
152+
}
153+
154+
writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: OnErrorFn) {
155+
this._setFileContent(fileName, data);
156+
}
157+
158+
getCurrentDirectory(): string {
159+
return this._delegate.getCurrentDirectory();
160+
}
161+
162+
getCanonicalFileName(fileName: string): string {
163+
return this._delegate.getCanonicalFileName(fileName);
164+
}
165+
166+
useCaseSensitiveFileNames(): boolean {
167+
return this._delegate.useCaseSensitiveFileNames();
168+
}
169+
170+
getNewLine(): string {
171+
return this._delegate.getNewLine();
172+
}
173+
}

packages/webpack/src/plugin.ts

+84-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as fs from 'fs';
12
import * as ts from 'typescript';
23
import * as path from 'path';
34

@@ -9,6 +10,19 @@ import {patchReflectorHost} from './reflector_host';
910
import {WebpackResourceLoader} from './resource_loader';
1011
import {createResolveDependenciesFromContextMap} from './utils';
1112
import { AngularCompilerOptions } from '@angular/tsc-wrapped';
13+
import {WebpackCompilerHost} from './compiler_host';
14+
15+
16+
function _findNodes(sourceFile: ts.SourceFile, node: ts.Node, kind: ts.SyntaxKind,
17+
keepGoing = false): ts.Node[] {
18+
if (node.kind == kind && !keepGoing) {
19+
return [node];
20+
}
21+
22+
return node.getChildren(sourceFile).reduce((result, n) => {
23+
return result.concat(_findNodes(sourceFile, n, kind, keepGoing));
24+
}, node.kind == kind ? [node] : []);
25+
}
1226

1327

1428
/**
@@ -22,6 +36,7 @@ export interface AngularWebpackPluginOptions {
2236
baseDir: string;
2337
basePath?: string;
2438
genDir?: string;
39+
main?: string;
2540
}
2641

2742

@@ -32,7 +47,7 @@ export class NgcWebpackPlugin {
3247
reflector: ngCompiler.StaticReflector;
3348
reflectorHost: ngCompiler.ReflectorHost;
3449
program: ts.Program;
35-
compilerHost: ts.CompilerHost;
50+
compilerHost: WebpackCompilerHost;
3651
compilerOptions: ts.CompilerOptions;
3752
angularCompilerOptions: AngularCompilerOptions;
3853
files: any[];
@@ -58,19 +73,77 @@ export class NgcWebpackPlugin {
5873
this.genDir = this.options.genDir
5974
|| path.resolve(process.cwd(), this.angularCompilerOptions.genDir + '/app');
6075
this.entryModule = options.entryModule || (this.angularCompilerOptions as any).entryModule;
76+
if (!options.entryModule && options.main) {
77+
this.entryModule = this.figureOutEntryFromMain(options.main);
78+
}
6179

6280
const entryModule = this.entryModule;
6381
const [rootModule, rootNgModule] = entryModule.split('#');
6482
this.projectPath = options.project;
6583
this.rootModule = rootModule;
6684
this.rootModuleName = rootNgModule;
67-
this.compilerHost = ts.createCompilerHost(this.compilerOptions, true);
85+
this.compilerHost = new WebpackCompilerHost(this.compilerOptions);
6886
this.program = ts.createProgram(this.files, this.compilerOptions, this.compilerHost);
6987
this.reflectorHost = new ngCompiler.ReflectorHost(
7088
this.program, this.compilerHost, this.angularCompilerOptions);
7189
this.reflector = new ngCompiler.StaticReflector(this.reflectorHost);
7290
}
7391

92+
figureOutEntryFromMain(mainPath: string): string {
93+
const source = ts.createSourceFile(
94+
mainPath, fs.readFileSync(mainPath, 'utf-8'), ts.ScriptTarget.Latest);
95+
96+
const bootstrap = _findNodes(source, source, ts.SyntaxKind.CallExpression, false)
97+
.map(node => node as ts.CallExpression)
98+
.filter(call => {
99+
const access = call.expression as ts.PropertyAccessExpression;
100+
return access.kind == ts.SyntaxKind.PropertyAccessExpression
101+
&& access.name.kind == ts.SyntaxKind.Identifier
102+
&& (access.name.text == 'bootstrapModule'
103+
|| access.name.text == 'bootstrapModuleFactory');
104+
});
105+
106+
if (bootstrap.length == 0) {
107+
throw new Error('Tried to find bootstrap code, but could not. Specify either '
108+
+ 'statically analyzable bootstrap code or pass in an entryModule '
109+
+ 'to the plugins options.');
110+
}
111+
112+
const bootstrapSymbolName = bootstrap[0].arguments[0].text;
113+
114+
// We found the bootstrap variable, now we just need to get where it's imported.
115+
const imports = _findNodes(source, source, ts.SyntaxKind.ImportDeclaration, false)
116+
.map(node => node as ts.ImportDeclaration);
117+
118+
for (const decl of imports) {
119+
if (!decl.importClause || !decl.moduleSpecifier) {
120+
continue;
121+
}
122+
123+
const module = decl.moduleSpecifier.text;
124+
125+
if (decl.importClause.namedBindings.kind == ts.SyntaxKind.NamespaceImport) {
126+
const binding = decl.importClause.namedBindings as ts.NamespaceImport;
127+
if (binding.name.text == bootstrapModuleName) {
128+
// This is a default export.
129+
return decl.moduleSpecifier.text;
130+
}
131+
} else if (decl.importClause.namedBindings.kind == ts.SyntaxKind.NamedImports) {
132+
const binding = decl.importClause.namedBindings as ts.NamedImports;
133+
for (const specifier: ts.ImportSpecifier of binding.elements) {
134+
if (specifier.name.text == bootstrapSymbolName) {
135+
return `${path.resolve(path.dirname(mainPath), module)}#${bootstrapSymbolName}`;
136+
}
137+
}
138+
}
139+
}
140+
141+
// shrug... something bad happened and we couldn't find the import statement.
142+
throw new Error('Tried to find bootstrap code, but could not. Specify either '
143+
+ 'statically analyzable bootstrap code or pass in an entryModule '
144+
+ 'to the plugins options.');
145+
}
146+
74147
// registration hook for webpack plugin
75148
apply(compiler: any) {
76149
this.compiler = compiler;
@@ -110,6 +183,15 @@ export class NgcWebpackPlugin {
110183
compilation._ngToolsWebpackPluginInstance = null;
111184
cb();
112185
});
186+
187+
// Virtual file system.
188+
compiler.resolvers.normal.plugin('resolve', (request: any, cb?: () => void) => {
189+
// populate the file system cache with the virtual module
190+
this.compilerHost.populateWebpackResolver(compiler.resolvers.normal);
191+
if (cb) {
192+
cb();
193+
}
194+
});
113195
}
114196

115197
private _make(compilation: any, cb: (err?: any, request?: any) => void) {

0 commit comments

Comments
 (0)