Skip to content

Commit 45d5154

Browse files
authored
fix(@ngtools/webpack): fixed path resolution for entry modules and lazy routes (#3332)
1 parent 53ab4df commit 45d5154

18 files changed

+431
-127
lines changed

packages/webpack/src/compiler_host.ts

+50-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as ts from 'typescript';
2-
import {basename, dirname} from 'path';
2+
import {basename, dirname, join} from 'path';
33
import * as fs from 'fs';
44

55

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

96-
constructor(private _options: ts.CompilerOptions, private _setParentNodes = true) {
96+
private _basePath: string;
97+
private _setParentNodes: boolean;
98+
99+
constructor(private _options: ts.CompilerOptions, basePath: string) {
100+
this._setParentNodes = true;
97101
this._delegate = ts.createCompilerHost(this._options, this._setParentNodes);
102+
this._basePath = this._normalizePath(basePath);
103+
}
104+
105+
private _normalizePath(path: string) {
106+
return path.replace(/\\/g, '/');
107+
}
108+
109+
private _resolve(path: string) {
110+
path = this._normalizePath(path);
111+
if (path[0] == '.') {
112+
return join(this.getCurrentDirectory(), path);
113+
} else if (path[0] == '/' || path.match(/^\w:\//)) {
114+
return path;
115+
} else {
116+
return join(this._basePath, path);
117+
}
98118
}
99119

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

138+
const isWindows = process.platform.startsWith('win');
118139
for (const fileName of Object.keys(this._files)) {
119140
const stats = this._files[fileName];
120-
fs._statStorage.data[fileName] = [null, stats];
121-
fs._readFileStorage.data[fileName] = [null, stats.content];
141+
// If we're on windows, we need to populate with the proper path separator.
142+
const path = isWindows ? fileName.replace(/\//g, '\\') : fileName;
143+
fs._statStorage.data[path] = [null, stats];
144+
fs._readFileStorage.data[path] = [null, stats.content];
122145
}
123-
for (const path of Object.keys(this._directories)) {
124-
const stats = this._directories[path];
125-
const dirs = this.getDirectories(path);
126-
const files = this.getFiles(path);
146+
for (const dirName of Object.keys(this._directories)) {
147+
const stats = this._directories[dirName];
148+
const dirs = this.getDirectories(dirName);
149+
const files = this.getFiles(dirName);
150+
// If we're on windows, we need to populate with the proper path separator.
151+
const path = isWindows ? dirName.replace(/\//g, '\\') : dirName;
127152
fs._statStorage.data[path] = [null, stats];
128153
fs._readdirStorage.data[path] = [null, files.concat(dirs)];
129154
}
@@ -132,26 +157,31 @@ export class WebpackCompilerHost implements ts.CompilerHost {
132157
}
133158

134159
fileExists(fileName: string): boolean {
160+
fileName = this._resolve(fileName);
135161
return fileName in this._files || this._delegate.fileExists(fileName);
136162
}
137163

138164
readFile(fileName: string): string {
165+
fileName = this._resolve(fileName);
139166
return (fileName in this._files)
140167
? this._files[fileName].content
141168
: this._delegate.readFile(fileName);
142169
}
143170

144171
directoryExists(directoryName: string): boolean {
172+
directoryName = this._resolve(directoryName);
145173
return (directoryName in this._directories) || this._delegate.directoryExists(directoryName);
146174
}
147175

148176
getFiles(path: string): string[] {
177+
path = this._resolve(path);
149178
return Object.keys(this._files)
150179
.filter(fileName => dirname(fileName) == path)
151180
.map(path => basename(path));
152181
}
153182

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

168198
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: OnErrorFn) {
199+
fileName = this._resolve(fileName);
200+
169201
if (!(fileName in this._files)) {
170202
return this._delegate.getSourceFile(fileName, languageVersion, onError);
171203
}
@@ -181,15 +213,22 @@ export class WebpackCompilerHost implements ts.CompilerHost {
181213
return this._delegate.getDefaultLibFileName(options);
182214
}
183215

184-
writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: OnErrorFn) {
185-
this._setFileContent(fileName, data);
216+
// This is due to typescript CompilerHost interface being weird on writeFile. This shuts down
217+
// typings in WebStorm.
218+
get writeFile() {
219+
return (fileName: string, data: string, writeByteOrderMark: boolean,
220+
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]): void => {
221+
fileName = this._resolve(fileName);
222+
this._setFileContent(fileName, data);
223+
};
186224
}
187225

188226
getCurrentDirectory(): string {
189-
return this._delegate.getCurrentDirectory();
227+
return this._basePath !== null ? this._basePath : this._delegate.getCurrentDirectory();
190228
}
191229

192230
getCanonicalFileName(fileName: string): string {
231+
fileName = this._resolve(fileName);
193232
return this._delegate.getCanonicalFileName(fileName);
194233
}
195234

packages/webpack/src/entry_resolver.ts

+54-72
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,45 @@
11
import * as fs from 'fs';
2-
import {dirname, join, resolve} from 'path';
2+
import {join} from 'path';
33
import * as ts from 'typescript';
44

5+
import {TypeScriptFileRefactor} from './refactor';
56

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-
}
157

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 {
8+
function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor,
9+
symbolName: string,
10+
host: ts.CompilerHost,
11+
program: ts.Program): string | null {
2412
// Check this file.
25-
const hasSymbol = _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ClassDeclaration)
13+
const hasSymbol = refactor.findAstNodes(null, ts.SyntaxKind.ClassDeclaration)
2614
.some((cd: ts.ClassDeclaration) => {
2715
return cd.name && cd.name.text == symbolName;
2816
});
2917
if (hasSymbol) {
30-
return sourcePath;
18+
return refactor.fileName;
3119
}
3220

3321
// 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)
22+
const exports = refactor.findAstNodes(null, ts.SyntaxKind.ExportDeclaration)
3523
.map(node => node as ts.ExportDeclaration);
3624

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

42-
const module = resolve(dirname(sourcePath), (decl.moduleSpecifier as ts.StringLiteral).text);
30+
const modulePath = (decl.moduleSpecifier as ts.StringLiteral).text;
31+
const resolvedModule = ts.resolveModuleName(
32+
modulePath, refactor.fileName, program.getCompilerOptions(), host);
33+
if (!resolvedModule.resolvedModule || !resolvedModule.resolvedModule.resolvedFileName) {
34+
return null;
35+
}
36+
37+
const module = resolvedModule.resolvedModule.resolvedFileName;
4338
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-
}
39+
const moduleRefactor = new TypeScriptFileRefactor(module, host, program);
40+
const maybeModule = _recursiveSymbolExportLookup(moduleRefactor, symbolName, host, program);
41+
if (maybeModule) {
42+
return maybeModule;
5143
}
5244
continue;
5345
}
@@ -59,25 +51,24 @@ function _recursiveSymbolExportLookup(sourcePath: string,
5951
if (fs.statSync(module).isDirectory()) {
6052
const indexModule = join(module, 'index.ts');
6153
if (fs.existsSync(indexModule)) {
54+
const indexRefactor = new TypeScriptFileRefactor(indexModule, host, program);
6255
const maybeModule = _recursiveSymbolExportLookup(
63-
indexModule, _createSource(indexModule), symbolName);
56+
indexRefactor, symbolName, host, program);
6457
if (maybeModule) {
6558
return maybeModule;
6659
}
6760
}
6861
}
6962

7063
// 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)
64+
const source = new TypeScriptFileRefactor(module, host, program);
65+
const hasSymbol = source.findAstNodes(null, ts.SyntaxKind.ClassDeclaration)
7366
.some((cd: ts.ClassDeclaration) => {
7467
return cd.name && cd.name.text == symbolName;
7568
});
7669

7770
if (hasSymbol) {
7871
return module;
79-
} else {
80-
return null;
8172
}
8273
}
8374
}
@@ -86,11 +77,12 @@ function _recursiveSymbolExportLookup(sourcePath: string,
8677
return null;
8778
}
8879

89-
function _symbolImportLookup(sourcePath: string,
90-
sourceFile: ts.SourceFile,
91-
symbolName: string): string | null {
80+
function _symbolImportLookup(refactor: TypeScriptFileRefactor,
81+
symbolName: string,
82+
host: ts.CompilerHost,
83+
program: ts.Program): string | null {
9284
// 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)
85+
const imports = refactor.findAstNodes(null, ts.SyntaxKind.ImportDeclaration)
9486
.map(node => node as ts.ImportDeclaration);
9587

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

104-
const module = resolve(dirname(sourcePath), (decl.moduleSpecifier as ts.StringLiteral).text);
96+
const resolvedModule = ts.resolveModuleName(
97+
(decl.moduleSpecifier as ts.StringLiteral).text,
98+
refactor.fileName, program.getCompilerOptions(), host);
99+
if (!resolvedModule.resolvedModule || !resolvedModule.resolvedModule.resolvedFileName) {
100+
return null;
101+
}
105102

103+
const module = resolvedModule.resolvedModule.resolvedFileName;
106104
if (decl.importClause.namedBindings.kind == ts.SyntaxKind.NamespaceImport) {
107105
const binding = decl.importClause.namedBindings as ts.NamespaceImport;
108106
if (binding.name.text == symbolName) {
@@ -113,29 +111,11 @@ function _symbolImportLookup(sourcePath: string,
113111
const binding = decl.importClause.namedBindings as ts.NamedImports;
114112
for (const specifier of binding.elements) {
115113
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;
114+
// Create the source and recursively lookup the import.
115+
const source = new TypeScriptFileRefactor(module, host, program);
116+
const maybeModule = _recursiveSymbolExportLookup(source, symbolName, host, program);
117+
if (maybeModule) {
118+
return maybeModule;
139119
}
140120
}
141121
}
@@ -145,30 +125,32 @@ function _symbolImportLookup(sourcePath: string,
145125
}
146126

147127

148-
export function resolveEntryModuleFromMain(mainPath: string) {
149-
const source = _createSource(mainPath);
128+
export function resolveEntryModuleFromMain(mainPath: string,
129+
host: ts.CompilerHost,
130+
program: ts.Program) {
131+
const source = new TypeScriptFileRefactor(mainPath, host, program);
150132

151-
const bootstrap = _findNodes(source, source, ts.SyntaxKind.CallExpression, false)
133+
const bootstrap = source.findAstNodes(source.sourceFile, ts.SyntaxKind.CallExpression, false)
152134
.map(node => node as ts.CallExpression)
153135
.filter(call => {
154136
const access = call.expression as ts.PropertyAccessExpression;
155137
return access.kind == ts.SyntaxKind.PropertyAccessExpression
156138
&& access.name.kind == ts.SyntaxKind.Identifier
157139
&& (access.name.text == 'bootstrapModule'
158140
|| access.name.text == 'bootstrapModuleFactory');
159-
});
141+
})
142+
.map(node => node.arguments[0] as ts.Identifier)
143+
.filter(node => node.kind == ts.SyntaxKind.Identifier);
160144

161-
if (bootstrap.length != 1
162-
|| bootstrap[0].arguments[0].kind !== ts.SyntaxKind.Identifier) {
145+
if (bootstrap.length != 1) {
163146
throw new Error('Tried to find bootstrap code, but could not. Specify either '
164147
+ 'statically analyzable bootstrap code or pass in an entryModule '
165148
+ 'to the plugins options.');
166149
}
167-
168-
const bootstrapSymbolName = (bootstrap[0].arguments[0] as ts.Identifier).text;
169-
const module = _symbolImportLookup(mainPath, source, bootstrapSymbolName);
150+
const bootstrapSymbolName = bootstrap[0].text;
151+
const module = _symbolImportLookup(source, bootstrapSymbolName, host, program);
170152
if (module) {
171-
return `${resolve(dirname(mainPath), module)}#${bootstrapSymbolName}`;
153+
return `${module.replace(/\.ts$/, '')}#${bootstrapSymbolName}`;
172154
}
173155

174156
// shrug... something bad happened and we couldn't find the import statement.

0 commit comments

Comments
 (0)