Skip to content

Commit

Permalink
fix(@ngtools/webpack): fixed path resolution for entry modules and la…
Browse files Browse the repository at this point in the history
…zy routes (#3332)
  • Loading branch information
hansl authored Dec 2, 2016
1 parent 53ab4df commit 45d5154
Show file tree
Hide file tree
Showing 18 changed files with 431 additions and 127 deletions.
61 changes: 50 additions & 11 deletions packages/webpack/src/compiler_host.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as ts from 'typescript';
import {basename, dirname} from 'path';
import {basename, dirname, join} from 'path';
import * as fs from 'fs';


Expand Down Expand Up @@ -93,8 +93,28 @@ export class WebpackCompilerHost implements ts.CompilerHost {
private _directories: {[path: string]: VirtualDirStats} = Object.create(null);
private _changed = false;

constructor(private _options: ts.CompilerOptions, private _setParentNodes = true) {
private _basePath: string;
private _setParentNodes: boolean;

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

private _normalizePath(path: string) {
return path.replace(/\\/g, '/');
}

private _resolve(path: string) {
path = this._normalizePath(path);
if (path[0] == '.') {
return join(this.getCurrentDirectory(), path);
} else if (path[0] == '/' || path.match(/^\w:\//)) {
return path;
} else {
return join(this._basePath, path);
}
}

private _setFileContent(fileName: string, content: string) {
Expand All @@ -115,15 +135,20 @@ export class WebpackCompilerHost implements ts.CompilerHost {
return;
}

const isWindows = process.platform.startsWith('win');
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];
// If we're on windows, we need to populate with the proper path separator.
const path = isWindows ? fileName.replace(/\//g, '\\') : fileName;
fs._statStorage.data[path] = [null, stats];
fs._readFileStorage.data[path] = [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);
for (const dirName of Object.keys(this._directories)) {
const stats = this._directories[dirName];
const dirs = this.getDirectories(dirName);
const files = this.getFiles(dirName);
// If we're on windows, we need to populate with the proper path separator.
const path = isWindows ? dirName.replace(/\//g, '\\') : dirName;
fs._statStorage.data[path] = [null, stats];
fs._readdirStorage.data[path] = [null, files.concat(dirs)];
}
Expand All @@ -132,26 +157,31 @@ export class WebpackCompilerHost implements ts.CompilerHost {
}

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

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

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

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

getDirectories(path: string): string[] {
path = this._resolve(path);
const subdirs = Object.keys(this._directories)
.filter(fileName => dirname(fileName) == path)
.map(path => basename(path));
Expand All @@ -166,6 +196,8 @@ export class WebpackCompilerHost implements ts.CompilerHost {
}

getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: OnErrorFn) {
fileName = this._resolve(fileName);

if (!(fileName in this._files)) {
return this._delegate.getSourceFile(fileName, languageVersion, onError);
}
Expand All @@ -181,15 +213,22 @@ export class WebpackCompilerHost implements ts.CompilerHost {
return this._delegate.getDefaultLibFileName(options);
}

writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: OnErrorFn) {
this._setFileContent(fileName, data);
// This is due to typescript CompilerHost interface being weird on writeFile. This shuts down
// typings in WebStorm.
get writeFile() {
return (fileName: string, data: string, writeByteOrderMark: boolean,
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]): void => {
fileName = this._resolve(fileName);
this._setFileContent(fileName, data);
};
}

getCurrentDirectory(): string {
return this._delegate.getCurrentDirectory();
return this._basePath !== null ? this._basePath : this._delegate.getCurrentDirectory();
}

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

Expand Down
126 changes: 54 additions & 72 deletions packages/webpack/src/entry_resolver.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,45 @@
import * as fs from 'fs';
import {dirname, join, resolve} from 'path';
import {join} from 'path';
import * as ts from 'typescript';

import {TypeScriptFileRefactor} from './refactor';

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 {
function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor,
symbolName: string,
host: ts.CompilerHost,
program: ts.Program): string | null {
// Check this file.
const hasSymbol = _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ClassDeclaration)
const hasSymbol = refactor.findAstNodes(null, ts.SyntaxKind.ClassDeclaration)
.some((cd: ts.ClassDeclaration) => {
return cd.name && cd.name.text == symbolName;
});
if (hasSymbol) {
return sourcePath;
return refactor.fileName;
}

// We found the bootstrap variable, now we just need to get where it's imported.
const exports = _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ExportDeclaration, false)
const exports = refactor.findAstNodes(null, ts.SyntaxKind.ExportDeclaration)
.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);
const modulePath = (decl.moduleSpecifier as ts.StringLiteral).text;
const resolvedModule = ts.resolveModuleName(
modulePath, refactor.fileName, program.getCompilerOptions(), host);
if (!resolvedModule.resolvedModule || !resolvedModule.resolvedModule.resolvedFileName) {
return null;
}

const module = resolvedModule.resolvedModule.resolvedFileName;
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;
}
const moduleRefactor = new TypeScriptFileRefactor(module, host, program);
const maybeModule = _recursiveSymbolExportLookup(moduleRefactor, symbolName, host, program);
if (maybeModule) {
return maybeModule;
}
continue;
}
Expand All @@ -59,25 +51,24 @@ function _recursiveSymbolExportLookup(sourcePath: string,
if (fs.statSync(module).isDirectory()) {
const indexModule = join(module, 'index.ts');
if (fs.existsSync(indexModule)) {
const indexRefactor = new TypeScriptFileRefactor(indexModule, host, program);
const maybeModule = _recursiveSymbolExportLookup(
indexModule, _createSource(indexModule), symbolName);
indexRefactor, symbolName, host, program);
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)
const source = new TypeScriptFileRefactor(module, host, program);
const hasSymbol = source.findAstNodes(null, ts.SyntaxKind.ClassDeclaration)
.some((cd: ts.ClassDeclaration) => {
return cd.name && cd.name.text == symbolName;
});

if (hasSymbol) {
return module;
} else {
return null;
}
}
}
Expand All @@ -86,11 +77,12 @@ function _recursiveSymbolExportLookup(sourcePath: string,
return null;
}

function _symbolImportLookup(sourcePath: string,
sourceFile: ts.SourceFile,
symbolName: string): string | null {
function _symbolImportLookup(refactor: TypeScriptFileRefactor,
symbolName: string,
host: ts.CompilerHost,
program: ts.Program): 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)
const imports = refactor.findAstNodes(null, ts.SyntaxKind.ImportDeclaration)
.map(node => node as ts.ImportDeclaration);

for (const decl of imports) {
Expand All @@ -101,8 +93,14 @@ function _symbolImportLookup(sourcePath: string,
continue;
}

const module = resolve(dirname(sourcePath), (decl.moduleSpecifier as ts.StringLiteral).text);
const resolvedModule = ts.resolveModuleName(
(decl.moduleSpecifier as ts.StringLiteral).text,
refactor.fileName, program.getCompilerOptions(), host);
if (!resolvedModule.resolvedModule || !resolvedModule.resolvedModule.resolvedFileName) {
return null;
}

const module = resolvedModule.resolvedModule.resolvedFileName;
if (decl.importClause.namedBindings.kind == ts.SyntaxKind.NamespaceImport) {
const binding = decl.importClause.namedBindings as ts.NamespaceImport;
if (binding.name.text == symbolName) {
Expand All @@ -113,29 +111,11 @@ function _symbolImportLookup(sourcePath: string,
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;
// Create the source and recursively lookup the import.
const source = new TypeScriptFileRefactor(module, host, program);
const maybeModule = _recursiveSymbolExportLookup(source, symbolName, host, program);
if (maybeModule) {
return maybeModule;
}
}
}
Expand All @@ -145,30 +125,32 @@ function _symbolImportLookup(sourcePath: string,
}


export function resolveEntryModuleFromMain(mainPath: string) {
const source = _createSource(mainPath);
export function resolveEntryModuleFromMain(mainPath: string,
host: ts.CompilerHost,
program: ts.Program) {
const source = new TypeScriptFileRefactor(mainPath, host, program);

const bootstrap = _findNodes(source, source, ts.SyntaxKind.CallExpression, false)
const bootstrap = source.findAstNodes(source.sourceFile, 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');
});
})
.map(node => node.arguments[0] as ts.Identifier)
.filter(node => node.kind == ts.SyntaxKind.Identifier);

if (bootstrap.length != 1
|| bootstrap[0].arguments[0].kind !== ts.SyntaxKind.Identifier) {
if (bootstrap.length != 1) {
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);
const bootstrapSymbolName = bootstrap[0].text;
const module = _symbolImportLookup(source, bootstrapSymbolName, host, program);
if (module) {
return `${resolve(dirname(mainPath), module)}#${bootstrapSymbolName}`;
return `${module.replace(/\.ts$/, '')}#${bootstrapSymbolName}`;
}

// shrug... something bad happened and we couldn't find the import statement.
Expand Down
Loading

0 comments on commit 45d5154

Please sign in to comment.