Skip to content

Commit f263bd9

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 f263bd9

File tree

7 files changed

+438
-30
lines changed

7 files changed

+438
-30
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ 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'),
59-
genDir: path.join(projectRoot, appConfig.outDir, 'ngfactory')
58+
main: path.join(projectRoot, appConfig.root, appConfig.main),
59+
genDir: path.resolve(projectRoot, '')
6060
}),
6161
]
6262
};

packages/webpack/src/compiler_host.ts

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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 _files: {[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._files[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._files)) {
113+
const stats = this._files[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 dirs = this.getDirectories(path);
120+
const files = this.getFiles(path);
121+
fs._statStorage.data[path] = [null, stats];
122+
fs._readdirStorage.data[path] = [null, files.concat(dirs)];
123+
}
124+
}
125+
126+
fileExists(fileName: string): boolean {
127+
return fileName in this._files || this._delegate.fileExists(fileName);
128+
}
129+
130+
readFile(fileName: string): string {
131+
return (fileName in this._files)
132+
? this._files[fileName].content
133+
: this._delegate.readFile(fileName);
134+
}
135+
136+
directoryExists(directoryName: string): boolean {
137+
return (directoryName in this._directories) || this._delegate.directoryExists(directoryName);
138+
}
139+
140+
getFiles(path: string): string[] {
141+
return Object.keys(this._files)
142+
.filter(fileName => dirname(fileName) == path)
143+
.map(path => basename(path));
144+
}
145+
146+
getDirectories(path: string): string[] {
147+
const subdirs = Object.keys(this._directories)
148+
.filter(fileName => dirname(fileName) == path)
149+
.map(path => basename(path));
150+
151+
let delegated: string[];
152+
try {
153+
delegated = this._delegate.getDirectories(path);
154+
} catch (e) {
155+
delegated = [];
156+
}
157+
return delegated.concat(subdirs);
158+
}
159+
160+
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: OnErrorFn) {
161+
if (!(fileName in this._files)) {
162+
return this._delegate.getSourceFile(fileName, languageVersion, onError);
163+
}
164+
165+
return this._files[fileName].getSourceFile(languageVersion, this._setParentNodes);
166+
}
167+
168+
getCancellationToken() {
169+
return this._delegate.getCancellationToken();
170+
}
171+
172+
getDefaultLibFileName(options: ts.CompilerOptions) {
173+
return this._delegate.getDefaultLibFileName(options);
174+
}
175+
176+
writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: OnErrorFn) {
177+
this._setFileContent(fileName, data);
178+
}
179+
180+
getCurrentDirectory(): string {
181+
return this._delegate.getCurrentDirectory();
182+
}
183+
184+
getCanonicalFileName(fileName: string): string {
185+
return this._delegate.getCanonicalFileName(fileName);
186+
}
187+
188+
useCaseSensitiveFileNames(): boolean {
189+
return this._delegate.useCaseSensitiveFileNames();
190+
}
191+
192+
getNewLine(): string {
193+
return this._delegate.getNewLine();
194+
}
195+
}
+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import * as fs from 'fs';
2+
import {dirname, join, relative, resolve} from 'path';
3+
import * as ts from 'typescript';
4+
5+
6+
function _createSource(path: string): ts.SourceFile {
7+
return ts.createSourceFile(path, fs.readFileSync(path, 'utf-8'), ts.ScriptTarget.Latest);
8+
}
9+
10+
function _findNodes(sourceFile: ts.SourceFile, node: ts.Node, kind: ts.SyntaxKind,
11+
keepGoing = false): ts.Node[] {
12+
if (node.kind == kind && !keepGoing) {
13+
return [node];
14+
}
15+
16+
return node.getChildren(sourceFile).reduce((result, n) => {
17+
return result.concat(_findNodes(sourceFile, n, kind, keepGoing));
18+
}, node.kind == kind ? [node] : []);
19+
}
20+
21+
function _recursiveSymbolExportLookup(sourcePath: string,
22+
sourceFile: ts.SourceFile,
23+
symbolName: string): string | null {
24+
// Check this file.
25+
const hasSymbol = _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ClassDeclaration)
26+
.some((cd: ts.ClassDeclaration) => {
27+
return cd.name && cd.name.text == symbolName;
28+
});
29+
if (hasSymbol) {
30+
return sourcePath;
31+
}
32+
33+
// We found the bootstrap variable, now we just need to get where it's imported.
34+
const exports = _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ExportDeclaration, false)
35+
.map(node => node as ts.ExportDeclaration);
36+
37+
for (const decl of exports) {
38+
if (!decl.moduleSpecifier || decl.moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) {
39+
continue;
40+
}
41+
42+
const module = resolve(dirname(sourcePath), (decl.moduleSpecifier as ts.StringLiteral).text);
43+
if (!decl.exportClause) {
44+
const moduleTs = module + '.ts';
45+
if (fs.existsSync(moduleTs)) {
46+
const moduleSource = _createSource(moduleTs);
47+
const maybeModule = _recursiveSymbolExportLookup(module, moduleSource, symbolName);
48+
if (maybeModule) {
49+
return maybeModule;
50+
}
51+
}
52+
continue;
53+
}
54+
55+
if (decl.exportClause.namedBindings.kind == ts.SyntaxKind.NamespaceImport) {
56+
const binding = decl.exportClause.namedBindings as ts.NamespaceImport;
57+
if (binding.name.text == symbolName) {
58+
// This is a default export.
59+
return module;
60+
}
61+
} else if (decl.exportClause.namedBindings.kind == ts.SyntaxKind.NamedImports) {
62+
const binding = decl.exportClause.namedBindings as ts.NamedImports;
63+
for (const specifier of binding.elements) {
64+
if (specifier.name.text == symbolName) {
65+
// If it's a directory, load its index and recursively lookup.
66+
if (fs.statSync(module).isDirectory()) {
67+
const indexModule = join(module, 'index.ts');
68+
if (fs.existsSync(indexModule)) {
69+
const maybeModule = _recursiveSymbolExportLookup(
70+
indexModule, _createSource(indexModule), symbolName);
71+
if (maybeModule) {
72+
return maybeModule;
73+
}
74+
}
75+
}
76+
77+
// Create the source and verify that the symbol is at least a class.
78+
const source = _createSource(module);
79+
const hasSymbol = _findNodes(source, source, ts.SyntaxKind.ClassDeclaration)
80+
.some((cd: ts.ClassDeclaration) => {
81+
return cd.name && cd.name.text == symbolName;
82+
});
83+
84+
if (hasSymbol) {
85+
return module;
86+
} else {
87+
return null;
88+
}
89+
}
90+
}
91+
}
92+
}
93+
94+
return null;
95+
}
96+
97+
function _symbolImportLookup(sourcePath: string,
98+
sourceFile: ts.SourceFile,
99+
symbolName: string): string | null {
100+
// We found the bootstrap variable, now we just need to get where it's imported.
101+
const imports = _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ImportDeclaration, false)
102+
.map(node => node as ts.ImportDeclaration);
103+
104+
for (const decl of imports) {
105+
if (!decl.importClause || !decl.moduleSpecifier) {
106+
continue;
107+
}
108+
if (decl.moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) {
109+
continue;
110+
}
111+
112+
const module = resolve(dirname(sourcePath), (decl.moduleSpecifier as ts.StringLiteral).text);
113+
114+
if (decl.importClause.namedBindings.kind == ts.SyntaxKind.NamespaceImport) {
115+
const binding = decl.importClause.namedBindings as ts.NamespaceImport;
116+
if (binding.name.text == symbolName) {
117+
// This is a default export.
118+
return module;
119+
}
120+
} else if (decl.importClause.namedBindings.kind == ts.SyntaxKind.NamedImports) {
121+
const binding = decl.importClause.namedBindings as ts.NamedImports;
122+
for (const specifier of binding.elements) {
123+
if (specifier.name.text == symbolName) {
124+
// If it's a directory, load its index and recursively lookup.
125+
if (fs.statSync(module).isDirectory()) {
126+
const indexModule = join(module, 'index.ts');
127+
if (fs.existsSync(indexModule)) {
128+
const maybeModule = _recursiveSymbolExportLookup(
129+
indexModule, _createSource(indexModule), symbolName);
130+
if (maybeModule) {
131+
return maybeModule;
132+
}
133+
}
134+
}
135+
136+
// Create the source and verify that the symbol is at least a class.
137+
const source = _createSource(module);
138+
const hasSymbol = _findNodes(source, source, ts.SyntaxKind.ClassDeclaration)
139+
.some((cd: ts.ClassDeclaration) => {
140+
return cd.name && cd.name.text == symbolName;
141+
});
142+
143+
if (hasSymbol) {
144+
return module;
145+
} else {
146+
return null;
147+
}
148+
}
149+
}
150+
}
151+
}
152+
return null;
153+
}
154+
155+
156+
export function resolveEntryModuleFromMain(mainPath: string) {
157+
const source = _createSource(mainPath);
158+
159+
const bootstrap = _findNodes(source, source, ts.SyntaxKind.CallExpression, false)
160+
.map(node => node as ts.CallExpression)
161+
.filter(call => {
162+
const access = call.expression as ts.PropertyAccessExpression;
163+
return access.kind == ts.SyntaxKind.PropertyAccessExpression
164+
&& access.name.kind == ts.SyntaxKind.Identifier
165+
&& (access.name.text == 'bootstrapModule'
166+
|| access.name.text == 'bootstrapModuleFactory');
167+
});
168+
169+
if (bootstrap.length != 1
170+
|| bootstrap[0].arguments[0].kind !== ts.SyntaxKind.Identifier) {
171+
throw new Error('Tried to find bootstrap code, but could not. Specify either '
172+
+ 'statically analyzable bootstrap code or pass in an entryModule '
173+
+ 'to the plugins options.');
174+
}
175+
176+
const bootstrapSymbolName = (bootstrap[0].arguments[0] as ts.Identifier).text;
177+
const module = _symbolImportLookup(mainPath, source, bootstrapSymbolName);
178+
if (module) {
179+
return `${resolve(dirname(mainPath), module)}#${bootstrapSymbolName}`;
180+
}
181+
182+
// shrug... something bad happened and we couldn't find the import statement.
183+
throw new Error('Tried to find bootstrap code, but could not. Specify either '
184+
+ 'statically analyzable bootstrap code or pass in an entryModule '
185+
+ 'to the plugins options.');
186+
}

0 commit comments

Comments
 (0)