From 1585456a4584ffdb84b184c15ab991b3c22e274a Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Tue, 15 Nov 2016 23:21:24 -0800 Subject: [PATCH 01/11] feature(@ngtools/webpack): add an option to skip code generation. --- .../models/webpack-build-typescript.ts | 62 ++++++++++------ packages/webpack/README.md | 4 +- packages/webpack/src/loader.ts | 73 ++++++++++++++++++- packages/webpack/src/plugin.ts | 23 ++++-- 4 files changed, 130 insertions(+), 32 deletions(-) diff --git a/packages/angular-cli/models/webpack-build-typescript.ts b/packages/angular-cli/models/webpack-build-typescript.ts index f1b511eca35a..d4d5f41e716e 100644 --- a/packages/angular-cli/models/webpack-build-typescript.ts +++ b/packages/angular-cli/models/webpack-build-typescript.ts @@ -12,37 +12,55 @@ const webpackLoader: string = g['angularCliIsLocal'] export const getWebpackNonAotConfigPartial = function(projectRoot: string, appConfig: any) { - const appRoot = path.resolve(projectRoot, appConfig.root); - const lazyModules = findLazyModules(appRoot); - + // const appRoot = path.resolve(projectRoot, appConfig.root); + // const lazyModules = findLazyModules(appRoot); + // + // return { + // resolve: { + // plugins: [ + // new atl.TsConfigPathsPlugin({ + // tsconfig: path.resolve(appRoot, appConfig.tsconfig) + // }) + // ] + // }, + // module: { + // rules: [ + // { + // test: /\.ts$/, + // loaders: [{ + // loader: 'awesome-typescript-loader', + // query: { + // forkChecker: true, + // tsconfig: path.resolve(appRoot, appConfig.tsconfig) + // } + // }, { + // loader: 'angular2-template-loader' + // }], + // exclude: [/\.(spec|e2e)\.ts$/] + // } + // ], + // }, + // plugins: [ + // new webpack.ContextReplacementPlugin(/.*/, appRoot, lazyModules), + // new atl.ForkCheckerPlugin(), + // ] + // }; return { - resolve: { - plugins: [ - new atl.TsConfigPathsPlugin({ - tsconfig: path.resolve(appRoot, appConfig.tsconfig) - }) - ] - }, module: { rules: [ { test: /\.ts$/, - loaders: [{ - loader: 'awesome-typescript-loader', - query: { - forkChecker: true, - tsconfig: path.resolve(appRoot, appConfig.tsconfig) - } - }, { - loader: 'angular2-template-loader' - }], + loader: webpackLoader, exclude: [/\.(spec|e2e)\.ts$/] } - ], + ] }, plugins: [ - new webpack.ContextReplacementPlugin(/.*/, appRoot, lazyModules), - new atl.ForkCheckerPlugin(), + new AotPlugin({ + tsConfigPath: path.resolve(projectRoot, appConfig.root, appConfig.tsconfig), + mainPath: path.join(projectRoot, appConfig.root, appConfig.main), + skipCodeGeneration: true + }), ] }; }; diff --git a/packages/webpack/README.md b/packages/webpack/README.md index e04251f4f7bc..2bd1e5abb5a2 100644 --- a/packages/webpack/README.md +++ b/packages/webpack/README.md @@ -35,5 +35,5 @@ The loader works with the webpack plugin to compile your TypeScript. It's import * `basePath`. Optional. The root to use by the compiler to resolve file paths. By default, use the `tsConfigPath` root. * `entryModule`. Optional if specified in `angularCompilerOptions`. The path and classname of the main application module. This follows the format `path/to/file#ClassName`. * `mainPath`. Optional if `entryModule` is specified. The `main.ts` file containing the bootstrap code. The plugin will use AST to determine the `entryModule`. -* `genDir`. Optional. The output directory of the offline compiler. The files created by the offline compiler will be in a virtual file system, but the import paths might change. This can also be specified in `angularCompilerOptions`, and by default will be the same as `basePath`. -* `typeChecking`. Optional, defaults to true. Enable type checking through your application. This will slow down compilation, but show syntactic and semantic errors in webpack. \ No newline at end of file +* `skipCodeGeneration`. Optional, defaults to false. Disable code generation and do not refactor the code to bootstrap. This replaces `templateUrl: "string"` with `template: require("string")` (and similar for styles) to allow for webpack to properly link the resources. +* `typeChecking`. Optional, defaults to true. Enable type checking through your application. This will slow down compilation, but show syntactic and semantic errors in webpack. diff --git a/packages/webpack/src/loader.ts b/packages/webpack/src/loader.ts index 3e2c006ce6b9..0e8cb1da9a1f 100644 --- a/packages/webpack/src/loader.ts +++ b/packages/webpack/src/loader.ts @@ -15,6 +15,16 @@ function _findNodes(sourceFile: ts.SourceFile, node: ts.Node, kind: ts.SyntaxKin }, node.kind == kind ? [node] : []); } +function _getContentOfKeyLiteral(source: ts.SourceFile, node: ts.Node): string { + if (node.kind == ts.SyntaxKind.Identifier) { + return (node as ts.Identifier).text; + } else if (node.kind == ts.SyntaxKind.StringLiteral) { + return (node as ts.StringLiteral).text; + } else { + return null; + } +} + function _removeDecorators(fileName: string, source: string): string { const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest); // Find all decorators. @@ -107,6 +117,58 @@ function _replaceBootstrap(fileName: string, }).then(() => sourceText); } +function _replaceResources(fileName: string, source: string) { + const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest); + + const changes = new MultiChange(); + + // Find all object literals. + _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ObjectLiteralExpression, true) + // Get all their property assignments. + .map(node => _findNodes(sourceFile, node, ts.SyntaxKind.PropertyAssignment)) + // Flatten into a single array (from an array of array). + .reduce((prev, curr) => curr ? prev.concat(curr) : prev, []) + // Remove every property assignment that aren't 'loadChildren'. + .filter((node: ts.PropertyAssignment) => { + const key = _getContentOfKeyLiteral(sourceFile, node.name); + if (!key) { + // key is an expression, can't do anything. + return false; + } + return key == 'templateUrl' || key == 'styleUrls'; + }) + // Get the full text of the initializer. + .forEach((node: ts.PropertyAssignment) => { + const key = _getContentOfKeyLiteral(sourceFile, node.name); + // changes.appendChange(new ReplaceChange(fileName, call.arguments[0].getStart(sourceFile), + // entryModule.className, entryModule.className + 'NgFactory')); + if (key == 'templateUrl') { + changes.appendChange(new ReplaceChange(fileName, node.getFullStart(), + node.getFullText(sourceFile), + `template: require(${node.initializer.getFullText(sourceFile)})`)); + } else if (key == 'styleUrls') { + const arr: ts.ArrayLiteralExpression[] = + _findNodes(sourceFile, node, + ts.SyntaxKind.ArrayLiteralExpression, false); + if (!arr || arr.length == 0 || arr[0].elements.length == 0) { + return; + } + + const initializer = arr[0].elements.map((element: ts.Expression) => { + return element.getFullText(sourceFile); + }); + changes.appendChange(new ReplaceChange(fileName, node.getFullStart(), + node.getFullText(sourceFile), + `styles: [require(${initializer.join('), require(')})]`)); + } + }); + + return changes.apply({ + read: (path: string) => Promise.resolve(source), + write: (path: string, content: string) => Promise.resolve(source = content) + }).then(() => source); +} + function _transpile(plugin: AotPlugin, fileName: string, sourceText: string) { const program = plugin.program; if (plugin.typeCheck) { @@ -151,8 +213,15 @@ export function ngcLoader(source: string) { const cb: any = this.async(); Promise.resolve() - .then(() => _removeDecorators(this.resource, source)) - .then(sourceText => _replaceBootstrap(this.resource, sourceText, plugin)) + .then(() => { + if (!plugin.skipCodeGeneration) { + return Promise.resolve() + .then(() => _removeDecorators(this.resource, source)) + .then(sourceText => _replaceBootstrap(this.resource, sourceText, plugin)); + } else { + return _replaceResources(this.resource, source); + } + }) .then(sourceText => { const result = _transpile(plugin, this.resourcePath, sourceText); cb(null, result.outputText, result.sourceMap); diff --git a/packages/webpack/src/plugin.ts b/packages/webpack/src/plugin.ts index e0ff548e834f..c7fda127b6a9 100644 --- a/packages/webpack/src/plugin.ts +++ b/packages/webpack/src/plugin.ts @@ -23,6 +23,8 @@ export interface AotPluginOptions { entryModule?: string; mainPath?: string; typeChecking?: boolean; + + skipCodeGeneration?: boolean; } @@ -69,6 +71,7 @@ export class AotPlugin { private _compilation: any = null; private _typeCheck: boolean = true; + private _skipCodeGeneration: boolean = false; private _basePath: string; private _genDir: string; @@ -85,6 +88,7 @@ export class AotPlugin { get entryModule() { return this._entryModule; } get genDir() { return this._genDir; } get program() { return this._program; } + get skipCodeGeneration() { return this._skipCodeGeneration; } get typeCheck() { return this._typeCheck; } private _setupOptions(options: AotPluginOptions) { @@ -134,6 +138,9 @@ export class AotPlugin { if (options.hasOwnProperty('typeChecking')) { this._typeCheck = options.typeChecking; } + if (options.hasOwnProperty('skipCodeGeneration')) { + this._skipCodeGeneration = options.skipCodeGeneration; + } this._compilerHost = new WebpackCompilerHost(this._compilerOptions); this._program = ts.createProgram( @@ -220,16 +227,20 @@ export class AotPlugin { this._resourceLoader ); + // Create a new Program, based on the old one. This will trigger a resolution of all + // transitive modules, which include files that might just have been generated. + this._program = ts.createProgram( + this._rootFilePath, this._compilerOptions, this._compilerHost, this._program); + // We need to temporarily patch the CodeGenerator until either it's patched or allows us // to pass in our own ReflectorHost. patchReflectorHost(codeGenerator); - this._donePromise = codeGenerator.codegen({transitiveModules: true}) + let promise = Promise.resolve(); + if (!this._skipCodeGeneration) { + promise = codeGenerator.codegen({transitiveModules: true}); + } + this._donePromise = promise .then(() => { - // Create a new Program, based on the old one. This will trigger a resolution of all - // transitive modules, which include files that might just have been generated. - this._program = ts.createProgram( - this._rootFilePath, this._compilerOptions, this._compilerHost, this._program); - const diagnostics = this._program.getGlobalDiagnostics(); if (diagnostics.length > 0) { const message = diagnostics From e272b082a52775eb8542430783ffa534fbad846d Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 17 Nov 2016 18:17:45 -0800 Subject: [PATCH 02/11] Adding a refactor module which is going to be the basis for the low level refactor API --- package.json | 3 + packages/webpack/package.json | 4 +- packages/webpack/src/loader.ts | 153 ++++++++++---------------- packages/webpack/src/plugin.ts | 6 +- packages/webpack/src/refactor.ts | 179 +++++++++++++++++++++++++++++++ 5 files changed, 248 insertions(+), 97 deletions(-) create mode 100644 packages/webpack/src/refactor.ts diff --git a/package.json b/package.json index 917dbb35b21d..ca6cc116f8fc 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "less": "^2.7.1", "less-loader": "^2.2.3", "lodash": "^4.11.1", + "magic-string": "^0.16.0", "markdown-it": "4.3.0", "markdown-it-terminal": "0.0.3", "minimatch": "^3.0.0", @@ -111,6 +112,7 @@ "script-loader": "^0.7.0", "semver": "^5.1.0", "silent-error": "^1.0.0", + "source-map": "^0.5.6", "source-map-loader": "^0.1.5", "sourcemap-istanbul-instrumenter-loader": "^0.2.0", "string-replace-loader": "^1.0.5", @@ -151,6 +153,7 @@ "@types/node": "^6.0.36", "@types/request": "0.0.30", "@types/rimraf": "0.0.25-alpha", + "@types/source-map": "^0.5.0", "@types/webpack": "^1.12.22-alpha", "chai": "^3.5.0", "conventional-changelog": "^1.1.0", diff --git a/packages/webpack/package.json b/packages/webpack/package.json index e8221626e700..88db168a9613 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -25,7 +25,9 @@ "npm": ">= 3.0.0" }, "dependencies": { - "@angular-cli/ast-tools": "^1.0.0" + "@angular-cli/ast-tools": "^1.0.0", + "magic-string": "^0.16.0", + "source-map": "^0.5.6" }, "peerDependencies": { "typescript": "^2.0.2", diff --git a/packages/webpack/src/loader.ts b/packages/webpack/src/loader.ts index 0e8cb1da9a1f..b1b0b0b66160 100644 --- a/packages/webpack/src/loader.ts +++ b/packages/webpack/src/loader.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import * as ts from 'typescript'; import {AotPlugin} from './plugin'; -import {MultiChange, ReplaceChange, insertImport} from '@angular-cli/ast-tools'; +import {TypeScriptFileRefactor} from './refactor'; // TODO: move all this to ast-tools. function _findNodes(sourceFile: ts.SourceFile, node: ts.Node, kind: ts.SyntaxKind, @@ -25,34 +25,23 @@ function _getContentOfKeyLiteral(source: ts.SourceFile, node: ts.Node): string { } } -function _removeDecorators(fileName: string, source: string): string { - const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest); +function _removeDecorators(refactor: TypeScriptFileRefactor) { // Find all decorators. - const decorators = _findNodes(sourceFile, sourceFile, ts.SyntaxKind.Decorator); - decorators.sort((a, b) => b.pos - a.pos); - - decorators.forEach(d => { - source = source.slice(0, d.pos) + source.slice(d.end); - }); - - return source; + _findNodes(refactor.sourceFile, refactor.sourceFile, ts.SyntaxKind.Decorator, false) + .forEach(d => refactor.removeNode(d)); } -function _replaceBootstrap(fileName: string, - source: string, - plugin: AotPlugin): Promise { +function _replaceBootstrap(plugin: AotPlugin, refactor: TypeScriptFileRefactor) { // If bootstrapModule can't be found, bail out early. - if (!source.match(/\bbootstrapModule\b/)) { - return Promise.resolve(source); + if (!refactor.sourceMatch(/\bbootstrapModule\b/)) { + return; } - let changes = new MultiChange(); - // Calculate the base path. const basePath = path.normalize(plugin.basePath); const genDir = path.normalize(plugin.genDir); - const dirName = path.normalize(path.dirname(fileName)); + const dirName = path.normalize(path.dirname(refactor.fileName)); const entryModule = plugin.entryModule; const entryModuleFileName = path.normalize(entryModule.path + '.ngfactory'); const relativeEntryModulePath = path.relative(basePath, entryModuleFileName); @@ -60,10 +49,8 @@ function _replaceBootstrap(fileName: string, const relativeNgFactoryPath = path.relative(dirName, fullEntryModulePath); const ngFactoryPath = './' + relativeNgFactoryPath.replace(/\\/g, '/'); - const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest); - - const allCalls = _findNodes( - sourceFile, sourceFile, ts.SyntaxKind.CallExpression, true) as ts.CallExpression[]; + const allCalls = _findNodes(refactor.sourceFile, refactor.sourceFile, + ts.SyntaxKind.CallExpression, true) as ts.CallExpression[]; const bootstraps = allCalls .filter(call => call.expression.kind == ts.SyntaxKind.PropertyAccessExpression) @@ -73,9 +60,10 @@ function _replaceBootstrap(fileName: string, && access.name.text == 'bootstrapModule'; }); - const calls: ts.Node[] = bootstraps + const calls: ts.CallExpression[] = bootstraps .reduce((previous, access) => { - return previous.concat(_findNodes(sourceFile, access, ts.SyntaxKind.CallExpression, true)); + return previous.concat( + _findNodes(refactor.sourceFile, access, ts.SyntaxKind.CallExpression, true)); }, []) .filter(call => { return call.expression.kind == ts.SyntaxKind.Identifier @@ -84,43 +72,30 @@ function _replaceBootstrap(fileName: string, if (calls.length == 0) { // Didn't find any dynamic bootstrapping going on. - return Promise.resolve(source); + return; } // Create the changes we need. allCalls .filter(call => bootstraps.some(bs => bs == call.expression)) .forEach((call: ts.CallExpression) => { - changes.appendChange(new ReplaceChange(fileName, call.arguments[0].getStart(sourceFile), - entryModule.className, entryModule.className + 'NgFactory')); + refactor.replaceNode(call.arguments[0], entryModule.className + 'NgFactory', true); }); - calls - .forEach(call => { - changes.appendChange(new ReplaceChange(fileName, call.getStart(sourceFile), - 'platformBrowserDynamic', 'platformBrowser')); - }); + calls.forEach(call => refactor.replaceNode(call.expression, 'platformBrowser', true)); bootstraps .forEach((bs: ts.PropertyAccessExpression) => { // This changes the call. - changes.appendChange(new ReplaceChange(fileName, bs.name.getStart(sourceFile), - 'bootstrapModule', 'bootstrapModuleFactory')); + refactor.replaceNode(bs.name, 'bootstrapModuleFactory', true); }); - changes.appendChange(insertImport(fileName, 'platformBrowser', '@angular/platform-browser')); - changes.appendChange(insertImport(fileName, entryModule.className + 'NgFactory', ngFactoryPath)); - - let sourceText = source; - return changes.apply({ - read: (path: string) => Promise.resolve(sourceText), - write: (path: string, content: string) => Promise.resolve(sourceText = content) - }).then(() => sourceText); -} -function _replaceResources(fileName: string, source: string) { - const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest); + refactor.insertImport('platformBrowser', '@angular/platform-browser'); + refactor.insertImport(entryModule.className + 'NgFactory', ngFactoryPath); +} - const changes = new MultiChange(); +function _replaceResources(refactor: TypeScriptFileRefactor) { + const sourceFile = refactor.sourceFile; // Find all object literals. _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ObjectLiteralExpression, true) @@ -140,12 +115,9 @@ function _replaceResources(fileName: string, source: string) { // Get the full text of the initializer. .forEach((node: ts.PropertyAssignment) => { const key = _getContentOfKeyLiteral(sourceFile, node.name); - // changes.appendChange(new ReplaceChange(fileName, call.arguments[0].getStart(sourceFile), - // entryModule.className, entryModule.className + 'NgFactory')); + if (key == 'templateUrl') { - changes.appendChange(new ReplaceChange(fileName, node.getFullStart(), - node.getFullText(sourceFile), - `template: require(${node.initializer.getFullText(sourceFile)})`)); + refactor.replaceNode(node, `template: require(${node.initializer.getFullText(sourceFile)})`); } else if (key == 'styleUrls') { const arr: ts.ArrayLiteralExpression[] = _findNodes(sourceFile, node, @@ -157,52 +129,28 @@ function _replaceResources(fileName: string, source: string) { const initializer = arr[0].elements.map((element: ts.Expression) => { return element.getFullText(sourceFile); }); - changes.appendChange(new ReplaceChange(fileName, node.getFullStart(), - node.getFullText(sourceFile), - `styles: [require(${initializer.join('), require(')})]`)); + refactor.replaceNode(node, `styles: [require(${initializer.join('), require(')})]`); } }); - - return changes.apply({ - read: (path: string) => Promise.resolve(source), - write: (path: string, content: string) => Promise.resolve(source = content) - }).then(() => source); } -function _transpile(plugin: AotPlugin, fileName: string, sourceText: string) { - const program = plugin.program; - if (plugin.typeCheck) { - const sourceFile = program.getSourceFile(fileName); - const diagnostics = program.getSyntacticDiagnostics(sourceFile) - .concat(program.getSemanticDiagnostics(sourceFile)) - .concat(program.getDeclarationDiagnostics(sourceFile)); - - if (diagnostics.length > 0) { - const message = diagnostics - .map(diagnostic => { - const {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); - const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); - return `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message})`; - }) - .join('\n'); - throw new Error(message); - } - } - // Force a few compiler options to make sure we get the result we want. - const compilerOptions: ts.CompilerOptions = Object.assign({}, plugin.compilerOptions, { - inlineSources: true, - inlineSourceMap: false, - sourceRoot: plugin.basePath - }); +function _checkDiagnostics(refactor: TypeScriptFileRefactor) { + const diagnostics = refactor.getDiagnostics(); - const result = ts.transpileModule(sourceText, { compilerOptions, fileName }); - return { - outputText: result.outputText, - sourceMap: JSON.parse(result.sourceMapText) - }; + if (diagnostics.length > 0) { + const message = diagnostics + .map(diagnostic => { + const {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); + return `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message})`; + }) + .join('\n'); + throw new Error(message); + } } + // Super simple TS transpiler loader for testing / isolated usage. does not type check! export function ngcLoader(source: string) { this.cacheable(); @@ -212,18 +160,33 @@ export function ngcLoader(source: string) { if (plugin && plugin instanceof AotPlugin) { const cb: any = this.async(); + const refactor = new TypeScriptFileRefactor( + this.resourcePath, plugin.compilerHost, plugin.program); + Promise.resolve() .then(() => { if (!plugin.skipCodeGeneration) { return Promise.resolve() - .then(() => _removeDecorators(this.resource, source)) - .then(sourceText => _replaceBootstrap(this.resource, sourceText, plugin)); + .then(() => _removeDecorators(refactor)) + .then(() => _replaceBootstrap(plugin, refactor)); } else { - return _replaceResources(this.resource, source); + return _replaceResources(refactor); } }) - .then(sourceText => { - const result = _transpile(plugin, this.resourcePath, sourceText); + .then(() => { + if (plugin.typeCheck) { + _checkDiagnostics(refactor); + } + }) + .then(() => { + // Force a few compiler options to make sure we get the result we want. + const compilerOptions: ts.CompilerOptions = Object.assign({}, plugin.compilerOptions, { + inlineSources: true, + inlineSourceMap: false, + sourceRoot: plugin.basePath + }); + + const result = refactor.transpile(compilerOptions); cb(null, result.outputText, result.sourceMap); }) .catch(err => cb(err)); diff --git a/packages/webpack/src/plugin.ts b/packages/webpack/src/plugin.ts index c7fda127b6a9..10d39b205216 100644 --- a/packages/webpack/src/plugin.ts +++ b/packages/webpack/src/plugin.ts @@ -266,7 +266,11 @@ export class AotPlugin { Object.keys(allLazyRoutes) .forEach(k => { const lazyRoute = allLazyRoutes[k]; - this._lazyRoutes[k + '.ngfactory'] = lazyRoute.moduleAbsolutePath + '.ngfactory.ts'; + if (this.skipCodeGeneration) { + this._lazyRoutes[k] = lazyRoute.moduleAbsolutePath; + } else { + this._lazyRoutes[k + '.ngfactory'] = lazyRoute.moduleAbsolutePath + '.ngfactory.ts'; + } }); }) .then(() => cb(), (err: any) => { cb(err); }); diff --git a/packages/webpack/src/refactor.ts b/packages/webpack/src/refactor.ts new file mode 100644 index 000000000000..06dc89a50c4e --- /dev/null +++ b/packages/webpack/src/refactor.ts @@ -0,0 +1,179 @@ +// TODO: move this in its own package. +import * as ts from 'typescript'; +import {SourceMapConsumer, SourceMapGenerator} from 'source-map'; + +const MagicString = require('magic-string'); + + + +/** + * Find all nodes from the AST in the subtree of node of SyntaxKind kind. + * @param node + * @param kind + * @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 + */ +function _findNodes(node: ts.Node, kind: ts.SyntaxKind, + recursive = false, + max: number = Infinity): ts.Node[] { + if (!node || max == 0) { + return []; + } + + let arr: ts.Node[] = []; + if (node.kind === kind) { + // If we're not recursively looking for children, stop here. + if (!recursive) { + return [node]; + } + + arr.push(node); + max--; + } + if (max > 0) { + for (const child of node.getChildren()) { + _findNodes(child, kind, max).forEach(node => { + if (max > 0) { + arr.push(node); + } + max--; + }); + + if (max <= 0) { + break; + } + } + } + return arr; +} + + + +export interface TranspileOutput { + outputText: string; + sourceMap: any; +} + +export class TypeScriptFileRefactor { + private _sourceFile: ts.SourceFile; + private _sourceString: any; + private _sourceText: string; + private _changed: boolean = false; + + get fileName() { return this._fileName; } + get sourceFile() { return this._sourceFile; } + + constructor(private _fileName: string, + private _host: ts.CompilerHost, + private _program?: ts.Program) { + if (_program) { + this._sourceFile = _program.getSourceFile(_fileName); + } + if (!this._sourceFile) { + this._program = null; + this._sourceFile = ts.createSourceFile(_fileName, _host.readFile(_fileName), + ts.ScriptTarget.Latest); + } + this._sourceText = this._sourceFile.getFullText(this._sourceFile); + this._sourceString = new MagicString(this._sourceText); + } + + getDiagnostics(): ts.Diagnostic[] { + if (!this._program) { + return []; + } + + return this._program.getSyntacticDiagnostics(this._sourceFile) + .concat(this._program.getSemanticDiagnostics(this._sourceFile)) + .concat(this._program.getDeclarationDiagnostics(this._sourceFile)); + } + + appendAfter(node: ts.Node, text: string) { + this._sourceString.insertRight(node.getEnd(), text); + } + + insertImport(symbolName: string, modulePath: string) { + // Find all imports. + const allImports = _findNodes(this._sourceFile, ts.SyntaxKind.ImportDeclaration); + const maybeImports = allImports + .filter((node: ts.ImportDeclaration) => { + // Filter all imports that do not match the modulePath. + return node.moduleSpecifier.kind == ts.SyntaxKind.StringLiteral + && (node.moduleSpecifier as ts.StringLiteral).text == modulePath; + }) + .filter((node: ts.ImportDeclaration) => { + // Remove import statements that are either `import 'XYZ'` or `import * as X from 'XYZ'`. + const clause = node.importClause as ts.ImportClause; + if (!clause || clause.name || !clause.namedBindings) { + return false; + } + return clause.namedBindings.kind == ts.SyntaxKind.NamedImports; + }) + .map((node: ts.ImportDeclaration) => { + // Return the `{ ... }` list of the named import. + return (node.importClause as ts.ImportClause).namedBindings as ts.NamedImports; + }); + + if (maybeImports.length) { + // There's an `import {A, B, C} from 'modulePath'`. + // Find if it's in either imports. If so, just return; nothing to do. + const hasImportAlready = maybeImports.some((node: ts.NamedImports) => { + return node.elements.some((element: ts.ImportSpecifier) => { + return element.name.text == symbolName; + }); + }); + if (hasImportAlready) { + return; + } + // Just pick the first one and insert at the end of its identifier list. + this.appendAfter(maybeImports[0].elements[maybeImports[0].elements.length - 1], + `, ${symbolName}`); + } else { + // Find the last import and insert after. + this.appendAfter(allImports[allImports.length - 1], + `import {${symbolName}} from '${modulePath}';`); + } + } + + removeNode(node: ts.Node) { + this._sourceString.remove(node.getStart(this._sourceFile), node.getEnd()); + this._changed = true; + } + + replaceNode(node: ts.Node, replacement: string, name = false) { + this._sourceString.overwrite(node.getStart(this._sourceFile), node.getEnd(), replacement, name); + this._changed = true; + } + + sourceMatch(re: RegExp) { + return this._sourceText.match(re) !== null; + } + + transpile(compilerOptions: ts.CompilerOptions): TranspileOutput { + const source = this._sourceString.toString(); + console.log(0, this._fileName); + console.log(source); + console.log('---'); + const result = ts.transpileModule(source, { + compilerOptions, + fileName: this._fileName + }); + + const map = SourceMapGenerator.fromSourceMap(new SourceMapConsumer(result.sourceMapText)); + if (this._changed) { + const sourceMap = this._sourceString.generateMap({ + file: this._fileName.replace(/\.ts$/, '.js'), + source: this._fileName, + hires: true, + includeContent: true, + }); + map.applySourceMap(new SourceMapConsumer(sourceMap)); + } + + return { + outputText: result.outputText, + sourceMap: map.toJSON() + }; + } +} From d33eebdc1de3c8937ece8c02589700052abc6af2 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Fri, 18 Nov 2016 15:23:14 -0800 Subject: [PATCH 03/11] fix for codegov --- packages/webpack/src/plugin.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/webpack/src/plugin.ts b/packages/webpack/src/plugin.ts index 10d39b205216..208988558e41 100644 --- a/packages/webpack/src/plugin.ts +++ b/packages/webpack/src/plugin.ts @@ -217,16 +217,6 @@ export class AotPlugin { basePath: this.basePath }; - // Create the Code Generator. - const codeGenerator = ngCompiler.CodeGenerator.create( - this._angularCompilerOptions, - i18nOptions, - this._program, - this._compilerHost, - new ngCompiler.NodeReflectorHostContext(this._compilerHost), - this._resourceLoader - ); - // Create a new Program, based on the old one. This will trigger a resolution of all // transitive modules, which include files that might just have been generated. this._program = ts.createProgram( @@ -234,9 +224,19 @@ export class AotPlugin { // We need to temporarily patch the CodeGenerator until either it's patched or allows us // to pass in our own ReflectorHost. - patchReflectorHost(codeGenerator); let promise = Promise.resolve(); if (!this._skipCodeGeneration) { + // Create the Code Generator. + const codeGenerator = ngCompiler.CodeGenerator.create( + this._angularCompilerOptions, + i18nOptions, + this._program, + this._compilerHost, + new ngCompiler.NodeReflectorHostContext(this._compilerHost), + this._resourceLoader + ); + + patchReflectorHost(codeGenerator); promise = codeGenerator.codegen({transitiveModules: true}); } this._donePromise = promise From 1ae717c8efc93b8cdfe15c7e8e8c2282301d0eee Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Fri, 18 Nov 2016 15:26:07 -0800 Subject: [PATCH 04/11] compile and lint fixes --- .../models/webpack-build-typescript.ts | 36 ------------------- packages/webpack/src/refactor.ts | 5 +-- 2 files changed, 3 insertions(+), 38 deletions(-) diff --git a/packages/angular-cli/models/webpack-build-typescript.ts b/packages/angular-cli/models/webpack-build-typescript.ts index d4d5f41e716e..8371e79c9364 100644 --- a/packages/angular-cli/models/webpack-build-typescript.ts +++ b/packages/angular-cli/models/webpack-build-typescript.ts @@ -1,9 +1,6 @@ import * as path from 'path'; -import * as webpack from 'webpack'; -import {findLazyModules} from './find-lazy-modules'; import {AotPlugin} from '@ngtools/webpack'; -const atl = require('awesome-typescript-loader'); const g: any = global; const webpackLoader: string = g['angularCliIsLocal'] @@ -12,39 +9,6 @@ const webpackLoader: string = g['angularCliIsLocal'] export const getWebpackNonAotConfigPartial = function(projectRoot: string, appConfig: any) { - // const appRoot = path.resolve(projectRoot, appConfig.root); - // const lazyModules = findLazyModules(appRoot); - // - // return { - // resolve: { - // plugins: [ - // new atl.TsConfigPathsPlugin({ - // tsconfig: path.resolve(appRoot, appConfig.tsconfig) - // }) - // ] - // }, - // module: { - // rules: [ - // { - // test: /\.ts$/, - // loaders: [{ - // loader: 'awesome-typescript-loader', - // query: { - // forkChecker: true, - // tsconfig: path.resolve(appRoot, appConfig.tsconfig) - // } - // }, { - // loader: 'angular2-template-loader' - // }], - // exclude: [/\.(spec|e2e)\.ts$/] - // } - // ], - // }, - // plugins: [ - // new webpack.ContextReplacementPlugin(/.*/, appRoot, lazyModules), - // new atl.ForkCheckerPlugin(), - // ] - // }; return { module: { rules: [ diff --git a/packages/webpack/src/refactor.ts b/packages/webpack/src/refactor.ts index 06dc89a50c4e..7d4edf18bf22 100644 --- a/packages/webpack/src/refactor.ts +++ b/packages/webpack/src/refactor.ts @@ -33,7 +33,7 @@ function _findNodes(node: ts.Node, kind: ts.SyntaxKind, } if (max > 0) { for (const child of node.getChildren()) { - _findNodes(child, kind, max).forEach(node => { + _findNodes(child, kind, recursive, max).forEach((node: ts.Node) => { if (max > 0) { arr.push(node); } @@ -160,7 +160,8 @@ export class TypeScriptFileRefactor { fileName: this._fileName }); - const map = SourceMapGenerator.fromSourceMap(new SourceMapConsumer(result.sourceMapText)); + const consumer = new SourceMapConsumer(JSON.parse(result.sourceMapText)); + const map = SourceMapGenerator.fromSourceMap(consumer); if (this._changed) { const sourceMap = this._sourceString.generateMap({ file: this._fileName.replace(/\.ts$/, '.js'), From b401a9619010a264e692674a73c9861a7348ad23 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Mon, 21 Nov 2016 18:19:44 -0500 Subject: [PATCH 05/11] adding path mapping plugin --- packages/webpack/src/paths-plugin.ts | 169 +++++++++++++++++++++++++++ packages/webpack/src/plugin.ts | 27 +++-- packages/webpack/src/refactor.ts | 3 - packages/webpack/src/webpack.ts | 23 ++++ 4 files changed, 212 insertions(+), 10 deletions(-) create mode 100644 packages/webpack/src/paths-plugin.ts create mode 100644 packages/webpack/src/webpack.ts diff --git a/packages/webpack/src/paths-plugin.ts b/packages/webpack/src/paths-plugin.ts new file mode 100644 index 000000000000..975ad4b247e6 --- /dev/null +++ b/packages/webpack/src/paths-plugin.ts @@ -0,0 +1,169 @@ +import * as path from 'path'; +import * as ts from 'typescript'; +import {Request, ResolverPlugin, Callback, Tapable} from './webpack'; + + +const ModulesInRootPlugin: new (a: string, b: string, c: string) => ResolverPlugin + = require('enhanced-resolve/lib/ModulesInRootPlugin'); + +interface CreateInnerCallback { + (callback: Callback, + options: Callback, + message?: string, + messageOptional?: string): Callback; +} + +const createInnerCallback: CreateInnerCallback + = require('enhanced-resolve/lib/createInnerCallback'); +const getInnerRequest: (resolver: ResolverPlugin, request: Request) => string + = require('enhanced-resolve/lib/getInnerRequest'); + + +function escapeRegExp(str: string): string { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); +} + + +export interface PathsPluginOptions { + tsConfigPath: string; + compilerOptions?: ts.CompilerOptions; + compilerHost?: ts.CompilerHost; +} + +export class PathsPlugin implements Tapable { + private _tsConfigPath: string; + private _compilerOptions: ts.CompilerOptions; + private _host: ts.CompilerHost; + + source: string; + target: string; + + private mappings: any; + + private _absoluteBaseUrl: string; + + private static _loadOptionsFromTsConfig(tsConfigPath: string, host: ts.CompilerHost): + ts.CompilerOptions { + const tsConfig = ts.readConfigFile(tsConfigPath, (path: string) => host.readFile(path)); + if (tsConfig.error) { + throw tsConfig.error; + } + return tsConfig.config; + } + + constructor(options: PathsPluginOptions) { + if (!options.hasOwnProperty('tsConfigPath')) { + // This could happen in JavaScript. + throw new Error('tsConfigPath option is mandatory.'); + } + this._tsConfigPath = options.tsConfigPath; + + if (options.hasOwnProperty('compilerHost')) { + this._host = options.compilerHost; + } else { + this._host = ts.createCompilerHost(this._compilerOptions, false); + } + + if (options.hasOwnProperty('compilerOptions')) { + this._compilerOptions = Object.assign({}, options.compilerOptions); + } else { + this._compilerOptions = PathsPlugin._loadOptionsFromTsConfig(this._tsConfigPath, this._host); + } + + this.source = 'described-resolve'; + this.target = 'resolve'; + + this._absoluteBaseUrl = path.resolve( + path.dirname(this._tsConfigPath), + this._compilerOptions.baseUrl || '.' + ); + + this.mappings = []; + let paths = this._compilerOptions.paths || {}; + Object.keys(paths).forEach(alias => { + let onlyModule = alias.indexOf('*') === -1; + let excapedAlias = escapeRegExp(alias); + let targets = paths[alias]; + targets.forEach(target => { + let aliasPattern: RegExp; + if (onlyModule) { + aliasPattern = new RegExp(`^${excapedAlias}$`); + } else { + let withStarCapturing = excapedAlias.replace('\\*', '(.*)'); + aliasPattern = new RegExp(`^${withStarCapturing}`); + } + + this.mappings.push({ + onlyModule, + alias, + aliasPattern, + target: target + }); + }); + }); + } + + apply(resolver: ResolverPlugin) { + let { baseUrl } = this._compilerOptions; + + if (baseUrl) { + resolver.apply(new ModulesInRootPlugin('module', this._absoluteBaseUrl, 'resolve')); + } + + this.mappings.forEach((mapping: any) => { + resolver.plugin(this.source, this.createPlugin(resolver, mapping)); + }); + } + + resolve(resolver: ResolverPlugin, mapping: any, request: any, callback: Callback) { + let innerRequest = getInnerRequest(resolver, request); + if (!innerRequest) { + return callback(); + } + + let match = innerRequest.match(mapping.aliasPattern); + if (!match) { + return callback(); + } + console.log(3, innerRequest); + + let newRequestStr = mapping.target; + if (!mapping.onlyModule) { + newRequestStr = newRequestStr.replace('*', match[1]); + } + if (newRequestStr[0] === '.') { + newRequestStr = path.resolve(this._absoluteBaseUrl, newRequestStr); + } + + let newRequest = Object.assign({}, request, { + request: newRequestStr + }) as Request; + + return resolver.doResolve( + this.target, + newRequest, + `aliased with mapping '${innerRequest}': '${mapping.alias}' to '${newRequestStr}'`, + createInnerCallback( + function(err, result) { + if (arguments.length > 0) { + return callback(err, result); + } + + // don't allow other aliasing or raw request + callback(null, null); + }, + callback + ) + ); + } + + createPlugin(resolver: ResolverPlugin, mapping: any) { + return (request: any, callback: Callback) => { + try { + this.resolve(resolver, mapping, request, callback); + } catch (err) { + callback(err); + } + }; + } +} diff --git a/packages/webpack/src/plugin.ts b/packages/webpack/src/plugin.ts index 208988558e41..200046018266 100644 --- a/packages/webpack/src/plugin.ts +++ b/packages/webpack/src/plugin.ts @@ -12,6 +12,8 @@ import {createResolveDependenciesFromContextMap} from './utils'; import {WebpackCompilerHost} from './compiler_host'; import {resolveEntryModuleFromMain} from './entry_resolver'; import {StaticSymbol} from '@angular/compiler-cli'; +import {Tapable} from './webpack'; +import {PathsPlugin} from './paths-plugin'; /** @@ -54,7 +56,7 @@ export class ModuleRoute { } -export class AotPlugin { +export class AotPlugin implements Tapable { private _entryModule: ModuleRoute; private _compilerOptions: ts.CompilerOptions; private _angularCompilerOptions: ngCompiler.AngularCompilerOptions; @@ -65,6 +67,7 @@ export class AotPlugin { private _compilerHost: WebpackCompilerHost; private _resourceLoader: WebpackResourceLoader; private _lazyRoutes: { [route: string]: string }; + private _tsConfigPath: string; private _donePromise: Promise; private _compiler: any = null; @@ -96,17 +99,18 @@ export class AotPlugin { if (!options.hasOwnProperty('tsConfigPath')) { throw new Error('Must specify "tsConfigPath" in the configuration of @ngtools/webpack.'); } + this._tsConfigPath = options.tsConfigPath; // Check the base path. - let basePath = path.resolve(process.cwd(), path.dirname(options.tsConfigPath)); - if (fs.statSync(options.tsConfigPath).isDirectory()) { - basePath = options.tsConfigPath; + let basePath = path.resolve(process.cwd(), path.dirname(this._tsConfigPath)); + if (fs.statSync(this._tsConfigPath).isDirectory()) { + basePath = this._tsConfigPath; } if (options.hasOwnProperty('basePath')) { basePath = options.basePath; } - const tsConfig = tsc.readConfiguration(options.tsConfigPath, basePath); + const tsConfig = tsc.readConfiguration(this._tsConfigPath, basePath); this._rootFilePath = tsConfig.parsed.fileNames .filter(fileName => !/\.spec\.ts$/.test(fileName)); @@ -155,13 +159,19 @@ export class AotPlugin { this._compiler = compiler; compiler.plugin('context-module-factory', (cmf: any) => { + cmf.resolvers.normal.apply(new PathsPlugin({ + tsConfigPath: this._tsConfigPath, + compilerOptions: this._compilerOptions, + compilerHost: this._compilerHost + })); + cmf.plugin('before-resolve', (request: any, callback: (err?: any, request?: any) => void) => { if (!request) { return callback(); } request.request = this.genDir; - request.recursive = true; + request.recursive = true; request.dependencies.forEach((d: any) => d.critical = false); return callback(null, request); }); @@ -237,8 +247,11 @@ export class AotPlugin { ); patchReflectorHost(codeGenerator); - promise = codeGenerator.codegen({transitiveModules: true}); + promise = promise.then(() => codeGenerator.codegen({ + transitiveModules: true + })); } + this._donePromise = promise .then(() => { const diagnostics = this._program.getGlobalDiagnostics(); diff --git a/packages/webpack/src/refactor.ts b/packages/webpack/src/refactor.ts index 7d4edf18bf22..4c8ec0f95a83 100644 --- a/packages/webpack/src/refactor.ts +++ b/packages/webpack/src/refactor.ts @@ -152,9 +152,6 @@ export class TypeScriptFileRefactor { transpile(compilerOptions: ts.CompilerOptions): TranspileOutput { const source = this._sourceString.toString(); - console.log(0, this._fileName); - console.log(source); - console.log('---'); const result = ts.transpileModule(source, { compilerOptions, fileName: this._fileName diff --git a/packages/webpack/src/webpack.ts b/packages/webpack/src/webpack.ts new file mode 100644 index 000000000000..0f21dcf1e93d --- /dev/null +++ b/packages/webpack/src/webpack.ts @@ -0,0 +1,23 @@ +export interface Request { + request?: Request; + relativePath: string; +} + +export interface Callback { + (err?: Error | null, result?: T): void; +} + +export interface ResolverCallback { + (request: Request, callback: Callback): void; +} + +export interface Tapable { + apply(plugin: ResolverPlugin): void; +} + +export interface ResolverPlugin extends Tapable { + plugin(source: string, cb: ResolverCallback): void; + doResolve(target: string, req: Request, desc: string, callback: Callback): void; + join(relativePath: string, innerRequest: Request): Request; +} + From d323e6caceefe2e79a361c8dd41856284e7f6436 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Tue, 22 Nov 2016 14:19:00 -0500 Subject: [PATCH 06/11] fixing e2e bug with aot --- packages/webpack/src/compiler_host.ts | 1 + packages/webpack/src/loader.ts | 6 +++--- packages/webpack/src/plugin.ts | 15 +++++++++------ packages/webpack/src/refactor.ts | 8 ++++++-- packages/webpack/src/webpack.ts | 2 ++ 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/webpack/src/compiler_host.ts b/packages/webpack/src/compiler_host.ts index a97e0c57d265..2c15bbd21804 100644 --- a/packages/webpack/src/compiler_host.ts +++ b/packages/webpack/src/compiler_host.ts @@ -116,6 +116,7 @@ export class WebpackCompilerHost implements ts.CompilerHost { } for (const fileName of Object.keys(this._files)) { + console.log(1, fileName); const stats = this._files[fileName]; fs._statStorage.data[fileName] = [null, stats]; fs._readFileStorage.data[fileName] = [null, stats.content]; diff --git a/packages/webpack/src/loader.ts b/packages/webpack/src/loader.ts index b1b0b0b66160..e74f51023011 100644 --- a/packages/webpack/src/loader.ts +++ b/packages/webpack/src/loader.ts @@ -79,15 +79,15 @@ function _replaceBootstrap(plugin: AotPlugin, refactor: TypeScriptFileRefactor) allCalls .filter(call => bootstraps.some(bs => bs == call.expression)) .forEach((call: ts.CallExpression) => { - refactor.replaceNode(call.arguments[0], entryModule.className + 'NgFactory', true); + refactor.replaceNode(call.arguments[0], entryModule.className + 'NgFactory'); }); - calls.forEach(call => refactor.replaceNode(call.expression, 'platformBrowser', true)); + calls.forEach(call => refactor.replaceNode(call.expression, 'platformBrowser')); bootstraps .forEach((bs: ts.PropertyAccessExpression) => { // This changes the call. - refactor.replaceNode(bs.name, 'bootstrapModuleFactory', true); + refactor.replaceNode(bs.name, 'bootstrapModuleFactory'); }); refactor.insertImport('platformBrowser', '@angular/platform-browser'); diff --git a/packages/webpack/src/plugin.ts b/packages/webpack/src/plugin.ts index 200046018266..bdc574e861c8 100644 --- a/packages/webpack/src/plugin.ts +++ b/packages/webpack/src/plugin.ts @@ -188,7 +188,7 @@ export class AotPlugin implements Tapable { (_: any, cb: any) => cb(null, this._lazyRoutes)); return callback(null, result); - }); + }).catch((err) => callback(err)); }); }); @@ -227,11 +227,6 @@ export class AotPlugin implements Tapable { basePath: this.basePath }; - // Create a new Program, based on the old one. This will trigger a resolution of all - // transitive modules, which include files that might just have been generated. - this._program = ts.createProgram( - this._rootFilePath, this._compilerOptions, this._compilerHost, this._program); - // We need to temporarily patch the CodeGenerator until either it's patched or allows us // to pass in our own ReflectorHost. let promise = Promise.resolve(); @@ -253,6 +248,14 @@ export class AotPlugin implements Tapable { } this._donePromise = promise + .then(() => { + // Create a new Program, based on the old one. This will trigger a resolution of all + // transitive modules, which include files that might just have been generated. + // This needs to happen after the code generator has been created for generated files + // to be properly resolved. + this._program = ts.createProgram( + this._rootFilePath, this._compilerOptions, this._compilerHost, this._program); + }) .then(() => { const diagnostics = this._program.getGlobalDiagnostics(); if (diagnostics.length > 0) { diff --git a/packages/webpack/src/refactor.ts b/packages/webpack/src/refactor.ts index 4c8ec0f95a83..d41b3b6fb383 100644 --- a/packages/webpack/src/refactor.ts +++ b/packages/webpack/src/refactor.ts @@ -141,8 +141,12 @@ export class TypeScriptFileRefactor { this._changed = true; } - replaceNode(node: ts.Node, replacement: string, name = false) { - this._sourceString.overwrite(node.getStart(this._sourceFile), node.getEnd(), replacement, name); + replaceNode(node: ts.Node, replacement: string) { + let replaceSymbolName: boolean = node.kind === ts.SyntaxKind.Identifier; + this._sourceString.overwrite(node.getStart(this._sourceFile), + node.getEnd(), + replacement, + replaceSymbolName); this._changed = true; } diff --git a/packages/webpack/src/webpack.ts b/packages/webpack/src/webpack.ts index 0f21dcf1e93d..0074bed42e12 100644 --- a/packages/webpack/src/webpack.ts +++ b/packages/webpack/src/webpack.ts @@ -1,3 +1,5 @@ +// Declarations for (some) Webpack types. Only what's needed. + export interface Request { request?: Request; relativePath: string; From b696caa1667858d0f05ce223a9131fdbacbfe35d Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Tue, 22 Nov 2016 15:32:00 -0500 Subject: [PATCH 07/11] comments, round 2 --- packages/webpack/src/compiler_host.ts | 1 - packages/webpack/src/plugin.ts | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/webpack/src/compiler_host.ts b/packages/webpack/src/compiler_host.ts index 2c15bbd21804..a97e0c57d265 100644 --- a/packages/webpack/src/compiler_host.ts +++ b/packages/webpack/src/compiler_host.ts @@ -116,7 +116,6 @@ export class WebpackCompilerHost implements ts.CompilerHost { } for (const fileName of Object.keys(this._files)) { - console.log(1, fileName); const stats = this._files[fileName]; fs._statStorage.data[fileName] = [null, stats]; fs._readFileStorage.data[fileName] = [null, stats.content]; diff --git a/packages/webpack/src/plugin.ts b/packages/webpack/src/plugin.ts index bdc574e861c8..ea4d7fd3b316 100644 --- a/packages/webpack/src/plugin.ts +++ b/packages/webpack/src/plugin.ts @@ -227,8 +227,6 @@ export class AotPlugin implements Tapable { basePath: this.basePath }; - // We need to temporarily patch the CodeGenerator until either it's patched or allows us - // to pass in our own ReflectorHost. let promise = Promise.resolve(); if (!this._skipCodeGeneration) { // Create the Code Generator. @@ -241,6 +239,9 @@ export class AotPlugin implements Tapable { this._resourceLoader ); + // We need to temporarily patch the CodeGenerator until either it's patched or allows us + // to pass in our own ReflectorHost. + // TODO: remove this. patchReflectorHost(codeGenerator); promise = promise.then(() => codeGenerator.codegen({ transitiveModules: true From c00a7fe996ee6d6096158e3592007be76772f1d9 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Tue, 22 Nov 2016 15:55:20 -0500 Subject: [PATCH 08/11] comments, round 3 --- packages/webpack/src/loader.ts | 89 +++++++++++--------------- packages/webpack/src/paths-plugin.ts | 7 +- packages/webpack/src/refactor.ts | 96 ++++++++++++++-------------- 3 files changed, 90 insertions(+), 102 deletions(-) diff --git a/packages/webpack/src/loader.ts b/packages/webpack/src/loader.ts index e74f51023011..4b7e597e5c23 100644 --- a/packages/webpack/src/loader.ts +++ b/packages/webpack/src/loader.ts @@ -3,18 +3,6 @@ import * as ts from 'typescript'; import {AotPlugin} from './plugin'; import {TypeScriptFileRefactor} from './refactor'; -// TODO: move all this to ast-tools. -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 _getContentOfKeyLiteral(source: ts.SourceFile, node: ts.Node): string { if (node.kind == ts.SyntaxKind.Identifier) { return (node as ts.Identifier).text; @@ -27,7 +15,7 @@ function _getContentOfKeyLiteral(source: ts.SourceFile, node: ts.Node): string { function _removeDecorators(refactor: TypeScriptFileRefactor) { // Find all decorators. - _findNodes(refactor.sourceFile, refactor.sourceFile, ts.SyntaxKind.Decorator, false) + refactor.findAstNodes(refactor.sourceFile, ts.SyntaxKind.Decorator) .forEach(d => refactor.removeNode(d)); } @@ -49,7 +37,7 @@ function _replaceBootstrap(plugin: AotPlugin, refactor: TypeScriptFileRefactor) const relativeNgFactoryPath = path.relative(dirName, fullEntryModulePath); const ngFactoryPath = './' + relativeNgFactoryPath.replace(/\\/g, '/'); - const allCalls = _findNodes(refactor.sourceFile, refactor.sourceFile, + const allCalls = refactor.findAstNodes(refactor.sourceFile, ts.SyntaxKind.CallExpression, true) as ts.CallExpression[]; const bootstraps = allCalls @@ -62,12 +50,13 @@ function _replaceBootstrap(plugin: AotPlugin, refactor: TypeScriptFileRefactor) const calls: ts.CallExpression[] = bootstraps .reduce((previous, access) => { - return previous.concat( - _findNodes(refactor.sourceFile, access, ts.SyntaxKind.CallExpression, true)); + const expressions + = refactor.findAstNodes(access, ts.SyntaxKind.CallExpression, true) as ts.CallExpression[]; + return previous.concat(expressions); }, []) - .filter(call => { + .filter((call: ts.CallExpression) => { return call.expression.kind == ts.SyntaxKind.Identifier - && call.expression.text == 'platformBrowserDynamic'; + && (call.expression as ts.Identifier).text == 'platformBrowserDynamic'; }); if (calls.length == 0) { @@ -98,40 +87,40 @@ function _replaceResources(refactor: TypeScriptFileRefactor) { const sourceFile = refactor.sourceFile; // Find all object literals. - _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ObjectLiteralExpression, true) - // Get all their property assignments. - .map(node => _findNodes(sourceFile, node, ts.SyntaxKind.PropertyAssignment)) - // Flatten into a single array (from an array of array). - .reduce((prev, curr) => curr ? prev.concat(curr) : prev, []) - // Remove every property assignment that aren't 'loadChildren'. - .filter((node: ts.PropertyAssignment) => { - const key = _getContentOfKeyLiteral(sourceFile, node.name); - if (!key) { - // key is an expression, can't do anything. - return false; - } - return key == 'templateUrl' || key == 'styleUrls'; - }) - // Get the full text of the initializer. - .forEach((node: ts.PropertyAssignment) => { - const key = _getContentOfKeyLiteral(sourceFile, node.name); - - if (key == 'templateUrl') { - refactor.replaceNode(node, `template: require(${node.initializer.getFullText(sourceFile)})`); - } else if (key == 'styleUrls') { - const arr: ts.ArrayLiteralExpression[] = - _findNodes(sourceFile, node, - ts.SyntaxKind.ArrayLiteralExpression, false); - if (!arr || arr.length == 0 || arr[0].elements.length == 0) { - return; + refactor.findAstNodes(sourceFile, ts.SyntaxKind.ObjectLiteralExpression, true) + // Get all their property assignments. + .map(node => refactor.findAstNodes(node, ts.SyntaxKind.PropertyAssignment)) + // Flatten into a single array (from an array of array). + .reduce((prev, curr) => curr ? prev.concat(curr) : prev, []) + // Remove every property assignment that aren't 'loadChildren'. + .filter((node: ts.PropertyAssignment) => { + const key = _getContentOfKeyLiteral(sourceFile, node.name); + if (!key) { + // key is an expression, can't do anything. + return false; } + return key == 'templateUrl' || key == 'styleUrls'; + }) + // Get the full text of the initializer. + .forEach((node: ts.PropertyAssignment) => { + const key = _getContentOfKeyLiteral(sourceFile, node.name); + + if (key == 'templateUrl') { + refactor.replaceNode(node, + `template: require(${node.initializer.getFullText(sourceFile)})`); + } else if (key == 'styleUrls') { + const arr = ( + refactor.findAstNodes(node, ts.SyntaxKind.ArrayLiteralExpression, false)); + if (!arr || arr.length == 0 || arr[0].elements.length == 0) { + return; + } - const initializer = arr[0].elements.map((element: ts.Expression) => { - return element.getFullText(sourceFile); - }); - refactor.replaceNode(node, `styles: [require(${initializer.join('), require(')})]`); - } - }); + const initializer = arr[0].elements.map((element: ts.Expression) => { + return element.getFullText(sourceFile); + }); + refactor.replaceNode(node, `styles: [require(${initializer.join('), require(')})]`); + } + }); } diff --git a/packages/webpack/src/paths-plugin.ts b/packages/webpack/src/paths-plugin.ts index 975ad4b247e6..60bf8d8a419e 100644 --- a/packages/webpack/src/paths-plugin.ts +++ b/packages/webpack/src/paths-plugin.ts @@ -103,7 +103,7 @@ export class PathsPlugin implements Tapable { }); } - apply(resolver: ResolverPlugin) { + apply(resolver: ResolverPlugin): void { let { baseUrl } = this._compilerOptions; if (baseUrl) { @@ -115,7 +115,7 @@ export class PathsPlugin implements Tapable { }); } - resolve(resolver: ResolverPlugin, mapping: any, request: any, callback: Callback) { + resolve(resolver: ResolverPlugin, mapping: any, request: any, callback: Callback): any { let innerRequest = getInnerRequest(resolver, request); if (!innerRequest) { return callback(); @@ -125,7 +125,6 @@ export class PathsPlugin implements Tapable { if (!match) { return callback(); } - console.log(3, innerRequest); let newRequestStr = mapping.target; if (!mapping.onlyModule) { @@ -157,7 +156,7 @@ export class PathsPlugin implements Tapable { ); } - createPlugin(resolver: ResolverPlugin, mapping: any) { + createPlugin(resolver: ResolverPlugin, mapping: any): any { return (request: any, callback: Callback) => { try { this.resolve(resolver, mapping, request, callback); diff --git a/packages/webpack/src/refactor.ts b/packages/webpack/src/refactor.ts index d41b3b6fb383..55a769b8d096 100644 --- a/packages/webpack/src/refactor.ts +++ b/packages/webpack/src/refactor.ts @@ -5,51 +5,6 @@ import {SourceMapConsumer, SourceMapGenerator} from 'source-map'; const MagicString = require('magic-string'); - -/** - * Find all nodes from the AST in the subtree of node of SyntaxKind kind. - * @param node - * @param kind - * @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 - */ -function _findNodes(node: ts.Node, kind: ts.SyntaxKind, - recursive = false, - max: number = Infinity): ts.Node[] { - if (!node || max == 0) { - return []; - } - - let arr: ts.Node[] = []; - if (node.kind === kind) { - // If we're not recursively looking for children, stop here. - if (!recursive) { - return [node]; - } - - arr.push(node); - max--; - } - if (max > 0) { - for (const child of node.getChildren()) { - _findNodes(child, kind, recursive, max).forEach((node: ts.Node) => { - if (max > 0) { - arr.push(node); - } - max--; - }); - - if (max <= 0) { - break; - } - } - } - return arr; -} - - - export interface TranspileOutput { outputText: string; sourceMap: any; @@ -89,13 +44,58 @@ export class TypeScriptFileRefactor { .concat(this._program.getDeclarationDiagnostics(this._sourceFile)); } - appendAfter(node: ts.Node, text: string) { + /** + * Find all nodes from the AST in the subtree of node of SyntaxKind kind. + * @param node + * @param kind + * @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, + kind: ts.SyntaxKind, + recursive = false, + max: number = Infinity): ts.Node[] { + if (!node || max == 0) { + return []; + } + + let arr: ts.Node[] = []; + if (node.kind === kind) { + // If we're not recursively looking for children, stop here. + if (!recursive) { + return [node]; + } + + arr.push(node); + max--; + } + + if (max > 0) { + for (const child of node.getChildren(this._sourceFile)) { + this.findAstNodes(child, kind, recursive, max) + .forEach((node: ts.Node) => { + if (max > 0) { + arr.push(node); + } + max--; + }); + + if (max <= 0) { + break; + } + } + } + return arr; + } + + appendAfter(node: ts.Node, text: string): void { this._sourceString.insertRight(node.getEnd(), text); } - insertImport(symbolName: string, modulePath: string) { + insertImport(symbolName: string, modulePath: string): void { // Find all imports. - const allImports = _findNodes(this._sourceFile, ts.SyntaxKind.ImportDeclaration); + const allImports = this.findAstNodes(this._sourceFile, ts.SyntaxKind.ImportDeclaration); const maybeImports = allImports .filter((node: ts.ImportDeclaration) => { // Filter all imports that do not match the modulePath. From 3d83a7aa787203e0cbdaeb80db273caf26cc0237 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Tue, 22 Nov 2016 16:51:39 -0500 Subject: [PATCH 09/11] removing a-t-l entirely --- package.json | 2 +- .../angular-cli/models/webpack-build-test.js | 15 +++++++++---- packages/angular-cli/package.json | 2 +- packages/webpack/src/index.ts | 5 +++-- packages/webpack/src/paths-plugin.ts | 22 ++++++++++++------- 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index ca6cc116f8fc..ba1f7626bfdc 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "@angular/tsc-wrapped": "0.4.0", "angular2-template-loader": "^0.5.0", "autoprefixer": "^6.5.3", - "awesome-typescript-loader": "^2.2.3", "chalk": "^1.1.3", "common-tags": "^1.3.1", "compression-webpack-plugin": "^0.3.2", @@ -60,6 +59,7 @@ "ember-cli-normalize-entity-name": "^1.0.0", "ember-cli-preprocess-registry": "^2.0.0", "ember-cli-string-utils": "^1.0.0", + "enhanced-resolve": "^2.3.0", "exists-sync": "0.0.3", "extract-text-webpack-plugin": "^2.0.0-beta.4", "file-loader": "^0.8.5", diff --git a/packages/angular-cli/models/webpack-build-test.js b/packages/angular-cli/models/webpack-build-test.js index 8361c87a00b4..c021f8b4b23e 100644 --- a/packages/angular-cli/models/webpack-build-test.js +++ b/packages/angular-cli/models/webpack-build-test.js @@ -2,7 +2,14 @@ const path = require('path'); const webpack = require('webpack'); -const atl = require('awesome-typescript-loader'); +const ngtools = require('@ngtools/webpack'); + + +const g = global; +const webpackLoader = g['angularCliIsLocal'] + ? g.angularCliPackages['@ngtools/webpack'].main + : '@ngtools/webpack'; + const getWebpackTestConfig = function (projectRoot, environment, appConfig, testConfig) { @@ -48,8 +55,8 @@ const getWebpackTestConfig = function (projectRoot, environment, appConfig, test resolve: { extensions: ['.ts', '.js'], plugins: [ - new atl.TsConfigPathsPlugin({ - tsconfig: path.resolve(appRoot, appConfig.tsconfig) + new ngtools.PathsPlugin({ + tsConfigPath: path.resolve(appRoot, appConfig.tsconfig) }) ] }, @@ -74,7 +81,7 @@ const getWebpackTestConfig = function (projectRoot, environment, appConfig, test test: /\.ts$/, loaders: [ { - loader: 'awesome-typescript-loader', + loader: webpackLoader, query: { tsconfig: path.resolve(appRoot, appConfig.tsconfig), module: 'commonjs', diff --git a/packages/angular-cli/package.json b/packages/angular-cli/package.json index b31db9bbde02..af055c584489 100644 --- a/packages/angular-cli/package.json +++ b/packages/angular-cli/package.json @@ -32,7 +32,6 @@ "@angular/core": "2.2.1", "@ngtools/webpack": "^1.0.0", "angular2-template-loader": "^0.5.0", - "awesome-typescript-loader": "^2.2.3", "chalk": "^1.1.3", "common-tags": "^1.3.1", "compression-webpack-plugin": "^0.3.2", @@ -45,6 +44,7 @@ "ember-cli-normalize-entity-name": "^1.0.0", "ember-cli-preprocess-registry": "^2.0.0", "ember-cli-string-utils": "^1.0.0", + "enhanced-resolve": "^2.3.0", "exists-sync": "0.0.3", "extract-text-webpack-plugin": "^2.0.0-beta.4", "file-loader": "^0.8.5", diff --git a/packages/webpack/src/index.ts b/packages/webpack/src/index.ts index 8fd3bad21818..067d1e724697 100644 --- a/packages/webpack/src/index.ts +++ b/packages/webpack/src/index.ts @@ -1,4 +1,5 @@ import 'reflect-metadata'; -export * from './plugin' -export {ngcLoader as default} from './loader' +export * from './plugin'; +export {ngcLoader as default} from './loader'; +export {PathsPlugin} from './paths-plugin'; diff --git a/packages/webpack/src/paths-plugin.ts b/packages/webpack/src/paths-plugin.ts index 60bf8d8a419e..b4ec576d462d 100644 --- a/packages/webpack/src/paths-plugin.ts +++ b/packages/webpack/src/paths-plugin.ts @@ -42,9 +42,15 @@ export class PathsPlugin implements Tapable { private _absoluteBaseUrl: string; - private static _loadOptionsFromTsConfig(tsConfigPath: string, host: ts.CompilerHost): + private static _loadOptionsFromTsConfig(tsConfigPath: string, host?: ts.CompilerHost): ts.CompilerOptions { - const tsConfig = ts.readConfigFile(tsConfigPath, (path: string) => host.readFile(path)); + const tsConfig = ts.readConfigFile(tsConfigPath, (path: string) => { + if (host) { + return host.readFile(path); + } else { + return ts.sys.readFile(path); + } + }); if (tsConfig.error) { throw tsConfig.error; } @@ -58,16 +64,16 @@ export class PathsPlugin implements Tapable { } this._tsConfigPath = options.tsConfigPath; - if (options.hasOwnProperty('compilerHost')) { - this._host = options.compilerHost; + if (options.hasOwnProperty('compilerOptions')) { + this._compilerOptions = Object.assign({}, options.compilerOptions); } else { - this._host = ts.createCompilerHost(this._compilerOptions, false); + this._compilerOptions = PathsPlugin._loadOptionsFromTsConfig(this._tsConfigPath, null); } - if (options.hasOwnProperty('compilerOptions')) { - this._compilerOptions = Object.assign({}, options.compilerOptions); + if (options.hasOwnProperty('compilerHost')) { + this._host = options.compilerHost; } else { - this._compilerOptions = PathsPlugin._loadOptionsFromTsConfig(this._tsConfigPath, this._host); + this._host = ts.createCompilerHost(this._compilerOptions, false); } this.source = 'described-resolve'; From 63550b337db3df0a0d6e33234210169dd0abe8a7 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 23 Nov 2016 00:40:37 -0500 Subject: [PATCH 10/11] fix coverage --- .../angular-cli/models/webpack-build-test.js | 9 ++--- packages/webpack/src/loader.ts | 33 ++++++++++++---- packages/webpack/src/refactor.ts | 38 +++++++++++-------- 3 files changed, 50 insertions(+), 30 deletions(-) diff --git a/packages/angular-cli/models/webpack-build-test.js b/packages/angular-cli/models/webpack-build-test.js index c021f8b4b23e..32d2c8400bca 100644 --- a/packages/angular-cli/models/webpack-build-test.js +++ b/packages/angular-cli/models/webpack-build-test.js @@ -83,14 +83,11 @@ const getWebpackTestConfig = function (projectRoot, environment, appConfig, test { loader: webpackLoader, query: { - tsconfig: path.resolve(appRoot, appConfig.tsconfig), + tsConfigPath: path.resolve(appRoot, appConfig.tsconfig), module: 'commonjs', - target: 'es5', - forkChecker: true + inlineSourceMap: true, + sourceRoot: appRoot } - }, - { - loader: 'angular2-template-loader' } ], exclude: [/\.e2e\.ts$/] diff --git a/packages/webpack/src/loader.ts b/packages/webpack/src/loader.ts index 4b7e597e5c23..a351600f1588 100644 --- a/packages/webpack/src/loader.ts +++ b/packages/webpack/src/loader.ts @@ -3,6 +3,9 @@ import * as ts from 'typescript'; import {AotPlugin} from './plugin'; import {TypeScriptFileRefactor} from './refactor'; +const loaderUtils = require('loader-utils'); + + function _getContentOfKeyLiteral(source: ts.SourceFile, node: ts.Node): string { if (node.kind == ts.SyntaxKind.Identifier) { return (node as ts.Identifier).text; @@ -83,7 +86,7 @@ function _replaceBootstrap(plugin: AotPlugin, refactor: TypeScriptFileRefactor) refactor.insertImport(entryModule.className + 'NgFactory', ngFactoryPath); } -function _replaceResources(refactor: TypeScriptFileRefactor) { +function _replaceResources(refactor: TypeScriptFileRefactor): void { const sourceFile = refactor.sourceFile; // Find all object literals. @@ -143,12 +146,11 @@ function _checkDiagnostics(refactor: TypeScriptFileRefactor) { // Super simple TS transpiler loader for testing / isolated usage. does not type check! export function ngcLoader(source: string) { this.cacheable(); + const cb: any = this.async(); const plugin = this._compilation._ngToolsWebpackPluginInstance as AotPlugin; // We must verify that AotPlugin is an instance of the right class. if (plugin && plugin instanceof AotPlugin) { - const cb: any = this.async(); - const refactor = new TypeScriptFileRefactor( this.resourcePath, plugin.compilerHost, plugin.program); @@ -180,11 +182,26 @@ export function ngcLoader(source: string) { }) .catch(err => cb(err)); } else { - return ts.transpileModule(source, { - compilerOptions: { - target: ts.ScriptTarget.ES5, - module: ts.ModuleKind.ES2015, + const options = loaderUtils.parseQuery(this.query); + const tsConfigPath = options.tsConfigPath; + const tsConfig = ts.readConfigFile(tsConfigPath, ts.sys.readFile); + + if (tsConfig.error) { + throw tsConfig.error; + } + + const compilerOptions = tsConfig.config.compilerOptions as ts.CompilerOptions; + for (const key of Object.keys(options)) { + if (key == 'tsConfigPath') { + continue; } - }).outputText; + compilerOptions[key] = options[key]; + } + const compilerHost = ts.createCompilerHost(compilerOptions); + const refactor = new TypeScriptFileRefactor(this.resourcePath, compilerHost); + _replaceResources(refactor); + + const result = refactor.transpile(compilerOptions); + cb(null, result.outputText, result.sourceMap); } } diff --git a/packages/webpack/src/refactor.ts b/packages/webpack/src/refactor.ts index 55a769b8d096..834973221026 100644 --- a/packages/webpack/src/refactor.ts +++ b/packages/webpack/src/refactor.ts @@ -7,7 +7,7 @@ const MagicString = require('magic-string'); export interface TranspileOutput { outputText: string; - sourceMap: any; + sourceMap?: any; } export class TypeScriptFileRefactor { @@ -161,21 +161,27 @@ export class TypeScriptFileRefactor { fileName: this._fileName }); - const consumer = new SourceMapConsumer(JSON.parse(result.sourceMapText)); - const map = SourceMapGenerator.fromSourceMap(consumer); - if (this._changed) { - const sourceMap = this._sourceString.generateMap({ - file: this._fileName.replace(/\.ts$/, '.js'), - source: this._fileName, - hires: true, - includeContent: true, - }); - map.applySourceMap(new SourceMapConsumer(sourceMap)); - } + if (result.sourceMapText) { + const consumer = new SourceMapConsumer(JSON.parse(result.sourceMapText)); + const map = SourceMapGenerator.fromSourceMap(consumer); + if (this._changed) { + const sourceMap = this._sourceString.generateMap({ + file: this._fileName.replace(/\.ts$/, '.js'), + source: this._fileName, + hires: true, + includeContent: true, + }); + map.applySourceMap(new SourceMapConsumer(sourceMap)); + } - return { - outputText: result.outputText, - sourceMap: map.toJSON() - }; + return { + outputText: result.outputText, + sourceMap: map.toJSON() + }; + } else { + return { + outputText: result.outputText + }; + } } } From 5db8abf2272b75345df2172a2f9ac297f2652019 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 23 Nov 2016 12:17:26 -0500 Subject: [PATCH 11/11] fix coverage --- .../angular-cli/models/webpack-build-test.js | 4 +- packages/webpack/src/compiler.ts | 16 -------- packages/webpack/src/loader.ts | 9 +++-- packages/webpack/src/refactor.ts | 38 +++++++++++++------ 4 files changed, 34 insertions(+), 33 deletions(-) delete mode 100644 packages/webpack/src/compiler.ts diff --git a/packages/angular-cli/models/webpack-build-test.js b/packages/angular-cli/models/webpack-build-test.js index 32d2c8400bca..3c4c390341b0 100644 --- a/packages/angular-cli/models/webpack-build-test.js +++ b/packages/angular-cli/models/webpack-build-test.js @@ -84,9 +84,7 @@ const getWebpackTestConfig = function (projectRoot, environment, appConfig, test loader: webpackLoader, query: { tsConfigPath: path.resolve(appRoot, appConfig.tsconfig), - module: 'commonjs', - inlineSourceMap: true, - sourceRoot: appRoot + module: 'commonjs' } } ], diff --git a/packages/webpack/src/compiler.ts b/packages/webpack/src/compiler.ts deleted file mode 100644 index 87dd33414619..000000000000 --- a/packages/webpack/src/compiler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as tscWrapped from '@angular/tsc-wrapped/src/compiler_host'; -import * as ts from 'typescript'; - - -export class NgcWebpackCompilerHost extends tscWrapped.DelegatingHost { - fileCache = new Map(); - - constructor(delegate: ts.CompilerHost) { - super(delegate); - } -} - -export function createCompilerHost(tsConfig: any) { - const delegateHost = ts.createCompilerHost(tsConfig['compilerOptions']); - return new NgcWebpackCompilerHost(delegateHost); -} diff --git a/packages/webpack/src/loader.ts b/packages/webpack/src/loader.ts index a351600f1588..8b51282cd8e0 100644 --- a/packages/webpack/src/loader.ts +++ b/packages/webpack/src/loader.ts @@ -147,12 +147,13 @@ function _checkDiagnostics(refactor: TypeScriptFileRefactor) { export function ngcLoader(source: string) { this.cacheable(); const cb: any = this.async(); + const sourceFileName: string = this.resourcePath; const plugin = this._compilation._ngToolsWebpackPluginInstance as AotPlugin; // We must verify that AotPlugin is an instance of the right class. if (plugin && plugin instanceof AotPlugin) { const refactor = new TypeScriptFileRefactor( - this.resourcePath, plugin.compilerHost, plugin.program); + sourceFileName, plugin.compilerHost, plugin.program); Promise.resolve() .then(() => { @@ -190,7 +191,7 @@ export function ngcLoader(source: string) { throw tsConfig.error; } - const compilerOptions = tsConfig.config.compilerOptions as ts.CompilerOptions; + const compilerOptions: ts.CompilerOptions = tsConfig.config.compilerOptions; for (const key of Object.keys(options)) { if (key == 'tsConfigPath') { continue; @@ -198,10 +199,12 @@ export function ngcLoader(source: string) { compilerOptions[key] = options[key]; } const compilerHost = ts.createCompilerHost(compilerOptions); - const refactor = new TypeScriptFileRefactor(this.resourcePath, compilerHost); + const refactor = new TypeScriptFileRefactor(sourceFileName, compilerHost); _replaceResources(refactor); const result = refactor.transpile(compilerOptions); + // Webpack is going to take care of this. + result.outputText = result.outputText.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''); cb(null, result.outputText, result.sourceMap); } } diff --git a/packages/webpack/src/refactor.ts b/packages/webpack/src/refactor.ts index 834973221026..9a2a174e3b33 100644 --- a/packages/webpack/src/refactor.ts +++ b/packages/webpack/src/refactor.ts @@ -1,4 +1,5 @@ // TODO: move this in its own package. +import * as path from 'path'; import * as ts from 'typescript'; import {SourceMapConsumer, SourceMapGenerator} from 'source-map'; @@ -7,7 +8,7 @@ const MagicString = require('magic-string'); export interface TranspileOutput { outputText: string; - sourceMap?: any; + sourceMap: any | null; } export class TypeScriptFileRefactor { @@ -18,6 +19,7 @@ export class TypeScriptFileRefactor { get fileName() { return this._fileName; } get sourceFile() { return this._sourceFile; } + get sourceText() { return this._sourceString.toString(); } constructor(private _fileName: string, private _host: ts.CompilerHost, @@ -155,32 +157,46 @@ export class TypeScriptFileRefactor { } transpile(compilerOptions: ts.CompilerOptions): TranspileOutput { - const source = this._sourceString.toString(); + // const basePath = path.resolve(path.dirname(tsConfigPath), + // tsConfig.config.compilerOptions.baseUrl || '.'); + compilerOptions = Object.assign({}, compilerOptions, { + sourceMap: true, + inlineSources: false, + inlineSourceMap: false, + sourceRoot: '' + }); + + const source = this.sourceText; const result = ts.transpileModule(source, { compilerOptions, fileName: this._fileName }); if (result.sourceMapText) { - const consumer = new SourceMapConsumer(JSON.parse(result.sourceMapText)); + const sourceMapJson = JSON.parse(result.sourceMapText); + sourceMapJson.sources = [ this._fileName ]; + + const consumer = new SourceMapConsumer(sourceMapJson); const map = SourceMapGenerator.fromSourceMap(consumer); if (this._changed) { const sourceMap = this._sourceString.generateMap({ - file: this._fileName.replace(/\.ts$/, '.js'), + file: path.basename(this._fileName.replace(/\.ts$/, '.js')), source: this._fileName, hires: true, - includeContent: true, }); - map.applySourceMap(new SourceMapConsumer(sourceMap)); + map.applySourceMap(new SourceMapConsumer(sourceMap), this._fileName); } - return { - outputText: result.outputText, - sourceMap: map.toJSON() - }; + const sourceMap = map.toJSON(); + sourceMap.sources = [ this._fileName ]; + sourceMap.file = path.basename(this._fileName, '.ts') + '.js'; + sourceMap.sourcesContent = [ this._sourceText ]; + + return { outputText: result.outputText, sourceMap }; } else { return { - outputText: result.outputText + outputText: result.outputText, + sourceMap: null }; } }