Skip to content

Commit e1d9930

Browse files
hanslDanny Blue
authored and
Danny Blue
committed
feat(aot): creating files in a virtual fs. (angular#2464)
In addition, reading and using AST on main.ts to figure out the entry module, if not specified.
1 parent 360274f commit e1d9930

File tree

7 files changed

+430
-30
lines changed

7 files changed

+430
-30
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ export const getWebpackAotConfigPartial = function(projectRoot: string, appConfi
6262
new NgcWebpackPlugin({
6363
project: path.resolve(projectRoot, appConfig.root, appConfig.tsconfig),
6464
baseDir: path.resolve(projectRoot, ''),
65-
entryModule: path.join(projectRoot, appConfig.root, 'app/app.module#AppModule'),
66-
genDir: path.join(projectRoot, appConfig.outDir, 'ngfactory')
65+
main: path.join(projectRoot, appConfig.root, appConfig.main),
66+
genDir: path.resolve(projectRoot, '')
6767
}),
6868
]
6969
};

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+
}
+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import * as fs from 'fs';
2+
import {dirname, join, 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+
const binding = decl.exportClause as ts.NamedExports;
56+
for (const specifier of binding.elements) {
57+
if (specifier.name.text == symbolName) {
58+
// If it's a directory, load its index and recursively lookup.
59+
if (fs.statSync(module).isDirectory()) {
60+
const indexModule = join(module, 'index.ts');
61+
if (fs.existsSync(indexModule)) {
62+
const maybeModule = _recursiveSymbolExportLookup(
63+
indexModule, _createSource(indexModule), symbolName);
64+
if (maybeModule) {
65+
return maybeModule;
66+
}
67+
}
68+
}
69+
70+
// Create the source and verify that the symbol is at least a class.
71+
const source = _createSource(module);
72+
const hasSymbol = _findNodes(source, source, ts.SyntaxKind.ClassDeclaration)
73+
.some((cd: ts.ClassDeclaration) => {
74+
return cd.name && cd.name.text == symbolName;
75+
});
76+
77+
if (hasSymbol) {
78+
return module;
79+
} else {
80+
return null;
81+
}
82+
}
83+
}
84+
}
85+
86+
return null;
87+
}
88+
89+
function _symbolImportLookup(sourcePath: string,
90+
sourceFile: ts.SourceFile,
91+
symbolName: string): string | null {
92+
// We found the bootstrap variable, now we just need to get where it's imported.
93+
const imports = _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ImportDeclaration, false)
94+
.map(node => node as ts.ImportDeclaration);
95+
96+
for (const decl of imports) {
97+
if (!decl.importClause || !decl.moduleSpecifier) {
98+
continue;
99+
}
100+
if (decl.moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) {
101+
continue;
102+
}
103+
104+
const module = resolve(dirname(sourcePath), (decl.moduleSpecifier as ts.StringLiteral).text);
105+
106+
if (decl.importClause.namedBindings.kind == ts.SyntaxKind.NamespaceImport) {
107+
const binding = decl.importClause.namedBindings as ts.NamespaceImport;
108+
if (binding.name.text == symbolName) {
109+
// This is a default export.
110+
return module;
111+
}
112+
} else if (decl.importClause.namedBindings.kind == ts.SyntaxKind.NamedImports) {
113+
const binding = decl.importClause.namedBindings as ts.NamedImports;
114+
for (const specifier of binding.elements) {
115+
if (specifier.name.text == symbolName) {
116+
// If it's a directory, load its index and recursively lookup.
117+
if (fs.statSync(module).isDirectory()) {
118+
const indexModule = join(module, 'index.ts');
119+
if (fs.existsSync(indexModule)) {
120+
const maybeModule = _recursiveSymbolExportLookup(
121+
indexModule, _createSource(indexModule), symbolName);
122+
if (maybeModule) {
123+
return maybeModule;
124+
}
125+
}
126+
}
127+
128+
// Create the source and verify that the symbol is at least a class.
129+
const source = _createSource(module);
130+
const hasSymbol = _findNodes(source, source, ts.SyntaxKind.ClassDeclaration)
131+
.some((cd: ts.ClassDeclaration) => {
132+
return cd.name && cd.name.text == symbolName;
133+
});
134+
135+
if (hasSymbol) {
136+
return module;
137+
} else {
138+
return null;
139+
}
140+
}
141+
}
142+
}
143+
}
144+
return null;
145+
}
146+
147+
148+
export function resolveEntryModuleFromMain(mainPath: string) {
149+
const source = _createSource(mainPath);
150+
151+
const bootstrap = _findNodes(source, source, ts.SyntaxKind.CallExpression, false)
152+
.map(node => node as ts.CallExpression)
153+
.filter(call => {
154+
const access = call.expression as ts.PropertyAccessExpression;
155+
return access.kind == ts.SyntaxKind.PropertyAccessExpression
156+
&& access.name.kind == ts.SyntaxKind.Identifier
157+
&& (access.name.text == 'bootstrapModule'
158+
|| access.name.text == 'bootstrapModuleFactory');
159+
});
160+
161+
if (bootstrap.length != 1
162+
|| bootstrap[0].arguments[0].kind !== ts.SyntaxKind.Identifier) {
163+
throw new Error('Tried to find bootstrap code, but could not. Specify either '
164+
+ 'statically analyzable bootstrap code or pass in an entryModule '
165+
+ 'to the plugins options.');
166+
}
167+
168+
const bootstrapSymbolName = (bootstrap[0].arguments[0] as ts.Identifier).text;
169+
const module = _symbolImportLookup(mainPath, source, bootstrapSymbolName);
170+
if (module) {
171+
return `${resolve(dirname(mainPath), module)}#${bootstrapSymbolName}`;
172+
}
173+
174+
// shrug... something bad happened and we couldn't find the import statement.
175+
throw new Error('Tried to find bootstrap code, but could not. Specify either '
176+
+ 'statically analyzable bootstrap code or pass in an entryModule '
177+
+ 'to the plugins options.');
178+
}

0 commit comments

Comments
 (0)