From 0486ba21f83973e55e2afca90cd7eacab750734c Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 30 Nov 2016 15:11:44 -0800 Subject: [PATCH 1/4] fix(@ngtools/webpack): fixed path resolution for entry modules and lazy routes. --- packages/webpack/src/compiler_host.ts | 37 ++++- packages/webpack/src/entry_resolver.ts | 126 ++++++++---------- packages/webpack/src/plugin.ts | 78 ++++++----- packages/webpack/src/refactor.ts | 31 +++-- .../test-app-weird/aotplugin.config.json | 4 + .../not/so/source/app/app.component.html | 5 + .../not/so/source/app/app.component.scss | 3 + .../not/so/source/app/app.component.ts | 10 ++ .../not/so/source/app/app.module.ts | 27 ++++ .../so/source/app/feature/feature.module.ts | 20 +++ .../source/app/feature/lazy-feature.module.ts | 23 ++++ .../not/so/source/app/lazy.module.ts | 26 ++++ .../not/so/source/app/main.jit.ts | 5 + .../test-app-weird/not/so/source/index.html | 12 ++ .../not/so/source/tsconfig.json | 24 ++++ .../webpack/test-app-weird/package.json | 26 ++++ .../webpack/test-app-weird/webpack.config.js | 27 ++++ tests/e2e/tests/packages/webpack/weird.ts | 43 ++++++ 18 files changed, 408 insertions(+), 119 deletions(-) create mode 100644 tests/e2e/assets/webpack/test-app-weird/aotplugin.config.json create mode 100644 tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.component.html create mode 100644 tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.component.scss create mode 100644 tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.component.ts create mode 100644 tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.module.ts create mode 100644 tests/e2e/assets/webpack/test-app-weird/not/so/source/app/feature/feature.module.ts create mode 100644 tests/e2e/assets/webpack/test-app-weird/not/so/source/app/feature/lazy-feature.module.ts create mode 100644 tests/e2e/assets/webpack/test-app-weird/not/so/source/app/lazy.module.ts create mode 100644 tests/e2e/assets/webpack/test-app-weird/not/so/source/app/main.jit.ts create mode 100644 tests/e2e/assets/webpack/test-app-weird/not/so/source/index.html create mode 100644 tests/e2e/assets/webpack/test-app-weird/not/so/source/tsconfig.json create mode 100644 tests/e2e/assets/webpack/test-app-weird/package.json create mode 100644 tests/e2e/assets/webpack/test-app-weird/webpack.config.js create mode 100644 tests/e2e/tests/packages/webpack/weird.ts diff --git a/packages/webpack/src/compiler_host.ts b/packages/webpack/src/compiler_host.ts index a97e0c57d265..e74ed5004921 100644 --- a/packages/webpack/src/compiler_host.ts +++ b/packages/webpack/src/compiler_host.ts @@ -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'; @@ -93,10 +93,23 @@ 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 _setParentNodes: boolean; + + constructor(private _options: ts.CompilerOptions, private _basePath: string) { + this._setParentNodes = true; this._delegate = ts.createCompilerHost(this._options, this._setParentNodes); } + private _resolve(path: string) { + if (path[0] == '.') { + return join(this.getCurrentDirectory(), path); + } else if (path[0] == '/') { + return path; + } else { + return join(this._basePath, path); + } + } + private _setFileContent(fileName: string, content: string) { this._files[fileName] = new VirtualFileStats(fileName, content); @@ -132,26 +145,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)); @@ -166,6 +184,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); } @@ -181,15 +201,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); } diff --git a/packages/webpack/src/entry_resolver.ts b/packages/webpack/src/entry_resolver.ts index a96ab266269f..1a51069f48e3 100644 --- a/packages/webpack/src/entry_resolver.ts +++ b/packages/webpack/src/entry_resolver.ts @@ -1,37 +1,25 @@ 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) { @@ -39,15 +27,19 @@ function _recursiveSymbolExportLookup(sourcePath: string, 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; } @@ -59,8 +51,9 @@ 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; } @@ -68,16 +61,14 @@ function _recursiveSymbolExportLookup(sourcePath: string, } // 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; } } } @@ -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) { @@ -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) { @@ -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; } } } @@ -145,10 +125,12 @@ 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; @@ -156,19 +138,19 @@ export function resolveEntryModuleFromMain(mainPath: string) { && 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. diff --git a/packages/webpack/src/plugin.ts b/packages/webpack/src/plugin.ts index ea4d7fd3b316..1d80dc8780a5 100644 --- a/packages/webpack/src/plugin.ts +++ b/packages/webpack/src/plugin.ts @@ -32,8 +32,8 @@ export interface AotPluginOptions { export interface LazyRoute { moduleRoute: ModuleRoute; - moduleRelativePath: string; - moduleAbsolutePath: string; + absolutePath: string; + absoluteGenDirPath: string; } @@ -102,12 +102,13 @@ export class AotPlugin implements Tapable { this._tsConfigPath = options.tsConfigPath; // Check the base path. - let basePath = path.resolve(process.cwd(), path.dirname(this._tsConfigPath)); - if (fs.statSync(this._tsConfigPath).isDirectory()) { - basePath = this._tsConfigPath; + const maybeBasePath = path.resolve(process.cwd(), this._tsConfigPath); + let basePath = maybeBasePath; + if (fs.statSync(maybeBasePath).isFile()) { + basePath = path.dirname(basePath); } if (options.hasOwnProperty('basePath')) { - basePath = options.basePath; + basePath = path.resolve(process.cwd(), options.basePath); } const tsConfig = tsc.readConfiguration(this._tsConfigPath, basePath); @@ -122,20 +123,7 @@ export class AotPlugin implements Tapable { this._compilerOptions = tsConfig.parsed.options; - if (options.entryModule) { - this._entryModule = ModuleRoute.fromString(options.entryModule); - } else { - if (options.mainPath) { - this._entryModule = ModuleRoute.fromString(resolveEntryModuleFromMain(options.mainPath)); - } else { - this._entryModule = ModuleRoute.fromString((tsConfig.ngOptions as any).entryModule); - } - } - this._angularCompilerOptions = Object.assign({}, tsConfig.ngOptions, { - basePath, - entryModule: this._entryModule.toString(), - genDir - }); + this._angularCompilerOptions = Object.assign({}, tsConfig.ngOptions, { basePath, genDir }); this._basePath = basePath; this._genDir = genDir; @@ -146,9 +134,22 @@ export class AotPlugin implements Tapable { this._skipCodeGeneration = options.skipCodeGeneration; } - this._compilerHost = new WebpackCompilerHost(this._compilerOptions); + this._compilerHost = new WebpackCompilerHost(this._compilerOptions, this._basePath); this._program = ts.createProgram( this._rootFilePath, this._compilerOptions, this._compilerHost); + + if (options.entryModule) { + this._entryModule = ModuleRoute.fromString(options.entryModule); + } else { + if (options.mainPath) { + const entryModuleString = resolveEntryModuleFromMain(options.mainPath, this._compilerHost, + this._program); + this._entryModule = ModuleRoute.fromString(entryModuleString); + } else { + this._entryModule = ModuleRoute.fromString((tsConfig.ngOptions as any).entryModule); + } + } + this._reflectorHost = new ngCompiler.ReflectorHost( this._program, this._compilerHost, this._angularCompilerOptions); this._reflector = new ngCompiler.StaticReflector(this._reflectorHost); @@ -170,7 +171,7 @@ export class AotPlugin implements Tapable { return callback(); } - request.request = this.genDir; + request.request = this.skipCodeGeneration ? this.basePath : this.genDir; request.recursive = true; request.dependencies.forEach((d: any) => d.critical = false); return callback(null, request); @@ -181,7 +182,7 @@ export class AotPlugin implements Tapable { } this.done.then(() => { - result.resource = this.genDir; + result.resource = this.skipCodeGeneration ? this.basePath : this.genDir; result.recursive = true; result.dependencies.forEach((d: any) => d.critical = false); result.resolveDependencies = createResolveDependenciesFromContextMap( @@ -284,9 +285,9 @@ export class AotPlugin implements Tapable { .forEach(k => { const lazyRoute = allLazyRoutes[k]; if (this.skipCodeGeneration) { - this._lazyRoutes[k] = lazyRoute.moduleAbsolutePath; + this._lazyRoutes[k] = lazyRoute.absolutePath + '.ts'; } else { - this._lazyRoutes[k + '.ngfactory'] = lazyRoute.moduleAbsolutePath + '.ngfactory.ts'; + this._lazyRoutes[k + '.ngfactory'] = lazyRoute.absoluteGenDirPath + '.ngfactory.ts'; } }); }) @@ -312,20 +313,31 @@ export class AotPlugin implements Tapable { const entryNgModuleMetadata = this.getNgModuleMetadata(staticSymbol); const loadChildrenRoute: LazyRoute[] = this.extractLoadChildren(entryNgModuleMetadata) .map(route => { - const mr = ModuleRoute.fromString(route); - const relativePath = this._resolveModulePath(mr, relativeModulePath); - const absolutePath = path.resolve(this.genDir, relativePath); + const moduleRoute = ModuleRoute.fromString(route); + const resolvedModule = ts.resolveModuleName(moduleRoute.path, + relativeModulePath, this._compilerOptions, this._compilerHost); + + if (!resolvedModule.resolvedModule) { + throw new Error(`Could not resolve route "${route}" from file "${relativeModulePath}".`); + } + + const relativePath = path.relative(this.basePath, + resolvedModule.resolvedModule.resolvedFileName).replace(/\.ts$/, ''); + + const absolutePath = path.join(this.basePath, relativePath); + const absoluteGenDirPath = path.join(this._genDir, relativePath); + return { - moduleRoute: mr, - moduleRelativePath: relativePath, - moduleAbsolutePath: absolutePath + moduleRoute, + absoluteGenDirPath, + absolutePath }; }); const resultMap: LazyRouteMap = loadChildrenRoute .reduce((acc: LazyRouteMap, curr: LazyRoute) => { const key = curr.moduleRoute.path; if (acc[key]) { - if (acc[key].moduleAbsolutePath != curr.moduleAbsolutePath) { + if (acc[key].absolutePath != curr.absolutePath) { throw new Error(`Duplicated path in loadChildren detected: "${key}" is used in 2 ` + 'loadChildren, but they point to different modules. Webpack cannot distinguish ' + 'between the two based on context and would fail to load the proper one.'); @@ -344,7 +356,7 @@ export class AotPlugin implements Tapable { const child = children[p]; const key = child.moduleRoute.path; if (resultMap[key]) { - if (resultMap[key].moduleAbsolutePath != child.moduleAbsolutePath) { + if (resultMap[key].absolutePath != child.absolutePath) { throw new Error(`Duplicated path in loadChildren detected: "${key}" is used in 2 ` + 'loadChildren, but they point to different modules. Webpack cannot distinguish ' + 'between the two based on context and would fail to load the proper one.'); diff --git a/packages/webpack/src/refactor.ts b/packages/webpack/src/refactor.ts index 9a2a174e3b33..db5e88d6b650 100644 --- a/packages/webpack/src/refactor.ts +++ b/packages/webpack/src/refactor.ts @@ -11,7 +11,17 @@ export interface TranspileOutput { sourceMap: any | null; } + +function resolve(filePath: string, host: ts.CompilerHost, program: ts.Program) { + if (filePath[0] == '/') { + return filePath; + } + return path.join(program.getCompilerOptions().baseUrl || process.cwd(), filePath); +} + + export class TypeScriptFileRefactor { + private _fileName: string; private _sourceFile: ts.SourceFile; private _sourceString: any; private _sourceText: string; @@ -21,15 +31,17 @@ export class TypeScriptFileRefactor { get sourceFile() { return this._sourceFile; } get sourceText() { return this._sourceString.toString(); } - constructor(private _fileName: string, + constructor(fileName: string, private _host: ts.CompilerHost, private _program?: ts.Program) { + fileName = resolve(fileName, _host, _program); + this._fileName = fileName; if (_program) { - this._sourceFile = _program.getSourceFile(_fileName); + this._sourceFile = _program.getSourceFile(fileName); } if (!this._sourceFile) { this._program = null; - this._sourceFile = ts.createSourceFile(_fileName, _host.readFile(_fileName), + this._sourceFile = ts.createSourceFile(fileName, _host.readFile(fileName), ts.ScriptTarget.Latest); } this._sourceText = this._sourceFile.getFullText(this._sourceFile); @@ -48,19 +60,22 @@ export class TypeScriptFileRefactor { /** * Find all nodes from the AST in the subtree of node of SyntaxKind kind. - * @param node - * @param kind + * @param node The root node to check, or null if the whole tree should be searched. + * @param kind The kind of nodes to find. * @param recursive Whether to go in matched nodes to keep matching. * @param max The maximum number of items to return. * @return all nodes of kind, or [] if none is found */ - findAstNodes(node: ts.Node, + findAstNodes(node: ts.Node | null, kind: ts.SyntaxKind, recursive = false, max: number = Infinity): ts.Node[] { - if (!node || max == 0) { + if (max == 0) { return []; } + if (!node) { + node = this._sourceFile; + } let arr: ts.Node[] = []; if (node.kind === kind) { @@ -157,8 +172,6 @@ export class TypeScriptFileRefactor { } transpile(compilerOptions: ts.CompilerOptions): TranspileOutput { - // const basePath = path.resolve(path.dirname(tsConfigPath), - // tsConfig.config.compilerOptions.baseUrl || '.'); compilerOptions = Object.assign({}, compilerOptions, { sourceMap: true, inlineSources: false, diff --git a/tests/e2e/assets/webpack/test-app-weird/aotplugin.config.json b/tests/e2e/assets/webpack/test-app-weird/aotplugin.config.json new file mode 100644 index 000000000000..b6d2d7a7816d --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird/aotplugin.config.json @@ -0,0 +1,4 @@ +{ + "tsConfigPath": "./not/so/source/tsconfig.json", + "mainPath": "app/main.jit.ts" +} \ No newline at end of file diff --git a/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.component.html b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.component.html new file mode 100644 index 000000000000..5a532db9308f --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.component.html @@ -0,0 +1,5 @@ +
+

hello world

+ lazy + +
diff --git a/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.component.scss b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.component.scss new file mode 100644 index 000000000000..5cde7b922336 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.component.scss @@ -0,0 +1,3 @@ +:host { + background-color: blue; +} diff --git a/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.component.ts b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.component.ts new file mode 100644 index 000000000000..09a19ad8f1ac --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.component.ts @@ -0,0 +1,10 @@ +import {Component, ViewEncapsulation} from '@angular/core'; + + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class AppComponent { } diff --git a/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.module.ts b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.module.ts new file mode 100644 index 000000000000..ded686868a22 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/app.module.ts @@ -0,0 +1,27 @@ +import { NgModule, Component } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { RouterModule } from '@angular/router'; +import { AppComponent } from './app.component'; + +@Component({ + selector: 'home-view', + template: 'home!' +}) +export class HomeView {} + + +@NgModule({ + declarations: [ + AppComponent, + HomeView + ], + imports: [ + BrowserModule, + RouterModule.forRoot([ + {path: 'lazy', loadChildren: './lazy.module#LazyModule'}, + {path: '', component: HomeView} + ]) + ], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/feature/feature.module.ts b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/feature/feature.module.ts new file mode 100644 index 000000000000..f464ca028b05 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/feature/feature.module.ts @@ -0,0 +1,20 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; + +@Component({ + selector: 'feature-component', + template: 'foo.html' +}) +export class FeatureComponent {} + +@NgModule({ + declarations: [ + FeatureComponent + ], + imports: [ + RouterModule.forChild([ + { path: '', component: FeatureComponent} + ]) + ] +}) +export class FeatureModule {} diff --git a/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/feature/lazy-feature.module.ts b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/feature/lazy-feature.module.ts new file mode 100644 index 000000000000..8fafca158b24 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/feature/lazy-feature.module.ts @@ -0,0 +1,23 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {HttpModule, Http} from '@angular/http'; + +@Component({ + selector: 'lazy-feature-comp', + template: 'lazy feature!' +}) +export class LazyFeatureComponent {} + +@NgModule({ + imports: [ + RouterModule.forChild([ + {path: '', component: LazyFeatureComponent, pathMatch: 'full'}, + {path: 'feature', loadChildren: './feature.module#FeatureModule'} + ]), + HttpModule + ], + declarations: [LazyFeatureComponent] +}) +export class LazyFeatureModule { + constructor(http: Http) {} +} diff --git a/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/lazy.module.ts b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/lazy.module.ts new file mode 100644 index 000000000000..96da4de7515b --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/lazy.module.ts @@ -0,0 +1,26 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {HttpModule, Http} from '@angular/http'; + +@Component({ + selector: 'lazy-comp', + template: 'lazy!' +}) +export class LazyComponent {} + +@NgModule({ + imports: [ + RouterModule.forChild([ + {path: '', component: LazyComponent, pathMatch: 'full'}, + {path: 'feature', loadChildren: './feature/feature.module#FeatureModule'}, + {path: 'lazy-feature', loadChildren: './feature/lazy-feature.module#LazyFeatureModule'} + ]), + HttpModule + ], + declarations: [LazyComponent] +}) +export class LazyModule { + constructor(http: Http) {} +} + +export class SecondModule {} diff --git a/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/main.jit.ts b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/main.jit.ts new file mode 100644 index 000000000000..4f083729991e --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird/not/so/source/app/main.jit.ts @@ -0,0 +1,5 @@ +import 'reflect-metadata'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {AppModule} from './app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/tests/e2e/assets/webpack/test-app-weird/not/so/source/index.html b/tests/e2e/assets/webpack/test-app-weird/not/so/source/index.html new file mode 100644 index 000000000000..89fb0893c35d --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird/not/so/source/index.html @@ -0,0 +1,12 @@ + + + + Document + + + + + + + + diff --git a/tests/e2e/assets/webpack/test-app-weird/not/so/source/tsconfig.json b/tests/e2e/assets/webpack/test-app-weird/not/so/source/tsconfig.json new file mode 100644 index 000000000000..74a0d8f522a4 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird/not/so/source/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "baseUrl": "", + "module": "es2015", + "moduleResolution": "node", + "target": "es5", + "noImplicitAny": false, + "sourceMap": true, + "mapRoot": "", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es2015", + "dom" + ], + "outDir": "lib", + "skipLibCheck": true, + "rootDir": "." + }, + "angularCompilerOptions": { + "genDir": "app/generated/", + "entryModule": "app/app.module#AppModule" + } +} diff --git a/tests/e2e/assets/webpack/test-app-weird/package.json b/tests/e2e/assets/webpack/test-app-weird/package.json new file mode 100644 index 000000000000..3820b8170840 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird/package.json @@ -0,0 +1,26 @@ +{ + "name": "test", + "license": "MIT", + "dependencies": { + "@angular/common": "2.2.1", + "@angular/compiler": "2.2.1", + "@angular/compiler-cli": "2.2.1", + "@angular/core": "2.2.1", + "@angular/http": "2.2.1", + "@angular/platform-browser": "2.2.1", + "@angular/platform-browser-dynamic": "2.2.1", + "@angular/platform-server": "2.2.1", + "@angular/router": "3.2.1", + "core-js": "^2.4.1", + "rxjs": "^5.0.0-beta.12", + "zone.js": "^0.6.21" + }, + "devDependencies": { + "node-sass": "^3.7.0", + "performance-now": "^0.2.0", + "raw-loader": "^0.5.1", + "sass-loader": "^3.2.0", + "typescript": "~2.0.3", + "webpack": "2.1.0-beta.22" + } +} diff --git a/tests/e2e/assets/webpack/test-app-weird/webpack.config.js b/tests/e2e/assets/webpack/test-app-weird/webpack.config.js new file mode 100644 index 000000000000..b1d3de68a4e1 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird/webpack.config.js @@ -0,0 +1,27 @@ +const ngToolsWebpack = require('@ngtools/webpack'); + +module.exports = { + resolve: { + extensions: ['.ts', '.js'] + }, + entry: './not/so/source/app/main.jit.ts', + output: { + path: './dist', + publicPath: 'dist/', + filename: 'app.main.js' + }, + plugins: [ + new ngToolsWebpack.AotPlugin(require('./aotplugin.config.json')) + ], + module: { + loaders: [ + { test: /\.scss$/, loaders: ['raw-loader', 'sass-loader'] }, + { test: /\.css$/, loader: 'raw-loader' }, + { test: /\.html$/, loader: 'raw-loader' }, + { test: /\.ts$/, loader: '@ngtools/webpack' } + ] + }, + devServer: { + historyApiFallback: true + } +}; diff --git a/tests/e2e/tests/packages/webpack/weird.ts b/tests/e2e/tests/packages/webpack/weird.ts new file mode 100644 index 000000000000..2987d935aae4 --- /dev/null +++ b/tests/e2e/tests/packages/webpack/weird.ts @@ -0,0 +1,43 @@ +import {copyAssets} from '../../../utils/assets'; +import {exec, silentNpm} from '../../../utils/process'; +import {updateJsonFile} from '../../../utils/project'; +import {join} from 'path'; +import {expectFileSizeToBeUnder, expectFileToExist} from '../../../utils/fs'; +import {expectToFail} from '../../../utils/utils'; + + +export default function(argv: any, skipCleaning: () => void) { + if (process.platform.startsWith('win')) { + // Disable the test on Windows. + return Promise.resolve(); + } + + return Promise.resolve() + .then(() => copyAssets('webpack/test-app-weird')) + .then(dir => process.chdir(dir)) + .then(() => updateJsonFile('package.json', json => { + const dist = '../../../../../dist/'; + json['dependencies']['@ngtools/webpack'] = join(__dirname, dist, 'webpack'); + })) + .then(() => silentNpm('install')) + .then(() => exec('node_modules/.bin/webpack', '-p')) + .then(() => expectFileToExist('dist/app.main.js')) + .then(() => expectFileToExist('dist/0.app.main.js')) + .then(() => expectFileToExist('dist/1.app.main.js')) + .then(() => expectFileToExist('dist/2.app.main.js')) + .then(() => expectFileSizeToBeUnder('dist/app.main.js', 400000)) + .then(() => expectFileSizeToBeUnder('dist/0.app.main.js', 40000)) + + // Skip code generation and rebuild. + .then(() => updateJsonFile('aotplugin.config.json', json => { + json['skipCodeGeneration'] = true; + })) + .then(() => exec('node_modules/.bin/webpack', '-p')) + .then(() => expectFileToExist('dist/app.main.js')) + .then(() => expectFileToExist('dist/0.app.main.js')) + .then(() => expectFileToExist('dist/1.app.main.js')) + .then(() => expectFileToExist('dist/2.app.main.js')) + .then(() => expectToFail(() => expectFileSizeToBeUnder('dist/app.main.js', 400000))) + .then(() => expectFileSizeToBeUnder('dist/0.app.main.js', 40000)) + .then(() => skipCleaning()); +} From 06fb4ea4f7f6a207fbce9804cef5aac03163a4aa Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 1 Dec 2016 11:24:10 -0800 Subject: [PATCH 2/4] Support for Windows paths. --- packages/webpack/src/compiler_host.ts | 11 +++++++++-- packages/webpack/src/refactor.ts | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/webpack/src/compiler_host.ts b/packages/webpack/src/compiler_host.ts index e74ed5004921..1a5b91ce7264 100644 --- a/packages/webpack/src/compiler_host.ts +++ b/packages/webpack/src/compiler_host.ts @@ -93,17 +93,24 @@ export class WebpackCompilerHost implements ts.CompilerHost { private _directories: {[path: string]: VirtualDirStats} = Object.create(null); private _changed = false; + private _basePath: string; private _setParentNodes: boolean; - constructor(private _options: ts.CompilerOptions, private _basePath: string) { + 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] == '/') { + } else if (path[0] == '/' || path.match(/^\w:\//)) { return path; } else { return join(this._basePath, path); diff --git a/packages/webpack/src/refactor.ts b/packages/webpack/src/refactor.ts index db5e88d6b650..29bc8a268649 100644 --- a/packages/webpack/src/refactor.ts +++ b/packages/webpack/src/refactor.ts @@ -13,7 +13,7 @@ export interface TranspileOutput { function resolve(filePath: string, host: ts.CompilerHost, program: ts.Program) { - if (filePath[0] == '/') { + if (path.isAbsolute(filePath)) { return filePath; } return path.join(program.getCompilerOptions().baseUrl || process.cwd(), filePath); @@ -34,7 +34,7 @@ export class TypeScriptFileRefactor { constructor(fileName: string, private _host: ts.CompilerHost, private _program?: ts.Program) { - fileName = resolve(fileName, _host, _program); + fileName = resolve(fileName, _host, _program).replace(/\\/g, '/'); this._fileName = fileName; if (_program) { this._sourceFile = _program.getSourceFile(fileName); From 3caf12c94aa2e75271bf46eae93ac15d7962e030 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 1 Dec 2016 12:34:00 -0800 Subject: [PATCH 3/4] Fix population of webpack FS in Windows. --- packages/webpack/src/compiler_host.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/webpack/src/compiler_host.ts b/packages/webpack/src/compiler_host.ts index 1a5b91ce7264..b1ff333a8944 100644 --- a/packages/webpack/src/compiler_host.ts +++ b/packages/webpack/src/compiler_host.ts @@ -135,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)]; } @@ -215,7 +220,7 @@ export class WebpackCompilerHost implements ts.CompilerHost { onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]): void => { fileName = this._resolve(fileName); this._setFileContent(fileName, data); - } + }; } getCurrentDirectory(): string { From 1d41102da3ff0e8a61e0342899a87263f7d10926 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 1 Dec 2016 15:02:31 -0800 Subject: [PATCH 4/4] more fixes --- packages/webpack/src/refactor.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/webpack/src/refactor.ts b/packages/webpack/src/refactor.ts index 29bc8a268649..d7f64e4fba57 100644 --- a/packages/webpack/src/refactor.ts +++ b/packages/webpack/src/refactor.ts @@ -201,8 +201,11 @@ export class TypeScriptFileRefactor { } const sourceMap = map.toJSON(); - sourceMap.sources = [ this._fileName ]; - sourceMap.file = path.basename(this._fileName, '.ts') + '.js'; + const fileName = process.platform.startsWith('win') + ? this._fileName.replace(/\//g, '\\') + : this._fileName; + sourceMap.sources = [ fileName ]; + sourceMap.file = path.basename(fileName, '.ts') + '.js'; sourceMap.sourcesContent = [ this._sourceText ]; return { outputText: result.outputText, sourceMap };