Skip to content

Commit ed4e34e

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 ed4e34e

File tree

4 files changed

+294
-25
lines changed

4 files changed

+294
-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

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import * as ts from 'typescript';
2+
import {basename, 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(protected _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+
const files = Object.keys(this._directories)
120+
.filter(fileName => dirname(fileName) == path)
121+
.map(path => basename(path));
122+
123+
fs._statStorage.data[path] = [null, stats];
124+
fs._readdirStorage.data[path] = [null, files];
125+
}
126+
}
127+
128+
fileExists(fileName: string): boolean {
129+
return fileName in this._cachedFiles || this._delegate.fileExists(fileName);
130+
}
131+
132+
readFile(fileName: string): string {
133+
return (fileName in this._cachedFiles)
134+
? this._cachedFiles[fileName].content
135+
: this._delegate.readFile(fileName);
136+
}
137+
138+
directoryExists(directoryName: string): boolean {
139+
return (directoryName in this._directories) || this._delegate.directoryExists(directoryName);
140+
}
141+
142+
getDirectories(path: string): string[] {
143+
return this._delegate.getDirectories(path);
144+
}
145+
146+
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: OnErrorFn) {
147+
if (!(fileName in this._cachedFiles)) {
148+
return this._delegate.getSourceFile(fileName, languageVersion, onError);
149+
}
150+
151+
return this._cachedFiles[fileName].getSourceFile(languageVersion, this._setParentNodes);
152+
}
153+
154+
getCancellationToken() {
155+
return this._delegate.getCancellationToken();
156+
}
157+
158+
getDefaultLibFileName(options: ts.CompilerOptions) {
159+
return this._delegate.getDefaultLibFileName(options);
160+
}
161+
162+
writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: OnErrorFn) {
163+
this._setFileContent(fileName, data);
164+
}
165+
166+
getCurrentDirectory(): string {
167+
return this._delegate.getCurrentDirectory();
168+
}
169+
170+
getCanonicalFileName(fileName: string): string {
171+
return this._delegate.getCanonicalFileName(fileName);
172+
}
173+
174+
useCaseSensitiveFileNames(): boolean {
175+
return this._delegate.useCaseSensitiveFileNames();
176+
}
177+
178+
getNewLine(): string {
179+
return this._delegate.getNewLine();
180+
}
181+
}

packages/webpack/src/plugin.ts

+88-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,81 @@ 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 != 1
107+
|| bootstrap[0].arguments[0].kind !== ts.SyntaxKind.Identifier) {
108+
throw new Error('Tried to find bootstrap code, but could not. Specify either '
109+
+ 'statically analyzable bootstrap code or pass in an entryModule '
110+
+ 'to the plugins options.');
111+
}
112+
113+
const bootstrapSymbolName = (bootstrap[0].arguments[0] as ts.Identifier).text;
114+
115+
// We found the bootstrap variable, now we just need to get where it's imported.
116+
const imports = _findNodes(source, source, ts.SyntaxKind.ImportDeclaration, false)
117+
.map(node => node as ts.ImportDeclaration);
118+
119+
for (const decl of imports) {
120+
if (!decl.importClause || !decl.moduleSpecifier) {
121+
continue;
122+
}
123+
if (decl.moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) {
124+
continue;
125+
}
126+
127+
const module = (decl.moduleSpecifier as ts.StringLiteral).text;
128+
129+
if (decl.importClause.namedBindings.kind == ts.SyntaxKind.NamespaceImport) {
130+
const binding = decl.importClause.namedBindings as ts.NamespaceImport;
131+
if (binding.name.text == bootstrapSymbolName) {
132+
// This is a default export.
133+
return module;
134+
}
135+
} else if (decl.importClause.namedBindings.kind == ts.SyntaxKind.NamedImports) {
136+
const binding = decl.importClause.namedBindings as ts.NamedImports;
137+
for (const specifier of binding.elements) {
138+
if (specifier.name.text == bootstrapSymbolName) {
139+
return `${path.resolve(path.dirname(mainPath), module)}#${bootstrapSymbolName}`;
140+
}
141+
}
142+
}
143+
}
144+
145+
// shrug... something bad happened and we couldn't find the import statement.
146+
throw new Error('Tried to find bootstrap code, but could not. Specify either '
147+
+ 'statically analyzable bootstrap code or pass in an entryModule '
148+
+ 'to the plugins options.');
149+
}
150+
74151
// registration hook for webpack plugin
75152
apply(compiler: any) {
76153
this.compiler = compiler;
@@ -110,6 +187,15 @@ export class NgcWebpackPlugin {
110187
compilation._ngToolsWebpackPluginInstance = null;
111188
cb();
112189
});
190+
191+
// Virtual file system.
192+
compiler.resolvers.normal.plugin('resolve', (request: any, cb?: () => void) => {
193+
// populate the file system cache with the virtual module
194+
this.compilerHost.populateWebpackResolver(compiler.resolvers.normal);
195+
if (cb) {
196+
cb();
197+
}
198+
});
113199
}
114200

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

0 commit comments

Comments
 (0)