Skip to content

Commit

Permalink
feat(aot): creating files in a virtual fs. (angular#2464)
Browse files Browse the repository at this point in the history
In addition, reading and using AST on main.ts to figure out the entry module, if not specified.
  • Loading branch information
hansl authored Oct 2, 2016
1 parent 23146f2 commit 790a1b4
Show file tree
Hide file tree
Showing 7 changed files with 430 additions and 30 deletions.
4 changes: 2 additions & 2 deletions packages/angular-cli/models/webpack-build-typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ export const getWebpackAotConfigPartial = function(projectRoot: string, appConfi
new NgcWebpackPlugin({
project: path.resolve(projectRoot, appConfig.root, appConfig.tsconfig),
baseDir: path.resolve(projectRoot, ''),
entryModule: path.join(projectRoot, appConfig.root, 'app/app.module#AppModule'),
genDir: path.join(projectRoot, appConfig.outDir, 'ngfactory')
main: path.join(projectRoot, appConfig.root, appConfig.main),
genDir: path.resolve(projectRoot, '')
}),
]
};
Expand Down
195 changes: 195 additions & 0 deletions packages/webpack/src/compiler_host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import * as ts from 'typescript';
import {basename, dirname} from 'path';
import * as fs from 'fs';


export interface OnErrorFn {
(message: string): void;
}


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


export class VirtualStats implements fs.Stats {
protected _ctime = new Date();
protected _mtime = new Date();
protected _atime = new Date();
protected _btime = new Date();
protected _dev = dev;
protected _ino = Math.floor(Math.random() * 100000);
protected _mode = parseInt('777', 8); // RWX for everyone.
protected _uid = process.env['UID'] || 0;
protected _gid = process.env['GID'] || 0;

constructor(protected _path: string) {}

isFile() { return false; }
isDirectory() { return false; }
isBlockDevice() { return false; }
isCharacterDevice() { return false; }
isSymbolicLink() { return false; }
isFIFO() { return false; }
isSocket() { return false; }

get dev() { return this._dev; }
get ino() { return this._ino; }
get mode() { return this._mode; }
get nlink() { return 1; } // Default to 1 hard link.
get uid() { return this._uid; }
get gid() { return this._gid; }
get rdev() { return 0; }
get size() { return 0; }
get blksize() { return 512; }
get blocks() { return Math.ceil(this.size / this.blksize); }
get atime() { return this._atime; }
get mtime() { return this._mtime; }
get ctime() { return this._ctime; }
get birthtime() { return this._btime; }
}

export class VirtualDirStats extends VirtualStats {
constructor(_fileName: string) {
super(_fileName);
}

isDirectory() { return true; }

get size() { return 1024; }
}

export class VirtualFileStats extends VirtualStats {
private _sourceFile: ts.SourceFile;
constructor(_fileName: string, private _content: string) {
super(_fileName);
}

get content() { return this._content; }
set content(v: string) {
this._content = v;
this._mtime = new Date();
}
getSourceFile(languageVersion: ts.ScriptTarget, setParentNodes: boolean) {
if (!this._sourceFile) {
this._sourceFile = ts.createSourceFile(
this._path,
this._content,
languageVersion,
setParentNodes);
}

return this._sourceFile;
}

isFile() { return true; }

get size() { return this._content.length; }
}


export class WebpackCompilerHost implements ts.CompilerHost {
private _delegate: ts.CompilerHost;
private _files: {[path: string]: VirtualFileStats} = Object.create(null);
private _directories: {[path: string]: VirtualDirStats} = Object.create(null);

constructor(private _options: ts.CompilerOptions, private _setParentNodes = true) {
this._delegate = ts.createCompilerHost(this._options, this._setParentNodes);
}

private _setFileContent(fileName: string, content: string) {
this._files[fileName] = new VirtualFileStats(fileName, content);

let p = dirname(fileName);
while (p && !this._directories[p]) {
this._directories[p] = new VirtualDirStats(p);
p = dirname(p);
}
}

populateWebpackResolver(resolver: any) {
const fs = resolver.fileSystem;

for (const fileName of Object.keys(this._files)) {
const stats = this._files[fileName];
fs._statStorage.data[fileName] = [null, stats];
fs._readFileStorage.data[fileName] = [null, stats.content];
}
for (const path of Object.keys(this._directories)) {
const stats = this._directories[path];
const dirs = this.getDirectories(path);
const files = this.getFiles(path);
fs._statStorage.data[path] = [null, stats];
fs._readdirStorage.data[path] = [null, files.concat(dirs)];
}
}

fileExists(fileName: string): boolean {
return fileName in this._files || this._delegate.fileExists(fileName);
}

readFile(fileName: string): string {
return (fileName in this._files)
? this._files[fileName].content
: this._delegate.readFile(fileName);
}

directoryExists(directoryName: string): boolean {
return (directoryName in this._directories) || this._delegate.directoryExists(directoryName);
}

getFiles(path: string): string[] {
return Object.keys(this._files)
.filter(fileName => dirname(fileName) == path)
.map(path => basename(path));
}

getDirectories(path: string): string[] {
const subdirs = Object.keys(this._directories)
.filter(fileName => dirname(fileName) == path)
.map(path => basename(path));

let delegated: string[];
try {
delegated = this._delegate.getDirectories(path);
} catch (e) {
delegated = [];
}
return delegated.concat(subdirs);
}

getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: OnErrorFn) {
if (!(fileName in this._files)) {
return this._delegate.getSourceFile(fileName, languageVersion, onError);
}

return this._files[fileName].getSourceFile(languageVersion, this._setParentNodes);
}

getCancellationToken() {
return this._delegate.getCancellationToken();
}

getDefaultLibFileName(options: ts.CompilerOptions) {
return this._delegate.getDefaultLibFileName(options);
}

writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: OnErrorFn) {
this._setFileContent(fileName, data);
}

getCurrentDirectory(): string {
return this._delegate.getCurrentDirectory();
}

getCanonicalFileName(fileName: string): string {
return this._delegate.getCanonicalFileName(fileName);
}

useCaseSensitiveFileNames(): boolean {
return this._delegate.useCaseSensitiveFileNames();
}

getNewLine(): string {
return this._delegate.getNewLine();
}
}
178 changes: 178 additions & 0 deletions packages/webpack/src/entry_resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import * as fs from 'fs';
import {dirname, join, resolve} from 'path';
import * as ts from 'typescript';


function _createSource(path: string): ts.SourceFile {
return ts.createSourceFile(path, fs.readFileSync(path, 'utf-8'), ts.ScriptTarget.Latest);
}

function _findNodes(sourceFile: ts.SourceFile, node: ts.Node, kind: ts.SyntaxKind,
keepGoing = false): ts.Node[] {
if (node.kind == kind && !keepGoing) {
return [node];
}

return node.getChildren(sourceFile).reduce((result, n) => {
return result.concat(_findNodes(sourceFile, n, kind, keepGoing));
}, node.kind == kind ? [node] : []);
}

function _recursiveSymbolExportLookup(sourcePath: string,
sourceFile: ts.SourceFile,
symbolName: string): string | null {
// Check this file.
const hasSymbol = _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ClassDeclaration)
.some((cd: ts.ClassDeclaration) => {
return cd.name && cd.name.text == symbolName;
});
if (hasSymbol) {
return sourcePath;
}

// We found the bootstrap variable, now we just need to get where it's imported.
const exports = _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ExportDeclaration, false)
.map(node => node as ts.ExportDeclaration);

for (const decl of exports) {
if (!decl.moduleSpecifier || decl.moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) {
continue;
}

const module = resolve(dirname(sourcePath), (decl.moduleSpecifier as ts.StringLiteral).text);
if (!decl.exportClause) {
const moduleTs = module + '.ts';
if (fs.existsSync(moduleTs)) {
const moduleSource = _createSource(moduleTs);
const maybeModule = _recursiveSymbolExportLookup(module, moduleSource, symbolName);
if (maybeModule) {
return maybeModule;
}
}
continue;
}

const binding = decl.exportClause as ts.NamedExports;
for (const specifier of binding.elements) {
if (specifier.name.text == symbolName) {
// If it's a directory, load its index and recursively lookup.
if (fs.statSync(module).isDirectory()) {
const indexModule = join(module, 'index.ts');
if (fs.existsSync(indexModule)) {
const maybeModule = _recursiveSymbolExportLookup(
indexModule, _createSource(indexModule), symbolName);
if (maybeModule) {
return maybeModule;
}
}
}

// Create the source and verify that the symbol is at least a class.
const source = _createSource(module);
const hasSymbol = _findNodes(source, source, ts.SyntaxKind.ClassDeclaration)
.some((cd: ts.ClassDeclaration) => {
return cd.name && cd.name.text == symbolName;
});

if (hasSymbol) {
return module;
} else {
return null;
}
}
}
}

return null;
}

function _symbolImportLookup(sourcePath: string,
sourceFile: ts.SourceFile,
symbolName: string): string | null {
// We found the bootstrap variable, now we just need to get where it's imported.
const imports = _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ImportDeclaration, false)
.map(node => node as ts.ImportDeclaration);

for (const decl of imports) {
if (!decl.importClause || !decl.moduleSpecifier) {
continue;
}
if (decl.moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) {
continue;
}

const module = resolve(dirname(sourcePath), (decl.moduleSpecifier as ts.StringLiteral).text);

if (decl.importClause.namedBindings.kind == ts.SyntaxKind.NamespaceImport) {
const binding = decl.importClause.namedBindings as ts.NamespaceImport;
if (binding.name.text == symbolName) {
// This is a default export.
return module;
}
} else if (decl.importClause.namedBindings.kind == ts.SyntaxKind.NamedImports) {
const binding = decl.importClause.namedBindings as ts.NamedImports;
for (const specifier of binding.elements) {
if (specifier.name.text == symbolName) {
// If it's a directory, load its index and recursively lookup.
if (fs.statSync(module).isDirectory()) {
const indexModule = join(module, 'index.ts');
if (fs.existsSync(indexModule)) {
const maybeModule = _recursiveSymbolExportLookup(
indexModule, _createSource(indexModule), symbolName);
if (maybeModule) {
return maybeModule;
}
}
}

// Create the source and verify that the symbol is at least a class.
const source = _createSource(module);
const hasSymbol = _findNodes(source, source, ts.SyntaxKind.ClassDeclaration)
.some((cd: ts.ClassDeclaration) => {
return cd.name && cd.name.text == symbolName;
});

if (hasSymbol) {
return module;
} else {
return null;
}
}
}
}
}
return null;
}


export function resolveEntryModuleFromMain(mainPath: string) {
const source = _createSource(mainPath);

const bootstrap = _findNodes(source, source, ts.SyntaxKind.CallExpression, false)
.map(node => node as ts.CallExpression)
.filter(call => {
const access = call.expression as ts.PropertyAccessExpression;
return access.kind == ts.SyntaxKind.PropertyAccessExpression
&& access.name.kind == ts.SyntaxKind.Identifier
&& (access.name.text == 'bootstrapModule'
|| access.name.text == 'bootstrapModuleFactory');
});

if (bootstrap.length != 1
|| bootstrap[0].arguments[0].kind !== ts.SyntaxKind.Identifier) {
throw new Error('Tried to find bootstrap code, but could not. Specify either '
+ 'statically analyzable bootstrap code or pass in an entryModule '
+ 'to the plugins options.');
}

const bootstrapSymbolName = (bootstrap[0].arguments[0] as ts.Identifier).text;
const module = _symbolImportLookup(mainPath, source, bootstrapSymbolName);
if (module) {
return `${resolve(dirname(mainPath), module)}#${bootstrapSymbolName}`;
}

// shrug... something bad happened and we couldn't find the import statement.
throw new Error('Tried to find bootstrap code, but could not. Specify either '
+ 'statically analyzable bootstrap code or pass in an entryModule '
+ 'to the plugins options.');
}
Loading

0 comments on commit 790a1b4

Please sign in to comment.