Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(@ngtools/webpack): fixed path resolution for entry modules and la… #3332

Merged
merged 4 commits into from
Dec 2, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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