diff --git a/packages/@angular/cli/models/webpack-configs/typescript.ts b/packages/@angular/cli/models/webpack-configs/typescript.ts index 39c7a7295687..3be9fc429a14 100644 --- a/packages/@angular/cli/models/webpack-configs/typescript.ts +++ b/packages/@angular/cli/models/webpack-configs/typescript.ts @@ -70,6 +70,7 @@ function _createAotPlugin(wco: WebpackConfigOptions, options: any) { locale: buildOptions.locale, replaceExport: appConfig.platform === 'server', hostReplacementPaths, + sourceMap: buildOptions.sourcemaps, // If we don't explicitely list excludes, it will default to `['**/*.spec.ts']`. exclude: [] }, options)); diff --git a/packages/@ngtools/webpack/README.md b/packages/@ngtools/webpack/README.md index bc34b62ede74..48bc83272b5d 100644 --- a/packages/@ngtools/webpack/README.md +++ b/packages/@ngtools/webpack/README.md @@ -38,6 +38,7 @@ The loader works with the webpack plugin to compile your TypeScript. It's import * `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. * `exclude`. Optional. Extra files to exclude from TypeScript compilation. +* `sourceMap`. Optional. Include sourcemaps. * `compilerOptions`. Optional. Override options in `tsconfig.json`. ## Features diff --git a/packages/@ngtools/webpack/package.json b/packages/@ngtools/webpack/package.json index 83975af5d1f8..04b5a8cfeab4 100644 --- a/packages/@ngtools/webpack/package.json +++ b/packages/@ngtools/webpack/package.json @@ -27,11 +27,12 @@ "dependencies": { "loader-utils": "^1.0.2", "magic-string": "^0.22.3", - "source-map": "^0.5.6" + "source-map": "^0.5.6", + "semver": "^5.3.0" }, "peerDependencies": { "enhanced-resolve": "^3.1.0", - "typescript": "^2.0.2", + "typescript": "^2.1.0", "webpack": "^2.2.0 || ^3.0.0" } } diff --git a/packages/@ngtools/webpack/src/entry_resolver.ts b/packages/@ngtools/webpack/src/entry_resolver.ts index 247e5bc6c99b..b24cbd747ed5 100644 --- a/packages/@ngtools/webpack/src/entry_resolver.ts +++ b/packages/@ngtools/webpack/src/entry_resolver.ts @@ -2,13 +2,14 @@ import * as fs from 'fs'; import {join} from 'path'; import * as ts from 'typescript'; -import {TypeScriptFileRefactor} from './refactor'; +import {TypeScriptFileRefactor, getTypeScriptFileRefactor} from './refactor/refactor'; +import {ProgramManager} from './program_manager'; function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor, symbolName: string, host: ts.CompilerHost, - program: ts.Program): string | null { + programManager: ProgramManager): string | null { // Check this file. const hasSymbol = refactor.findAstNodes(null, ts.SyntaxKind.ClassDeclaration) .some((cd: ts.ClassDeclaration) => { @@ -29,15 +30,16 @@ function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor, const modulePath = (decl.moduleSpecifier as ts.StringLiteral).text; const resolvedModule = ts.resolveModuleName( - modulePath, refactor.fileName, program.getCompilerOptions(), host); + modulePath, refactor.fileName, programManager.program.getCompilerOptions(), host); if (!resolvedModule.resolvedModule || !resolvedModule.resolvedModule.resolvedFileName) { return null; } const module = resolvedModule.resolvedModule.resolvedFileName; if (!decl.exportClause) { - const moduleRefactor = new TypeScriptFileRefactor(module, host, program); - const maybeModule = _recursiveSymbolExportLookup(moduleRefactor, symbolName, host, program); + const moduleRefactor = getTypeScriptFileRefactor(module, host, programManager); + const maybeModule = _recursiveSymbolExportLookup( + moduleRefactor, symbolName, host, programManager); if (maybeModule) { return maybeModule; } @@ -51,9 +53,9 @@ function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor, if (fs.statSync(module).isDirectory()) { const indexModule = join(module, 'index.ts'); if (fs.existsSync(indexModule)) { - const indexRefactor = new TypeScriptFileRefactor(indexModule, host, program); + const indexRefactor = getTypeScriptFileRefactor(indexModule, host, programManager); const maybeModule = _recursiveSymbolExportLookup( - indexRefactor, symbolName, host, program); + indexRefactor, symbolName, host, programManager); if (maybeModule) { return maybeModule; } @@ -61,7 +63,7 @@ function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor, } // Create the source and verify that the symbol is at least a class. - const source = new TypeScriptFileRefactor(module, host, program); + const source = getTypeScriptFileRefactor(module, host, programManager); const hasSymbol = source.findAstNodes(null, ts.SyntaxKind.ClassDeclaration) .some((cd: ts.ClassDeclaration) => { return cd.name && cd.name.text == symbolName; @@ -80,7 +82,7 @@ function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor, function _symbolImportLookup(refactor: TypeScriptFileRefactor, symbolName: string, host: ts.CompilerHost, - program: ts.Program): string | null { + programManager: ProgramManager): string | null { // We found the bootstrap variable, now we just need to get where it's imported. const imports = refactor.findAstNodes(null, ts.SyntaxKind.ImportDeclaration) .map(node => node as ts.ImportDeclaration); @@ -95,7 +97,7 @@ function _symbolImportLookup(refactor: TypeScriptFileRefactor, const resolvedModule = ts.resolveModuleName( (decl.moduleSpecifier as ts.StringLiteral).text, - refactor.fileName, program.getCompilerOptions(), host); + refactor.fileName, programManager.program.getCompilerOptions(), host); if (!resolvedModule.resolvedModule || !resolvedModule.resolvedModule.resolvedFileName) { continue; } @@ -112,8 +114,9 @@ function _symbolImportLookup(refactor: TypeScriptFileRefactor, for (const specifier of binding.elements) { if (specifier.name.text == symbolName) { // Create the source and recursively lookup the import. - const source = new TypeScriptFileRefactor(module, host, program); - const maybeModule = _recursiveSymbolExportLookup(source, symbolName, host, program); + const source = getTypeScriptFileRefactor(module, host, programManager); + const maybeModule = _recursiveSymbolExportLookup( + source, symbolName, host, programManager); if (maybeModule) { return maybeModule; } @@ -127,8 +130,8 @@ function _symbolImportLookup(refactor: TypeScriptFileRefactor, export function resolveEntryModuleFromMain(mainPath: string, host: ts.CompilerHost, - program: ts.Program) { - const source = new TypeScriptFileRefactor(mainPath, host, program); + programManager: ProgramManager) { + const source = getTypeScriptFileRefactor(mainPath, host, programManager); const bootstrap = source.findAstNodes(source.sourceFile, ts.SyntaxKind.CallExpression, true) .map(node => node as ts.CallExpression) @@ -148,7 +151,7 @@ export function resolveEntryModuleFromMain(mainPath: string, + 'to the plugins options.'); } const bootstrapSymbolName = bootstrap[0].text; - const module = _symbolImportLookup(source, bootstrapSymbolName, host, program); + const module = _symbolImportLookup(source, bootstrapSymbolName, host, programManager); if (module) { return `${module.replace(/\.ts$/, '')}#${bootstrapSymbolName}`; } diff --git a/packages/@ngtools/webpack/src/lazy_routes.ts b/packages/@ngtools/webpack/src/lazy_routes.ts index aa49c8c16821..d9de04a64978 100644 --- a/packages/@ngtools/webpack/src/lazy_routes.ts +++ b/packages/@ngtools/webpack/src/lazy_routes.ts @@ -1,7 +1,8 @@ import {dirname, join} from 'path'; import * as ts from 'typescript'; -import {TypeScriptFileRefactor} from './refactor'; +import {getTypeScriptFileRefactor} from './refactor/refactor'; +import {ProgramManager} from './program_manager'; function _getContentOfKeyLiteral(_source: ts.SourceFile, node: ts.Node): string { @@ -21,9 +22,9 @@ export interface LazyRouteMap { export function findLazyRoutes(filePath: string, - program: ts.Program, + programManager: ProgramManager, host: ts.CompilerHost): LazyRouteMap { - const refactor = new TypeScriptFileRefactor(filePath, host, program); + const refactor = getTypeScriptFileRefactor(filePath, host, programManager); return refactor // Find all object literals in the file. @@ -50,7 +51,8 @@ export function findLazyRoutes(filePath: string, ? ({ resolvedModule: { resolvedFileName: join(dirname(filePath), moduleName) + '.ts' } } as any) - : ts.resolveModuleName(moduleName, filePath, program.getCompilerOptions(), host); + : ts.resolveModuleName( + moduleName, filePath, programManager.program.getCompilerOptions(), host); if (resolvedModuleName.resolvedModule && resolvedModuleName.resolvedModule.resolvedFileName && host.fileExists(resolvedModuleName.resolvedModule.resolvedFileName)) { diff --git a/packages/@ngtools/webpack/src/loader.spec.ts b/packages/@ngtools/webpack/src/loader.spec.ts index 5514dd342f7f..f76da19bd39a 100644 --- a/packages/@ngtools/webpack/src/loader.spec.ts +++ b/packages/@ngtools/webpack/src/loader.spec.ts @@ -1,7 +1,7 @@ -import * as ts from 'typescript'; import {removeModuleIdOnlyForTesting} from './loader'; import {WebpackCompilerHost} from './compiler_host'; -import {TypeScriptFileRefactor} from './refactor'; +import {getTypeScriptFileRefactor} from './refactor/refactor'; +import {ProgramManager} from './program_manager'; describe('@ngtools/webpack', () => { describe('loader', () => { @@ -20,25 +20,28 @@ describe('@ngtools/webpack', () => { @SomeDecorator({ otherValue4: 4, moduleId: 123 }) class CLS4 {} `, false); - const program = ts.createProgram(['/file.ts', '/file2.ts'], {}, host); + const programManager = new ProgramManager(['/file.ts', '/file2.ts'], {}, host); - const refactor = new TypeScriptFileRefactor('/file.ts', host, program); + const refactor = getTypeScriptFileRefactor('/file.ts', host, programManager); removeModuleIdOnlyForTesting(refactor); - expect(refactor.sourceText).not.toMatch(/obj = \{\s+};/); - expect(refactor.sourceText).not.toMatch(/\{\s*otherValue: 1\s*};/); - const refactor2 = new TypeScriptFileRefactor('/file2.ts', host, program); + const outputText = refactor.transpile().outputText; + expect(outputText).not.toMatch(/obj = \{\s+};/); + expect(outputText).not.toMatch(/\{\s*otherValue: 1\s*};/); + + const refactor2 = getTypeScriptFileRefactor('/file2.ts', host, programManager); removeModuleIdOnlyForTesting(refactor2); - expect(refactor2.sourceText).toMatch(/\(\{\s+}\)/); - expect(refactor2.sourceText).toMatch(/\(\{\s*otherValue1: 1\s*}\)/); - expect(refactor2.sourceText).toMatch(/\(\{\s*otherValue2: 2\s*,\s*otherValue3: 3\s*}\)/); - expect(refactor2.sourceText).toMatch(/\(\{\s*otherValue4: 4\s*}\)/); + const outputText2 = refactor2.transpile().outputText; + expect(outputText2).toMatch(/\(\{\s*}\)/); + expect(outputText2).toMatch(/\(\{\s*otherValue1: 1\s*}\)/); + expect(outputText2).toMatch(/\(\{\s*otherValue2: 2\s*,\s*otherValue3: 3\s*}\)/); + expect(outputText2).toMatch(/\(\{\s*otherValue4: 4\s*}\)/); }); it('should work without a root name', () => { const host = new WebpackCompilerHost({}, ''); host.writeFile('/file.ts', ` - import './file2.ts'; + import './file2'; `, false); host.writeFile('/file2.ts', ` @SomeDecorator({ moduleId: 123 }) class CLS {} @@ -47,13 +50,14 @@ describe('@ngtools/webpack', () => { @SomeDecorator({ otherValue4: 4, moduleId: 123 }) class CLS4 {} `, false); - const program = ts.createProgram(['/file.ts'], {}, host); - const refactor = new TypeScriptFileRefactor('/file2.ts', host, program); + const programManager = new ProgramManager(['/file.ts'], {}, host); + const refactor = getTypeScriptFileRefactor('/file2.ts', host, programManager); removeModuleIdOnlyForTesting(refactor); - expect(refactor.sourceText).toMatch(/\(\{\s+}\)/); - expect(refactor.sourceText).toMatch(/\(\{\s*otherValue1: 1\s*}\)/); - expect(refactor.sourceText).toMatch(/\(\{\s*otherValue2: 2\s*,\s*otherValue3: 3\s*}\)/); - expect(refactor.sourceText).toMatch(/\(\{\s*otherValue4: 4\s*}\)/); + const outputText = refactor.transpile().outputText; + expect(outputText).toMatch(/\(\{\s*}\)/); + expect(outputText).toMatch(/\(\{\s*otherValue1: 1\s*}\)/); + expect(outputText).toMatch(/\(\{\s*otherValue2: 2\s*,\s*otherValue3: 3\s*}\)/); + expect(outputText).toMatch(/\(\{\s*otherValue4: 4\s*}\)/); }); }); }); diff --git a/packages/@ngtools/webpack/src/loader.ts b/packages/@ngtools/webpack/src/loader.ts index 22215a1023c1..daf42e432662 100644 --- a/packages/@ngtools/webpack/src/loader.ts +++ b/packages/@ngtools/webpack/src/loader.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import * as ts from 'typescript'; import {AotPlugin} from './plugin'; -import {TypeScriptFileRefactor} from './refactor'; +import {getTypeScriptFileRefactor, TypeScriptFileRefactor} from './refactor/refactor'; import {LoaderContext, ModuleReason} from './webpack'; interface Platform { @@ -150,7 +150,11 @@ function _addCtorParameters(classNode: ts.ClassDeclaration, }); const ctorParametersDecl = `static ctorParameters() { return [ ${params.join(', ')} ]; }`; - refactor.prependBefore(classNode.getLastToken(refactor.sourceFile), ctorParametersDecl); + // ts.createIdentifier() with complex declarations is a hack to not have to manually + // create the node with TS primitives. + // TODO: replace with primitives over time. + const ctorParametersNode = ts.createIdentifier(ctorParametersDecl); + refactor.prependNode(classNode.getLastToken(refactor.sourceFile), ctorParametersNode); } @@ -214,7 +218,7 @@ function _replacePlatform( const platform = changeMap[(call.expression as ts.Identifier).text]; // Replace with mapped replacement - refactor.replaceNode(call.expression, platform.name); + refactor.replaceNode(call.expression, ts.createIdentifier(platform.name)); // Add the appropriate import refactor.insertImport(platform.name, platform.importLocation); @@ -247,7 +251,7 @@ function _replaceBootstrapOrRender(refactor: TypeScriptFileRefactor, call: ts.Ca } if (identifier && identifier.text === replacementTarget) { - refactor.replaceNode(identifier, replacementTarget + 'Factory'); + refactor.replaceNode(identifier, ts.createIdentifier(replacementTarget + 'Factory')); } } @@ -280,7 +284,7 @@ function _replaceEntryModule(plugin: AotPlugin, refactor: TypeScriptFileRefactor modules .forEach(reference => { - refactor.replaceNode(reference, factoryClassName); + refactor.replaceNode(reference, ts.createIdentifier(factoryClassName)); const caller = _getCaller(reference); _replaceBootstrapOrRender(refactor, caller); }); @@ -325,7 +329,7 @@ function _removeModuleId(refactor: TypeScriptFileRefactor) { // Get the trailing comma. const moduleIdCommaProp = moduleIdProp.parent ? moduleIdProp.parent.getChildAt(1).getChildren()[1] : null; - refactor.removeNodes(moduleIdProp, moduleIdCommaProp); + refactor.removeNodes([moduleIdProp, moduleIdCommaProp]); }); } @@ -349,8 +353,9 @@ function _replaceResources(refactor: TypeScriptFileRefactor): void { const key = _getContentOfKeyLiteral(sourceFile, node.name); if (key == 'templateUrl') { - refactor.replaceNode(node, - `template: require(${_getResourceRequest(node.initializer, sourceFile)})`); + refactor.replaceNode(node, ts.createIdentifier( + `template: require(${_getResourceRequest(node.initializer, sourceFile)})` + )); } else if (key == 'styleUrls') { const arr = ( refactor.findAstNodes(node, ts.SyntaxKind.ArrayLiteralExpression, false)); @@ -361,7 +366,9 @@ function _replaceResources(refactor: TypeScriptFileRefactor): void { const initializer = arr[0].elements.map((element: ts.Expression) => { return _getResourceRequest(element, sourceFile); }); - refactor.replaceNode(node, `styles: [require(${initializer.join('), require(')})]`); + refactor.replaceNode(node, ts.createIdentifier( + `styles: [require(${initializer.join('), require(')})]` + )); } }); } @@ -460,7 +467,7 @@ export function _replaceExport(plugin: AotPlugin, refactor: TypeScriptFileRefact const factoryPath = _getNgFactoryPath(plugin, refactor); const factoryClassName = plugin.entryModule.className + 'NgFactory'; const exportStatement = `export \{ ${factoryClassName} \} from '${factoryPath}'`; - refactor.appendAfter(node, exportStatement); + refactor.appendNode(node, ts.createIdentifier(exportStatement)); }); } @@ -497,7 +504,9 @@ export function _exportModuleMap(plugin: AotPlugin, refactor: TypeScriptFileRefa modules.forEach((module, index) => { const relativePath = path.relative(dirName, module.modulePath).replace(/\\/g, '/'); - refactor.prependBefore(node, `import * as __lazy_${index}__ from './${relativePath}'`); + refactor.prependNode(node, ts.createIdentifier( + `import * as __lazy_${index}__ from './${relativePath}'` + )); }); const jsonContent: string = modules @@ -505,7 +514,9 @@ export function _exportModuleMap(plugin: AotPlugin, refactor: TypeScriptFileRefa `"${module.loadChildrenString}": __lazy_${index}__.${module.moduleName}`) .join(); - refactor.appendAfter(node, `export const LAZY_MODULE_MAP = {${jsonContent}};`); + refactor.prependNode(node, ts.createIdentifier( + `export const LAZY_MODULE_MAP = {${jsonContent}};` + )); }); } @@ -531,8 +542,8 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s // extra TS loaders and there's no need to do any trickery. source = null; } - const refactor = new TypeScriptFileRefactor( - sourceFileName, plugin.compilerHost, plugin.program, source); + const refactor = getTypeScriptFileRefactor( + sourceFileName, plugin.compilerHost, plugin.programManager, source); Promise.resolve() .then(() => { @@ -586,14 +597,7 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s } } - // 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); + const result = refactor.transpile(); cb(null, result.outputText, result.sourceMap); }) .catch(err => cb(err)); @@ -621,7 +625,7 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s compilerOptions[key] = options[key]; } const compilerHost = ts.createCompilerHost(compilerOptions); - const refactor = new TypeScriptFileRefactor(sourceFileName, compilerHost); + const refactor = getTypeScriptFileRefactor(sourceFileName, compilerHost); _replaceResources(refactor); const result = refactor.transpile(compilerOptions); diff --git a/packages/@ngtools/webpack/src/plugin.ts b/packages/@ngtools/webpack/src/plugin.ts index 0535aa3a9aff..c381859f8392 100644 --- a/packages/@ngtools/webpack/src/plugin.ts +++ b/packages/@ngtools/webpack/src/plugin.ts @@ -13,6 +13,7 @@ import {resolveEntryModuleFromMain} from './entry_resolver'; import {Tapable} from './webpack'; import {PathsPlugin} from './paths-plugin'; import {findLazyRoutes, LazyRouteMap} from './lazy_routes'; +import {ProgramManager} from './program_manager'; /** @@ -31,9 +32,11 @@ export interface AotPluginOptions { i18nFile?: string; i18nFormat?: string; locale?: string; + sourceMap?: boolean; // Use tsconfig to include path globs. exclude?: string | string[]; + include?: string[]; compilerOptions?: ts.CompilerOptions; } @@ -46,8 +49,7 @@ export class AotPlugin implements Tapable { private _compilerOptions: ts.CompilerOptions; private _angularCompilerOptions: any; - private _program: ts.Program; - private _rootFilePath: string[]; + private _programManager: ProgramManager; private _compilerHost: WebpackCompilerHost; private _resourceLoader: WebpackResourceLoader; private _discoveredLazyRoutes: LazyRouteMap; @@ -90,7 +92,8 @@ export class AotPlugin implements Tapable { return {path, className}; } get genDir() { return this._genDir; } - get program() { return this._program; } + get program() { return this._programManager.program; } + get programManager() { return this._programManager; } get skipCodeGeneration() { return this._skipCodeGeneration; } get replaceExport() { return this._replaceExport; } get typeCheck() { return this._typeCheck; } @@ -161,7 +164,6 @@ export class AotPlugin implements Tapable { tsConfigJson, ts.sys, basePath, null, this._tsConfigPath); let fileNames = tsConfig.fileNames; - this._rootFilePath = fileNames; // Check the genDir. We generate a default gendir that's under basepath; it will generate // a `node_modules` directory and because of that we don't want TypeScript resolution to @@ -208,8 +210,17 @@ export class AotPlugin implements Tapable { } } - this._program = ts.createProgram( - this._rootFilePath, this._compilerOptions, this._compilerHost); + // Force the right sourcemap options. + if (options.sourceMap) { + this._compilerOptions.sourceMap = true; + this._compilerOptions.inlineSources = true; + this._compilerOptions.inlineSourceMap = false; + this._compilerOptions.sourceRoot = basePath; + } else { + this._compilerOptions.sourceMap = false; + } + + this._programManager = new ProgramManager(fileNames, this._compilerOptions, this._compilerHost); // We enable caching of the filesystem in compilerHost _after_ the program has been created, // because we don't want SourceFile instances to be cached past this point. @@ -226,7 +237,8 @@ export class AotPlugin implements Tapable { // still no _entryModule? => try to resolve from mainPath if (!this._entryModule && options.mainPath) { const mainPath = path.resolve(basePath, options.mainPath); - this._entryModule = resolveEntryModuleFromMain(mainPath, this._compilerHost, this._program); + this._entryModule = resolveEntryModuleFromMain( + mainPath, this._compilerHost, this.programManager); } if (options.hasOwnProperty('i18nFile')) { @@ -247,7 +259,7 @@ export class AotPlugin implements Tapable { const result: LazyRouteMap = Object.create(null); const changedFilePaths = this._compilerHost.getChangedFilePaths(); for (const filePath of changedFilePaths) { - const fileLazyRoutes = findLazyRoutes(filePath, this._program, this._compilerHost); + const fileLazyRoutes = findLazyRoutes(filePath, this.programManager, this._compilerHost); for (const routeKey of Object.keys(fileLazyRoutes)) { const route = fileLazyRoutes[routeKey]; if (routeKey in this._lazyRoutes) { @@ -271,7 +283,7 @@ export class AotPlugin implements Tapable { private _getLazyRoutesFromNgtools() { try { return __NGTOOLS_PRIVATE_API_2.listLazyRoutes({ - program: this._program, + program: this.program, host: this._compilerHost, angularCompilerOptions: this._angularCompilerOptions, entryModule: this._entryModule @@ -407,17 +419,17 @@ export class AotPlugin implements Tapable { } this._diagnoseFiles[fileName] = true; - const sourceFile = this._program.getSourceFile(fileName); + const sourceFile = this.program.getSourceFile(fileName); if (!sourceFile) { return; } const diagnostics: ts.Diagnostic[] = [] .concat( - this._program.getCompilerOptions().declaration - ? this._program.getDeclarationDiagnostics(sourceFile) : [], - this._program.getSyntacticDiagnostics(sourceFile), - this._program.getSemanticDiagnostics(sourceFile) + this.program.getCompilerOptions().declaration + ? this.program.getDeclarationDiagnostics(sourceFile) : [], + this.program.getSyntacticDiagnostics(sourceFile), + this.program.getSemanticDiagnostics(sourceFile) ); if (diagnostics.length > 0) { @@ -463,7 +475,7 @@ export class AotPlugin implements Tapable { return __NGTOOLS_PRIVATE_API_2.codeGen({ basePath: this._basePath, compilerOptions: this._compilerOptions, - program: this._program, + program: this.program, host: this._compilerHost, angularCompilerOptions: this._angularCompilerOptions, i18nFile: this.i18nFile, @@ -476,19 +488,11 @@ export class AotPlugin implements Tapable { .then(() => { // Get the ngfactory that were created by the previous step, and add them to the root // file path (if those files exists). - const newRootFilePath = this._compilerHost.getChangedFilePaths() + const newFiles = this._compilerHost.getChangedFilePaths() .filter(x => x.match(/\.ngfactory\.ts$/)); - // Remove files that don't exist anymore, and add new files. - this._rootFilePath = this._rootFilePath - .filter(x => this._compilerHost.fileExists(x)) - .concat(newRootFilePath); - - // 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); + this._programManager.update(newFiles); }) .then(() => { // Re-diagnose changed files. @@ -497,7 +501,7 @@ export class AotPlugin implements Tapable { }) .then(() => { if (this._typeCheck) { - const diagnostics = this._program.getGlobalDiagnostics(); + const diagnostics = this.program.getGlobalDiagnostics(); if (diagnostics.length > 0) { const message = diagnostics .map(diagnostic => { diff --git a/packages/@ngtools/webpack/src/program_manager.ts b/packages/@ngtools/webpack/src/program_manager.ts new file mode 100644 index 000000000000..09a1dc42131a --- /dev/null +++ b/packages/@ngtools/webpack/src/program_manager.ts @@ -0,0 +1,33 @@ +import * as ts from 'typescript'; + +export class ProgramManager { + private _program: ts.Program; + + get program() { return this._program; } + + /** + * Create and manage a ts.program instance. + */ + constructor( + private _files: string[], + private _compilerOptions: ts.CompilerOptions, + private _compilerHost: ts.CompilerHost + ) { + this._program = ts.createProgram(_files, _compilerOptions, _compilerHost); + } + + /** + * 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. + */ + update(newFiles: string[]) { + // Remove files that don't exist anymore, and add new files. + this._files = this._files.concat(newFiles) + .filter(x => this._compilerHost.fileExists(x)); + + this._program = ts.createProgram(this._files, this._compilerOptions, this._compilerHost, + this._program); + } +} diff --git a/packages/@ngtools/webpack/src/refactor/magic_string_refactor.spec.ts b/packages/@ngtools/webpack/src/refactor/magic_string_refactor.spec.ts new file mode 100644 index 000000000000..98392fb5ad70 --- /dev/null +++ b/packages/@ngtools/webpack/src/refactor/magic_string_refactor.spec.ts @@ -0,0 +1,89 @@ +import * as ts from 'typescript'; + +import { WebpackCompilerHost } from '../compiler_host'; +import { TypeScriptMagicStringFileRefactor } from './magic_string_refactor'; +import { ProgramManager } from '../program_manager'; + + +describe('@ngtools/webpack', () => { + describe('TypeScriptMagicStringFileRefactor', () => { + let host, programManager, refactor: TypeScriptMagicStringFileRefactor, firstDecorator: ts.Node; + + beforeEach(() => { + host = new WebpackCompilerHost({}, ''); + host.writeFile('/file.ts', ` + @SomeDecorator() class CLS {} + const value = 42; + `, false); + + programManager = new ProgramManager(['/file.ts'], {}, host); + refactor = new TypeScriptMagicStringFileRefactor('/file.ts', host, programManager); + + firstDecorator = refactor.findFirstAstNode(refactor.sourceFile, ts.SyntaxKind.Decorator); + }); + + it('removeNode should work', () => { + refactor.removeNode(firstDecorator); + expect(refactor.sourceText).toEqual(` + class CLS {} + const value = 42; + `); + }); + + it('replaceNode should work', () => { + refactor.replaceNode(firstDecorator, ts.createIdentifier('@AnotherDecorator()')); + + expect(refactor.sourceText).toEqual(` + @AnotherDecorator() class CLS {} + const value = 42; + `); + }); + + it('appendNode should work', () => { + refactor.appendNode(firstDecorator, ts.createIdentifier('class CLS2 {};')); + + expect(refactor.sourceText).toEqual(` + @SomeDecorator()class CLS2 {}; class CLS {} + const value = 42; + `); + }); + + it('appendNode should work', () => { + refactor.appendNode(firstDecorator, ts.createIdentifier('class CLS2 {};')); + + expect(refactor.sourceText).toEqual(` + @SomeDecorator()class CLS2 {}; class CLS {} + const value = 42; + `); + }); + + describe('insertImport', () => { + beforeEach(() => { + const host = new WebpackCompilerHost({}, ''); + host.writeFile('/file.ts', ` + import { something } from 'some-module'; + const value = something; + `, false); + + programManager = new ProgramManager(['/file.ts'], {}, host); + refactor = new TypeScriptMagicStringFileRefactor('/file.ts', host, programManager); + }); + + it('should append new import after existing imports statements', () => { + refactor.insertImport('symbolName', 'modulePath'); + + expect(refactor.sourceText).toContain( + `import { something } from 'some-module';import {symbolName} from 'modulePath';` + ); + }); + + it('should append new import after existing named imports', () => { + refactor.insertImport('somethingElse', 'some-module'); + + expect(refactor.sourceText).toContain( + `import { something, somethingElse } from 'some-module';` + ); + }); + }); + }); +}); diff --git a/packages/@ngtools/webpack/src/refactor.ts b/packages/@ngtools/webpack/src/refactor/magic_string_refactor.ts similarity index 70% rename from packages/@ngtools/webpack/src/refactor.ts rename to packages/@ngtools/webpack/src/refactor/magic_string_refactor.ts index c96c90d3e00b..b16a389d80f4 100644 --- a/packages/@ngtools/webpack/src/refactor.ts +++ b/packages/@ngtools/webpack/src/refactor/magic_string_refactor.ts @@ -5,31 +5,16 @@ import {SourceMapConsumer, SourceMapGenerator} from 'source-map'; const MagicString = require('magic-string'); - -export interface TranspileOutput { - outputText: string; - sourceMap: any | null; -} +import {TypeScriptFileRefactor, TranspileOutput, resolve} from './refactor'; +import {ProgramManager} from '../program_manager'; -function resolve(filePath: string, _host: ts.CompilerHost, program: ts.Program) { - if (path.isAbsolute(filePath)) { - return filePath; - } - const compilerOptions = program.getCompilerOptions(); - const basePath = compilerOptions.baseUrl || compilerOptions.rootDir; - if (!basePath) { - throw new Error(`Trying to resolve '${filePath}' without a basePath.`); - } - return path.join(basePath, filePath); -} - - -export class TypeScriptFileRefactor { +export class TypeScriptMagicStringFileRefactor implements TypeScriptFileRefactor { private _fileName: string; private _sourceFile: ts.SourceFile; private _sourceString: any; private _sourceText: string; + private _compilerOptions: ts.CompilerOptions = {}; private _changed = false; get fileName() { return this._fileName; } @@ -38,15 +23,18 @@ export class TypeScriptFileRefactor { constructor(fileName: string, _host: ts.CompilerHost, - private _program?: ts.Program, + private _programManager?: ProgramManager, source?: string | null) { - fileName = resolve(fileName, _host, _program).replace(/\\/g, '/'); + + const program = _programManager.program; + fileName = resolve(fileName, _host, program).replace(/\\/g, '/'); this._fileName = fileName; - if (_program) { + if (program) { + this._compilerOptions = program.getCompilerOptions(); if (source) { this._sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true); } else { - this._sourceFile = _program.getSourceFile(fileName); + this._sourceFile = program.getSourceFile(fileName); } } if (!this._sourceFile) { @@ -57,25 +45,47 @@ export class TypeScriptFileRefactor { this._sourceString = new MagicString(this._sourceText); } + // Nodes don't always have text, and .getText() fails on nodes created manually. + // We have to duck type it. + _getNodeText(node: ts.Node) { + const text = (node as any).text; + if (text === undefined) { + throw new Error('Refactor failed to retrieve node text.'); + } + + return text; + } + /** * Collates the diagnostic messages for the current source file */ getDiagnostics(typeCheck = true): ts.Diagnostic[] { - if (!this._program) { + const program = this._programManager.program; + if (!program) { return []; } let diagnostics: ts.Diagnostic[] = []; // only concat the declaration diagnostics if the tsconfig config sets it to true. - if (this._program.getCompilerOptions().declaration == true) { - diagnostics = diagnostics.concat(this._program.getDeclarationDiagnostics(this._sourceFile)); + if (program.getCompilerOptions().declaration == true) { + diagnostics = diagnostics.concat(program.getDeclarationDiagnostics(this._sourceFile)); } - diagnostics = diagnostics.concat( - this._program.getSyntacticDiagnostics(this._sourceFile), - typeCheck ? this._program.getSemanticDiagnostics(this._sourceFile) : []); + diagnostics = diagnostics.concat(program.getSyntacticDiagnostics(this._sourceFile), + typeCheck ? program.getSemanticDiagnostics(this._sourceFile) : []); return diagnostics; } + getFirstNode(): ts.Node | null { + const syntaxList = this.findFirstAstNode(null, ts.SyntaxKind.SyntaxList); + return (syntaxList && syntaxList.getChildCount() > 0) ? syntaxList.getChildAt(0) : null; + } + + getLastNode(): ts.Node | null { + const syntaxList = this.findFirstAstNode(null, ts.SyntaxKind.SyntaxList); + const childCount = syntaxList.getChildCount(); + return childCount > 0 ? syntaxList.getChildAt(childCount - 1) : null; + } + /** * Find all nodes from the AST in the subtree of node of SyntaxKind kind. * @param node The root node to check, or null if the whole tree should be searched. @@ -128,15 +138,12 @@ export class TypeScriptFileRefactor { return this.findAstNodes(node, kind, false, 1)[0] || null; } - appendAfter(node: ts.Node, text: string): void { - this._sourceString.appendRight(node.getEnd(), text); - } - append(node: ts.Node, text: string): void { - this._sourceString.appendLeft(node.getEnd(), text); + appendNode(node: ts.Node, newNode: ts.Node): void { + this._sourceString.appendRight(node.getEnd(), this._getNodeText(newNode)); } - prependBefore(node: ts.Node, text: string) { - this._sourceString.appendLeft(node.getStart(), text); + prependNode(node: ts.Node, newNode: ts.Node) { + this._sourceString.appendLeft(node.getStart(), this._getNodeText(newNode)); } insertImport(symbolName: string, modulePath: string): void { @@ -173,12 +180,12 @@ export class TypeScriptFileRefactor { 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}`); + this.appendNode(maybeImports[0].elements[maybeImports[0].elements.length - 1], + ts.createIdentifier(`, ${symbolName}`)); } else { // Find the last import and insert after. - this.appendAfter(allImports[allImports.length - 1], - `import {${symbolName}} from '${modulePath}';`); + this.appendNode(allImports[allImports.length - 1], + ts.createIdentifier(`import {${symbolName}} from '${modulePath}';`)); } } @@ -187,15 +194,16 @@ export class TypeScriptFileRefactor { this._changed = true; } - removeNodes(...nodes: ts.Node[]) { + removeNodes(nodes: ts.Node[]) { nodes.forEach(node => node && this.removeNode(node)); } - replaceNode(node: ts.Node, replacement: string) { + replaceNode(node: ts.Node, replacementNode: ts.Node) { + const replacementString = this._getNodeText(replacementNode); let replaceSymbolName: boolean = node.kind === ts.SyntaxKind.Identifier; this._sourceString.overwrite(node.getStart(this._sourceFile), node.getEnd(), - replacement, + replacementString, { storeName: replaceSymbolName }); this._changed = true; } @@ -207,12 +215,15 @@ export class TypeScriptFileRefactor { transpile(compilerOptions: ts.CompilerOptions): TranspileOutput { const source = this.sourceText; const result = ts.transpileModule(source, { - compilerOptions: Object.assign({}, compilerOptions, { - sourceMap: true, - inlineSources: false, - inlineSourceMap: false, - sourceRoot: '' - }), + compilerOptions: Object.assign({}, + this._compilerOptions, + compilerOptions, + { + sourceMap: true, + inlineSources: false, + inlineSourceMap: false, + sourceRoot: '' + }), fileName: this._fileName }); diff --git a/packages/@ngtools/webpack/src/refactor/refactor.ts b/packages/@ngtools/webpack/src/refactor/refactor.ts new file mode 100644 index 000000000000..55775941773e --- /dev/null +++ b/packages/@ngtools/webpack/src/refactor/refactor.ts @@ -0,0 +1,69 @@ +import * as path from 'path'; +import * as ts from 'typescript'; +import * as semver from 'semver'; + +import { TypeScriptMagicStringFileRefactor } from './magic_string_refactor'; +import { TypeScriptFileTransformRefactor } from './transform_refactor'; +import { ProgramManager } from '../program_manager'; + +export interface TypeScriptFileRefactor { + + // Properties. + fileName: string; + sourceFile: ts.SourceFile; + + // Primitives. + removeNode(node: ts.Node): void; + removeNodes(nodes: ts.Node[]): void; + replaceNode(node: ts.Node, replacement: ts.Node): void; + appendNode(node: ts.Node, newNode: ts.Node): void; + prependNode(node: ts.Node, newNode: ts.Node): void; + + // Lookups. + getFirstNode(): ts.Node | null; + getLastNode(): ts.Node | null; + findFirstAstNode(node: ts.Node | null, kind: ts.SyntaxKind): ts.Node | null; + findAstNodes(node: ts.Node | null, kind: ts.SyntaxKind, recursive?: boolean, + max?: number): ts.Node[]; + + // Macros. + insertImport(symbolName: string, modulePath: string): void; + + // Transpilation. + getDiagnostics(typeCheck: boolean): ts.Diagnostic[]; + transpile(compilerOptions?: ts.CompilerOptions): TranspileOutput; +} + +export interface TranspileOutput { + outputText: string; + sourceMap: any | null; +} + +export function resolve(filePath: string, _host: ts.CompilerHost, program: ts.Program) { + if (path.isAbsolute(filePath)) { + return filePath; + } + const compilerOptions = program.getCompilerOptions(); + const basePath = compilerOptions.baseUrl || compilerOptions.rootDir; + if (!basePath) { + throw new Error(`Trying to resolve '${filePath}' without a basePath.`); + } + return path.join(basePath, filePath); +} + +export function getTypeScriptFileRefactor( + fileName: string, + host: ts.CompilerHost, + programManager?: ProgramManager, + source?: string | null +): TypeScriptFileRefactor { + + const projectTypeScriptVersion = require('typescript/package.json').version; + const transformMinTypeScriptVersion = '^2.3.0'; + + if (programManager && semver.satisfies(projectTypeScriptVersion, transformMinTypeScriptVersion)) { + return new TypeScriptFileTransformRefactor(fileName, host, programManager, source); + } else { + return new TypeScriptMagicStringFileRefactor(fileName, host, programManager, source); + } +} diff --git a/packages/@ngtools/webpack/src/refactor/transform_refactor.spec.ts b/packages/@ngtools/webpack/src/refactor/transform_refactor.spec.ts new file mode 100644 index 000000000000..bb9d2bf3298f --- /dev/null +++ b/packages/@ngtools/webpack/src/refactor/transform_refactor.spec.ts @@ -0,0 +1,318 @@ +import * as ts from 'typescript'; +import * as path from 'path'; + +import { WebpackCompilerHost } from '../compiler_host'; +import { TypeScriptFileTransformRefactor } from './transform_refactor'; +import { ProgramManager } from '../program_manager'; + + +describe('@ngtools/webpack', () => { + describe('TypeScriptFileTransformRefactor', () => { + const compilerOptions = { + target: ts.ScriptTarget.ES5, + module: ts.ModuleKind.ES2015, + emitDecoratorMetadata: true, + experimentalDecorators: true, + sourceMap: true, + inlineSources: true, + inlineSourceMap: false, + sourceRoot: '', + }; + + const fileContent = ` + const value = 42; + const obj = {}; + const bool = true; + `; + + let refactor: TypeScriptFileTransformRefactor, + firstVarStmt: ts.Node; + + beforeEach(() => { + const host = new WebpackCompilerHost({}, ''); + host.writeFile('/file.ts', fileContent, false); + + const programManager = new ProgramManager(['/file.ts'], compilerOptions, host); + refactor = new TypeScriptFileTransformRefactor('/file.ts', host, programManager); + + firstVarStmt = refactor.findFirstAstNode(null, ts.SyntaxKind.VariableStatement); + }); + + describe('lookups', () => { + it('findAstNodes should work', () => { + const varStmts = refactor.findAstNodes(null, ts.SyntaxKind.VariableStatement); + expect(varStmts[0].getText()).toContain('const value = 42;'); + expect(varStmts[1].getText()).toContain('const obj = {};'); + expect(varStmts[2].getText()).toContain('const bool = true;'); + }); + + it('findFirstAstNode should work', () => { + const firstVarStmt = refactor.findFirstAstNode(null, ts.SyntaxKind.VariableStatement); + expect(firstVarStmt.getText()).toContain('const value = 42;'); + }); + + it('getFirstNode should work', () => { + expect(refactor.getFirstNode().getText()).toContain('const value = 42;'); + }); + + it('getLastNode should work', () => { + expect(refactor.getLastNode().getText()).toContain('const bool = true;'); + }); + }); + + describe('transforms', () => { + it('not doing any should work', () => { + expect(refactor.transpile().outputText).toContain('var value = 42;'); + expect(refactor.transpile().outputText).toContain('var obj = {};'); + expect(refactor.transpile().outputText).toContain('var bool = true;'); + }); + + it('removeNode should work', () => { + refactor.removeNode(firstVarStmt); + expect(refactor.transpile().outputText).not.toContain('const value = 42;'); + }); + + it('removeNodes should work', () => { + const varStmts = refactor.findAstNodes(null, ts.SyntaxKind.VariableStatement); + refactor.removeNodes(varStmts); + expect(refactor.transpile().outputText).not.toContain('const value = 42;'); + expect(refactor.transpile().outputText).not.toContain('const bool = true;'); + }); + + it('replaceNode should work', () => { + // var idx = 1; + const varStmt = ts.createVariableStatement(undefined, [ + ts.createVariableDeclaration('idx', undefined, ts.createNumericLiteral('1')) + ]); + + refactor.replaceNode(firstVarStmt, varStmt); + + expect(refactor.transpile().outputText).not.toContain('const value = 42;'); + expect(refactor.transpile().outputText).toContain('var idx = 1;'); + }); + + it('appendNode should work', () => { + // var idx = 1; + const varStmt = ts.createVariableStatement(undefined, [ + ts.createVariableDeclaration('idx', undefined, ts.createNumericLiteral('1')) + ]); + + refactor.appendNode(firstVarStmt, varStmt); + + expect(refactor.transpile().outputText).toMatch(/var value = 42;\s*var idx = 1;/); + }); + + it('prependNode should work', () => { + // var idx = 1; + const varStmt = ts.createVariableStatement(undefined, [ + ts.createVariableDeclaration('idx', undefined, ts.createNumericLiteral('1')) + ]); + + refactor.prependNode(firstVarStmt, varStmt); + + expect(refactor.transpile().outputText).toMatch(/var idx = 1;\s*var value = 42;/); + }); + + it('transforms should work together', () => { + // var idx = 1; + const varStmt = ts.createVariableStatement(undefined, [ + ts.createVariableDeclaration('idx', undefined, ts.createNumericLiteral('1')) + ]); + // var str = "str"; + const varStmt2 = ts.createVariableStatement(undefined, [ + ts.createVariableDeclaration('str', undefined, ts.createLiteral('str')) + ]); + // var key = "key"; + const varStmt3 = ts.createVariableStatement(undefined, [ + ts.createVariableDeclaration('key', undefined, ts.createLiteral('key')) + ]); + + const varStmts = refactor.findAstNodes(null, ts.SyntaxKind.VariableStatement); + + refactor.prependNode(varStmts[0], varStmt); + refactor.appendNode(varStmts[0], varStmt2); + refactor.removeNode(varStmts[1]); + refactor.replaceNode(varStmts[2], varStmt3); + + expect(refactor.transpile().outputText).toMatch( + /var idx = 1;\s*var value = 42;\s*var str = "str";\s*var key = "key";/ + ); + }); + }); + + describe('insertImport', () => { + + let refactor: TypeScriptFileTransformRefactor; + + beforeEach(() => { + const host = new WebpackCompilerHost({}, ''); + host.writeFile('/file.ts', ` + import { something } from 'some-module'; + const value = something; + `, false); + + const programManager = new ProgramManager(['/file.ts'], compilerOptions, host); + refactor = new TypeScriptFileTransformRefactor('/file.ts', host, programManager); + }); + + it('should append new import after existing imports statements', () => { + refactor.insertImport('symbolName', 'modulePath'); + + expect(refactor.transpile().outputText).toMatch( + /import { something } from 'some-module';\s*import { symbolName } from "modulePath";/ + ); + }); + + it('should work when there are no imports', () => { + const host = new WebpackCompilerHost({}, ''); + host.writeFile('/file.ts', `const value = 42;`, false); + const programManager = new ProgramManager(['/file.ts'], compilerOptions, host); + refactor = new TypeScriptFileTransformRefactor('/file.ts', host, programManager); + + refactor.insertImport('symbolName', 'modulePath'); + + expect(refactor.transpile().outputText).toContain( + `import { symbolName } from "modulePath";` + ); + }); + + it('should append new import after existing named imports', () => { + refactor.insertImport('somethingElse', 'some-module'); + + expect(refactor.transpile().outputText).toContain( + `import { something, somethingElse } from 'some-module';` + ); + }); + }); + + describe('outDir', () => { + it('should work with outDir', () => { + // Make a new refactor and add a transform. + const host = new WebpackCompilerHost({}, ''); + host.writeFile('/file.ts', fileContent, false); + const newCompilerOptions = Object.assign({ outDir: '/path/' }, compilerOptions); + const programManager = new ProgramManager(['/file.ts'], newCompilerOptions, host); + refactor = new TypeScriptFileTransformRefactor('/file.ts', host, programManager); + const varStmt = ts.createVariableStatement(undefined, [ + ts.createVariableDeclaration('idx', undefined, ts.createNumericLiteral('1')) + ]); + const varStmts = refactor.findAstNodes(null, ts.SyntaxKind.VariableStatement); + refactor.prependNode(varStmts[0], varStmt); + + expect(refactor.transpile().outputText).toContain('var idx = 1;'); + }); + + it('should work without outDir', () => { + // Make a new refactor and add a transform. + const host = new WebpackCompilerHost({}, ''); + host.writeFile('/file.ts', fileContent, false); + const programManager = new ProgramManager(['/file.ts'], compilerOptions, host); + refactor = new TypeScriptFileTransformRefactor('/file.ts', host, programManager); + const varStmt = ts.createVariableStatement(undefined, [ + ts.createVariableDeclaration('idx', undefined, ts.createNumericLiteral('1')) + ]); + const varStmts = refactor.findAstNodes(null, ts.SyntaxKind.VariableStatement); + refactor.prependNode(varStmts[0], varStmt); + + expect(refactor.transpile().outputText).toContain('var idx = 1;'); + }); + }); + + describe('update program', () => { + it('should update the program by file path', () => { + const host = new WebpackCompilerHost({}, ''); + host.writeFile('/file.ts', ` + export const value = 42; + `, false); + host.writeFile('/file2.ts', ` + import { value } from './file'; + console.log(value); + `, false); + + const programManager = new ProgramManager(['/file.ts'], compilerOptions, host); + refactor = new TypeScriptFileTransformRefactor('/file2.ts', host, programManager); + refactor.appendNode(refactor.getLastNode(), ts.createIdentifier('console.log(43);')); + + const output = refactor.transpile().outputText; + expect(output).toContain('console.log(43)'); + }); + + it('should update the program by file source', () => { + const host = new WebpackCompilerHost({}, ''); + host.writeFile('/file.ts', ` + export const value = 42; + `, false); + + const programManager = new ProgramManager(['/file.ts'], compilerOptions, host); + refactor = new TypeScriptFileTransformRefactor('/file2.ts', host, programManager, ` + import { value } from './file'; + console.log(value); + `); + refactor.appendNode(refactor.getLastNode(), ts.createIdentifier('console.log(43);')); + + const output = refactor.transpile().outputText; + expect(output).toContain('console.log(43)'); + }); + }); + + describe('sourcemaps', () => { + beforeEach(() => { + // Do a bunch of transforms. + + // var idx = 1; + const varStmt = ts.createVariableStatement(undefined, [ + ts.createVariableDeclaration('idx', undefined, ts.createNumericLiteral('1')) + ]); + // var str = "str"; + const varStmt2 = ts.createVariableStatement(undefined, [ + ts.createVariableDeclaration('str', undefined, ts.createLiteral('str')) + ]); + // var key = "key"; + const varStmt3 = ts.createVariableStatement(undefined, [ + ts.createVariableDeclaration('key', undefined, ts.createLiteral('key')) + ]); + + const varStmts = refactor.findAstNodes(null, ts.SyntaxKind.VariableStatement); + + refactor.prependNode(varStmts[0], varStmt); + refactor.appendNode(varStmts[0], varStmt2); + refactor.removeNode(varStmts[1]); + refactor.replaceNode(varStmts[2], varStmt3); + }); + + it('should output sourcemaps', () => { + const transpileOutput = refactor.transpile(); + expect(transpileOutput.sourceMap).toBeTruthy(); + }); + + it('should output sourcemaps with file, sources and sourcesContent', () => { + const transpileOutput = refactor.transpile(); + const { file, sources, sourcesContent } = transpileOutput.sourceMap; + expect(file).toBe('file.js'); + expect(sources[0]).toBe(`${path.sep}file.ts`); + expect(sourcesContent[0]).toBe(fileContent); + }); + + it('should not output sourcemaps if they are off', () => { + const compilerOptions = { + target: ts.ScriptTarget.ESNext, + sourceMap: false, + sourceRoot: '' + }; + + const host = new WebpackCompilerHost({}, ''); + host.writeFile('/file.ts', ` + const value = 42; + const obj = {}; + const bool = true; + `, false); + + const programManager = new ProgramManager(['/file.ts'], compilerOptions, host); + refactor = new TypeScriptFileTransformRefactor('/file.ts', host, programManager); + const transpileOutput = refactor.transpile(); + + expect(transpileOutput.sourceMap).toBeNull(); + }); + }); + }); +}); diff --git a/packages/@ngtools/webpack/src/refactor/transform_refactor.ts b/packages/@ngtools/webpack/src/refactor/transform_refactor.ts new file mode 100644 index 000000000000..83358df4835b --- /dev/null +++ b/packages/@ngtools/webpack/src/refactor/transform_refactor.ts @@ -0,0 +1,311 @@ +import * as path from 'path'; +import * as ts from 'typescript'; + +import { TypeScriptFileRefactor, TranspileOutput, resolve } from './refactor'; +import { ProgramManager } from '../program_manager'; + + +export class TypeScriptFileTransformRefactor implements TypeScriptFileRefactor { + private _fileName: string; + private _sourceFile: ts.SourceFile; + private _dropNodes: ts.Node[] = []; + private _replaceNodes: { node: ts.Node, replacementNode: ts.Node }[] = []; + private _addNodes: { node: ts.Node, before?: ts.Node, after?: ts.Node }[] = []; + + get fileName() { return this._fileName; } + get sourceFile() { return this._sourceFile; } + + constructor( + fileName: string, + private _host: ts.CompilerHost, + private _programManager: ProgramManager, + source?: string | null + ) { + if (!fileName.endsWith('.ts')) { + throw new Error('Unable to refactor non-TS files.'); + } + + const program = _programManager.program; + fileName = this._normalize(resolve(fileName, _host, program)); + this._fileName = fileName; + + if (!source) { + this._sourceFile = program.getSourceFile(fileName); + } + + if (!this._sourceFile) { + this._host.writeFile(fileName, source || _host.readFile(fileName), false); + this._programManager.update([fileName]); + this._sourceFile = _host.getSourceFile(fileName, ts.ScriptTarget.Latest); + } + + // Something bad happened and we couldn't find the TS source. + if (!this._sourceFile) { + throw new Error(`Could not retrieve TS Source for ${this._fileName}.`); + } + } + + _normalize(path: string) { + return path.replace(/\\/g, '/'); + } + + removeNode(node: ts.Node) { + this._dropNodes.push(node); + } + + removeNodes(nodes: ts.Node[]) { + nodes.forEach(node => node && this.removeNode(node)); + } + + replaceNode(node: ts.Node, replacementNode: ts.Node) { + this._replaceNodes.push({ node, replacementNode }); + } + + appendNode(node: ts.Node, newNode: ts.Node): void { + this._addNodes.push({ node, after: newNode }); + } + + prependNode(node: ts.Node, newNode: ts.Node) { + this._addNodes.push({ node, before: newNode }); + } + + /** + * Collates the diagnostic messages for the current source file + */ + getDiagnostics(typeCheck = true): ts.Diagnostic[] { + const program = this._programManager.program; + if (!program) { + return []; + } + let diagnostics: ts.Diagnostic[] = []; + // only concat the declaration diagnostics if the tsconfig config sets it to true. + if (program.getCompilerOptions().declaration == true) { + diagnostics = diagnostics.concat(program.getDeclarationDiagnostics(this._sourceFile)); + } + diagnostics = diagnostics.concat(program.getSyntacticDiagnostics(this._sourceFile), + typeCheck ? program.getSemanticDiagnostics(this._sourceFile) : []); + + return diagnostics; + } + + getFirstNode(): ts.Node | null { + const syntaxList = this.findFirstAstNode(null, ts.SyntaxKind.SyntaxList); + return (syntaxList && syntaxList.getChildCount() > 0) ? syntaxList.getChildAt(0) : null; + } + + getLastNode(): ts.Node | null { + const syntaxList = this.findFirstAstNode(null, ts.SyntaxKind.SyntaxList); + const childCount = syntaxList.getChildCount(); + return childCount > 0 ? syntaxList.getChildAt(childCount - 1) : null; + } + + findFirstAstNode(node: ts.Node | null, kind: ts.SyntaxKind): ts.Node | null { + return this.findAstNodes(node, kind, false, 1)[0] || null; + } + + /** + * Find all nodes from the AST in the subtree of node of SyntaxKind 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 | null, + kind: ts.SyntaxKind, + recursive = false, + max: number = Infinity): ts.Node[] { + if (max == 0) { + return []; + } + if (!node) { + node = this._sourceFile; + } + + 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; + } + + insertImport(symbolName: string, modulePath: string): void { + // Find all imports. + 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. + 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.appendNode( + maybeImports[0].elements[maybeImports[0].elements.length - 1], + ts.createImportSpecifier(undefined, ts.createIdentifier(symbolName)) + ); + } else { + // Create the new import node. + const namedImports = ts.createNamedImports([ts.createImportSpecifier(undefined, + ts.createIdentifier(symbolName))]); + // typescript@2.4 fixes the function parameter types of ts.createImportClause. + // https://github.com/Microsoft/TypeScript/pull/15999 + const importClause = (ts.createImportClause as any)(undefined, namedImports); + const newImport = ts.createImportDeclaration(undefined, undefined, importClause, + ts.createLiteral(modulePath)); + + if (allImports.length > 0) { + // Find the last import and insert after. + this.appendNode(allImports[allImports.length - 1], newImport); + } else { + // Insert before the first node. + this.prependNode(this.getFirstNode(), newImport); + } + } + } + + private _getTransform(): ts.TransformerFactory { + const dropNodes = this._dropNodes; + const replaceNodes = this._replaceNodes; + const addNodes = this._addNodes; + + return (context: ts.TransformationContext): ts.Transformer => { + const transformer: ts.Transformer = (sf: ts.SourceFile) => { + + const visitor: ts.Visitor = (node) => { + // Check if node should be dropped. + if (dropNodes.find((n) => n === node)) { + return undefined; + } + + // Check if node should be replaced. + const replace = replaceNodes.find((rpl) => rpl.node === node); + if (replace) { + return replace.replacementNode; + } + + // Check if node should be added to. + const add = addNodes.filter((a) => a.node === node); + if (add.length > 0) { + return [ + ...add.filter((a) => a.before).map(((a) => a.before)), + node, + ...add.filter((a) => a.after).map(((a) => a.after)) + ]; + } + + // Otherwise return node as is. + return ts.visitEachChild(node, visitor, context); + }; + + return ts.visitNode(sf, visitor); + }; + + return transformer; + }; + } + + transpile(): TranspileOutput { + const program = this._programManager.program; + + // Capture text and sourcemap via a custom writeFile function. + let outputText: string; + let sourceMapText: string; + + const writeFile = (fileName: string, data: string) => { + if (path.extname(fileName) === '.js') { + if (outputText !== undefined) { + // This really shouldn't happen, so error out if it does. + throw new Error(`Double JS emit for ${this._fileName}.`); + } + outputText = data; + } else if (path.extname(fileName) === '.map') { + sourceMapText = data; + } + }; + + /** + * There are currently two severe bugs affecting transforms: + * - Before transforms break with decorated class constructor parameters + * (https://github.com/Microsoft/TypeScript/issues/17551) + * - Imports made unused are not dropped (https://github.com/Microsoft/TypeScript/issues/17552) + * The first breaks compilation, the second retains extra imports like the compiler. + */ + const { emitSkipped } = program.emit( + this._sourceFile, writeFile, undefined, undefined, + { before: [this._getTransform()] } + ); + + if (emitSkipped) { + throw new Error(`${this._fileName} emit failed.`); + } + + if (outputText === undefined) { + // Something went wrong in reading the emitted file; + throw new Error(`Could not retrieve emitted TypeScript for ${this._fileName}.`); + } + + if (sourceMapText !== undefined) { + const sourceMap = JSON.parse(sourceMapText); + const fileName = this._fileName.replace(/\//g, path.sep); + sourceMap.file = path.basename(fileName, '.ts') + '.js'; + sourceMap.sources = [fileName]; + sourceMap.sourcesContent = [this._host.readFile(this._fileName)]; + + return { + outputText, + sourceMap: sourceMap + }; + } else { + return { + outputText, + sourceMap: null + }; + } + } +} diff --git a/tests/e2e/assets/webpack/test-app-old-ts/app/app.component.html b/tests/e2e/assets/webpack/test-app-old-ts/app/app.component.html new file mode 100644 index 000000000000..5a532db9308f --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-old-ts/app/app.component.html @@ -0,0 +1,5 @@ +
+

hello world

+ lazy + +
diff --git a/tests/e2e/assets/webpack/test-app-old-ts/app/app.component.scss b/tests/e2e/assets/webpack/test-app-old-ts/app/app.component.scss new file mode 100644 index 000000000000..5cde7b922336 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-old-ts/app/app.component.scss @@ -0,0 +1,3 @@ +:host { + background-color: blue; +} diff --git a/tests/e2e/assets/webpack/test-app-old-ts/app/app.component.ts b/tests/e2e/assets/webpack/test-app-old-ts/app/app.component.ts new file mode 100644 index 000000000000..82a4059565d3 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-old-ts/app/app.component.ts @@ -0,0 +1,15 @@ +import {Component, ViewEncapsulation} from '@angular/core'; +import {MyInjectable} from './injectable'; + + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class AppComponent { + constructor(public inj: MyInjectable) { + console.log(inj); + } +} diff --git a/tests/e2e/assets/webpack/test-app-old-ts/app/app.module.ts b/tests/e2e/assets/webpack/test-app-old-ts/app/app.module.ts new file mode 100644 index 000000000000..ded686868a22 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-old-ts/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-old-ts/app/feature/feature.module.ts b/tests/e2e/assets/webpack/test-app-old-ts/app/feature/feature.module.ts new file mode 100644 index 000000000000..f464ca028b05 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-old-ts/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-old-ts/app/feature/lazy-feature.module.ts b/tests/e2e/assets/webpack/test-app-old-ts/app/feature/lazy-feature.module.ts new file mode 100644 index 000000000000..8fafca158b24 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-old-ts/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-old-ts/app/injectable.ts b/tests/e2e/assets/webpack/test-app-old-ts/app/injectable.ts new file mode 100644 index 000000000000..04d8486586c4 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-old-ts/app/injectable.ts @@ -0,0 +1,8 @@ +import {Injectable, Inject, ViewContainerRef} from '@angular/core'; +import {DOCUMENT} from '@angular/platform-browser'; + + +@Injectable() +export class MyInjectable { + constructor(public viewContainer: ViewContainerRef, @Inject(DOCUMENT) public doc) {} +} diff --git a/tests/e2e/assets/webpack/test-app-old-ts/app/lazy.module.ts b/tests/e2e/assets/webpack/test-app-old-ts/app/lazy.module.ts new file mode 100644 index 000000000000..96da4de7515b --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-old-ts/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-old-ts/app/main.aot.ts b/tests/e2e/assets/webpack/test-app-old-ts/app/main.aot.ts new file mode 100644 index 000000000000..1b4503a81e31 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-old-ts/app/main.aot.ts @@ -0,0 +1,5 @@ +import 'core-js/es7/reflect'; +import {platformBrowser} from '@angular/platform-browser'; +import {AppModuleNgFactory} from './ngfactory/app/app.module.ngfactory'; + +platformBrowser().bootstrapModuleFactory(AppModuleNgFactory); diff --git a/tests/e2e/assets/webpack/test-app-old-ts/app/main.jit.ts b/tests/e2e/assets/webpack/test-app-old-ts/app/main.jit.ts new file mode 100644 index 000000000000..0a705a5daebb --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-old-ts/app/main.jit.ts @@ -0,0 +1,5 @@ +import 'core-js/es7/reflect'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {AppModule} from './app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/tests/e2e/assets/webpack/test-app-old-ts/index.html b/tests/e2e/assets/webpack/test-app-old-ts/index.html new file mode 100644 index 000000000000..89fb0893c35d --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-old-ts/index.html @@ -0,0 +1,12 @@ + + + + Document + + + + + + + + diff --git a/tests/e2e/assets/webpack/test-app-old-ts/package.json b/tests/e2e/assets/webpack/test-app-old-ts/package.json new file mode 100644 index 000000000000..ef0b67aa2e49 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-old-ts/package.json @@ -0,0 +1,27 @@ +{ + "name": "test", + "license": "MIT", + "dependencies": { + "@angular/common": "^2.3.1", + "@angular/compiler": "^2.3.1", + "@angular/compiler-cli": "^2.3.1", + "@angular/core": "^2.3.1", + "@angular/http": "^2.3.1", + "@angular/platform-browser": "^2.3.1", + "@angular/platform-browser-dynamic": "^2.3.1", + "@angular/platform-server": "^2.3.1", + "@angular/router": "^3.3.1", + "@ngtools/webpack": "0.0.0", + "core-js": "^2.4.1", + "rxjs": "^5.4.2", + "zone.js": "^0.8.14" + }, + "devDependencies": { + "node-sass": "^3.7.0", + "performance-now": "^0.2.0", + "raw-loader": "^0.5.1", + "sass-loader": "^3.2.0", + "typescript": "~2.1.0", + "webpack": "2.2.0" + } +} diff --git a/tests/e2e/assets/webpack/test-app-old-ts/tsconfig.json b/tests/e2e/assets/webpack/test-app-old-ts/tsconfig.json new file mode 100644 index 000000000000..c245563164ba --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-old-ts/tsconfig.json @@ -0,0 +1,25 @@ +{ + // Test comment + "compilerOptions": { + "baseUrl": "", + "module": "es2015", + "moduleResolution": "node", + "target": "es5", + "noImplicitAny": false, + "sourceMap": true, + "mapRoot": "", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es2017", + "dom" + ], + "outDir": "lib", + "skipLibCheck": true, + "rootDir": "." + }, + "angularCompilerOptions": { + "genDir": "./app/ngfactory", + "entryModule": "app/app.module#AppModule" + } +} diff --git a/tests/e2e/assets/webpack/test-app-old-ts/webpack.config.js b/tests/e2e/assets/webpack/test-app-old-ts/webpack.config.js new file mode 100644 index 000000000000..9f4cd36678c6 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-old-ts/webpack.config.js @@ -0,0 +1,29 @@ +const ngToolsWebpack = require('@ngtools/webpack'); + +module.exports = { + resolve: { + extensions: ['.ts', '.js'] + }, + entry: './app/main.aot.ts', + output: { + path: './dist', + publicPath: 'dist/', + filename: 'app.main.js' + }, + plugins: [ + new ngToolsWebpack.AotPlugin({ + tsConfigPath: './tsconfig.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/test-old-ts.ts b/tests/e2e/tests/packages/webpack/test-old-ts.ts new file mode 100644 index 000000000000..2849bab32453 --- /dev/null +++ b/tests/e2e/tests/packages/webpack/test-old-ts.ts @@ -0,0 +1,28 @@ +import {normalize} from 'path'; +import {createProjectFromAsset} from '../../../utils/assets'; +import {exec} from '../../../utils/process'; +import {expectFileSizeToBeUnder, replaceInFile, expectFileToMatch} from '../../../utils/fs'; + + +export default function(skipCleaning: () => void) { + return Promise.resolve() + .then(() => createProjectFromAsset('webpack/test-app-old-ts')) + .then(() => exec(normalize('node_modules/.bin/webpack'), '-p')) + .then(() => expectFileSizeToBeUnder('dist/app.main.js', 420000)) + .then(() => expectFileSizeToBeUnder('dist/0.app.main.js', 10000)) + // test resource urls without ./ + .then(() => replaceInFile('app/app.component.ts', + './app.component.html', 'app.component.html')) + .then(() => replaceInFile('app/app.component.ts', + './app.component.scss', 'app.component.scss')) + // test the inclusion of metadata + // This build also test resource URLs without ./ + .then(() => exec(normalize('node_modules/.bin/webpack'))) + .then(() => expectFileToMatch('dist/app.main.js', + new RegExp('MyInjectable.ctorParameters = .*' + + 'type: .*ViewContainerRef.*' + + 'type: undefined, decorators.*Inject.*args: .*DOCUMENT.*')) + .then(() => expectFileToMatch('dist/app.main.js', + new RegExp('AppComponent.ctorParameters = .*MyInjectable')) + .then(() => skipCleaning()); +}