forked from angular/angular-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
7 changed files
with
430 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.'); | ||
} |
Oops, something went wrong.