From 308b50e37cc44e94634db0e51f0b0381dcab87b1 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Sun, 6 Aug 2017 11:43:32 +0100 Subject: [PATCH] refactor(@ngtools/webpack): use emit when available --- .../cli/models/webpack-configs/typescript.ts | 1 + packages/@ngtools/webpack/README.md | 1 + .../@ngtools/webpack/src/compiler_host.ts | 8 +- .../@ngtools/webpack/src/entry_resolver.ts | 31 +- packages/@ngtools/webpack/src/lazy_routes.ts | 8 +- packages/@ngtools/webpack/src/loader.spec.ts | 38 +-- packages/@ngtools/webpack/src/loader.ts | 31 +- packages/@ngtools/webpack/src/plugin.ts | 65 +++-- .../@ngtools/webpack/src/program_manager.ts | 37 +++ .../@ngtools/webpack/src/refactor.spec.ts | 273 ++++++++++++++++++ packages/@ngtools/webpack/src/refactor.ts | 247 ++++++++++------ tests/e2e/tests/build/rebuild.ts | 3 +- 12 files changed, 581 insertions(+), 162 deletions(-) create mode 100644 packages/@ngtools/webpack/src/program_manager.ts create mode 100644 packages/@ngtools/webpack/src/refactor.spec.ts diff --git a/packages/@angular/cli/models/webpack-configs/typescript.ts b/packages/@angular/cli/models/webpack-configs/typescript.ts index 348dc6369805..3423d946bf94 100644 --- a/packages/@angular/cli/models/webpack-configs/typescript.ts +++ b/packages/@angular/cli/models/webpack-configs/typescript.ts @@ -71,6 +71,7 @@ function _createAotPlugin(wco: WebpackConfigOptions, options: any) { replaceExport: appConfig.platform === 'server', missingTranslation: buildOptions.missingTranslation, 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/src/compiler_host.ts b/packages/@ngtools/webpack/src/compiler_host.ts index e8f49d645bce..f6ca672b899d 100644 --- a/packages/@ngtools/webpack/src/compiler_host.ts +++ b/packages/@ngtools/webpack/src/compiler_host.ts @@ -150,8 +150,12 @@ export class WebpackCompilerHost implements ts.CompilerHost { this._changedFiles = Object.create(null); this._changedDirs = Object.create(null); } - getChangedFilePaths(): string[] { - return Object.keys(this._changedFiles); + + getChangedFilePaths(tsOnly = true): string[] { + // Only get changed ts files by default. + // That's what we mostly care about and want to transpile, but all kinds of files are on this + // list, like package.json and .ngsummary.json files. + return Object.keys(this._changedFiles).filter(k => !tsOnly || k.endsWith('.ts')); } invalidate(fileName: string): void { diff --git a/packages/@ngtools/webpack/src/entry_resolver.ts b/packages/@ngtools/webpack/src/entry_resolver.ts index e406dc60e900..692e90748fa6 100644 --- a/packages/@ngtools/webpack/src/entry_resolver.ts +++ b/packages/@ngtools/webpack/src/entry_resolver.ts @@ -3,12 +3,13 @@ import {join} from 'path'; import * as ts from 'typescript'; import {TypeScriptFileRefactor} from './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 = new TypeScriptFileRefactor(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 = new TypeScriptFileRefactor(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 = new TypeScriptFileRefactor(module, host, programManager); const hasSymbol = source.findAstNodes(null, ts.SyntaxKind.ClassDeclaration) .some((cd: ts.ClassDeclaration) => { return cd.name != undefined && 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; } @@ -114,8 +116,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 = new TypeScriptFileRefactor(module, host, programManager); + const maybeModule = _recursiveSymbolExportLookup( + source, symbolName, host, programManager); if (maybeModule) { return maybeModule; } @@ -129,8 +132,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 = new TypeScriptFileRefactor(mainPath, host, programManager); const bootstrap = source.findAstNodes(source.sourceFile, ts.SyntaxKind.CallExpression, true) .map(node => node as ts.CallExpression) @@ -150,7 +153,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 9f42aaf2bec9..33cf5c65cd0d 100644 --- a/packages/@ngtools/webpack/src/lazy_routes.ts +++ b/packages/@ngtools/webpack/src/lazy_routes.ts @@ -2,6 +2,7 @@ import {dirname, join} from 'path'; import * as ts from 'typescript'; import {TypeScriptFileRefactor} from './refactor'; +import {ProgramManager} from './program_manager'; function _getContentOfKeyLiteral(_source: ts.SourceFile, node: ts.Node): string | null { @@ -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 = new TypeScriptFileRefactor(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..ec8bf1a3926d 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 {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 = new TypeScriptFileRefactor('/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 = new TypeScriptFileRefactor('/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 = new TypeScriptFileRefactor('/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 4be0c7e04905..a1afe615eeea 100644 --- a/packages/@ngtools/webpack/src/loader.ts +++ b/packages/@ngtools/webpack/src/loader.ts @@ -151,7 +151,7 @@ function _addCtorParameters(classNode: ts.ClassDeclaration, }); const ctorParametersDecl = `static ctorParameters() { return [ ${params.join(', ')} ]; }`; - refactor.prependBefore(classNode.getLastToken(refactor.sourceFile), ctorParametersDecl); + refactor.prependNode(classNode.getLastToken(refactor.sourceFile), ctorParametersDecl); } @@ -347,6 +347,7 @@ function _getResourceRequest(element: ts.Expression, sourceFile: ts.SourceFile) function _replaceResources(refactor: TypeScriptFileRefactor): void { const sourceFile = refactor.sourceFile; + let refactored = false; _getResourceNodes(refactor) // Get the full text of the initializer. @@ -356,9 +357,11 @@ function _replaceResources(refactor: TypeScriptFileRefactor): void { if (key == 'templateUrl') { refactor.replaceNode(node, `template: require(${_getResourceRequest(node.initializer, sourceFile)})`); + refactored = true; } else if (key == 'styleUrls') { const arr = ( refactor.findAstNodes(node, ts.SyntaxKind.ArrayLiteralExpression, false)); + refactored = true; if (!arr || arr.length == 0 || arr[0].elements.length == 0) { return; } @@ -367,8 +370,17 @@ function _replaceResources(refactor: TypeScriptFileRefactor): void { return _getResourceRequest(element, sourceFile); }); refactor.replaceNode(node, `styles: [require(${initializer.join('), require(')})]`); + refactored = true; } }); + + if (refactored) { + // If we added a require call, we need to also add typings for it. + // The typings need to be compatible with node typings, but also work by themselves. + refactor.prependNode(refactor.getFirstNode(), + 'declare var require: NodeRequire;interface NodeRequire {(id: string): any;}' + ); + } } @@ -465,7 +477,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, exportStatement); }); } @@ -502,7 +514,7 @@ 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, `import * as __lazy_${index}__ from './${relativePath}'`); }); const jsonContent: string = modules @@ -510,7 +522,7 @@ export function _exportModuleMap(plugin: AotPlugin, refactor: TypeScriptFileRefa `"${module.loadChildrenString}": __lazy_${index}__.${module.moduleName}`) .join(); - refactor.appendAfter(node, `export const LAZY_MODULE_MAP = {${jsonContent}};`); + refactor.appendNode(node, `export const LAZY_MODULE_MAP = {${jsonContent}};`); }); } @@ -537,7 +549,7 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s source = null; } const refactor = new TypeScriptFileRefactor( - sourceFileName, plugin.compilerHost, plugin.program, source); + sourceFileName, plugin.compilerHost, plugin.programManager, source); Promise.resolve() .then(() => { @@ -594,14 +606,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)); diff --git a/packages/@ngtools/webpack/src/plugin.ts b/packages/@ngtools/webpack/src/plugin.ts index 81ae899bd757..1dbb5eb61038 100644 --- a/packages/@ngtools/webpack/src/plugin.ts +++ b/packages/@ngtools/webpack/src/plugin.ts @@ -15,6 +15,7 @@ import {Tapable} from './webpack'; import {PathsPlugin} from './paths-plugin'; import {findLazyRoutes, LazyRouteMap} from './lazy_routes'; import {VirtualFileSystemDecorator} from './virtual_file_system_decorator'; +import {ProgramManager} from './program_manager'; /** @@ -34,9 +35,11 @@ export interface AotPluginOptions { i18nFormat?: string; locale?: string; missingTranslation?: string; + sourceMap?: boolean; // Use tsconfig to include path globs. exclude?: string | string[]; + include?: string[]; compilerOptions?: ts.CompilerOptions; } @@ -49,8 +52,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; @@ -94,7 +96,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; } @@ -171,7 +174,6 @@ export class AotPlugin implements Tapable { tsConfigJson, ts.sys, basePath, undefined, 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 @@ -218,8 +220,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. @@ -236,7 +247,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')) { @@ -266,7 +278,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) { @@ -290,7 +302,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 @@ -434,16 +446,16 @@ 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: Array = [ - ...(this._program.getCompilerOptions().declaration - ? this._program.getDeclarationDiagnostics(sourceFile) : []), - ...this._program.getSyntacticDiagnostics(sourceFile), - ...this._program.getSemanticDiagnostics(sourceFile) + const diagnostics: ts.Diagnostic[] = [ + ...(this.program.getCompilerOptions().declaration + ? this.program.getDeclarationDiagnostics(sourceFile) : []), + ...this.program.getSyntacticDiagnostics(sourceFile), + ...this.program.getSemanticDiagnostics(sourceFile) ]; if (diagnostics.length > 0) { @@ -495,7 +507,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, @@ -507,21 +519,14 @@ 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() - .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. + // Get the changed files plus ngfactory that were created by the previous step, and + // add them to the root file path (if those files exists). + const newFiles = this._compilerHost.getChangedFilePaths(); // 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); + if (newFiles.length > 0) { + this._programManager.update(newFiles); + } }) .then(() => { // Re-diagnose changed files. @@ -530,7 +535,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..5a7e3ae035df --- /dev/null +++ b/packages/@ngtools/webpack/src/program_manager.ts @@ -0,0 +1,37 @@ +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); + } + + hasFile(fileName: string) { + return this._files.includes(fileName); + } +} diff --git a/packages/@ngtools/webpack/src/refactor.spec.ts b/packages/@ngtools/webpack/src/refactor.spec.ts new file mode 100644 index 000000000000..f559b06462cb --- /dev/null +++ b/packages/@ngtools/webpack/src/refactor.spec.ts @@ -0,0 +1,273 @@ +import * as path from 'path'; +import * as ts from 'typescript'; + +import { WebpackCompilerHost } from './compiler_host'; +import { TypeScriptFileRefactor } from './refactor'; +import { ProgramManager } from './program_manager'; + +// TODO add programless test +fdescribe('@ngtools/webpack', () => { + describe('TypeScriptFileRefactor', () => { + 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: TypeScriptFileRefactor, firstVarStmt: ts.Node, host: WebpackCompilerHost; + + beforeEach(() => { + const host = new WebpackCompilerHost({}, ''); + host.writeFile('/file.ts', fileContent, false); + refactor = new TypeScriptFileRefactor('/file.ts', host); + 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.sourceText).toContain('const value = 42;'); + expect(refactor.sourceText).toContain('const obj = {};'); + expect(refactor.sourceText).toContain('const bool = true;'); + }); + + it('removeNode should work', () => { + refactor.removeNode(firstVarStmt); + expect(refactor.sourceText).not.toContain('const value = 42;'); + }); + + it('removeNodes should work', () => { + const varStmts = refactor.findAstNodes(null, ts.SyntaxKind.VariableStatement); + refactor.removeNodes(...varStmts); + expect(refactor.sourceText).not.toContain('const value = 42;'); + expect(refactor.sourceText).not.toContain('const obj = {};'); + expect(refactor.sourceText).not.toContain('const bool = true;'); + }); + + it('replaceNode should work', () => { + refactor.replaceNode(firstVarStmt, 'const idx = 1;'); + expect(refactor.sourceText).not.toContain('const value = 42;'); + expect(refactor.sourceText).toContain('const idx = 1;'); + }); + + it('appendNode should work', () => { + refactor.appendNode(firstVarStmt, 'const idx = 1;'); + expect(refactor.sourceText).toMatch(/const value = 42;\s*const idx = 1;/); + }); + + it('prependNode should work', () => { + refactor.prependNode(firstVarStmt, 'const idx = 1;'); + expect(refactor.sourceText).toMatch(/const idx = 1;\s*const value = 42;/); + }); + + it('transforms should work together', () => { + const varStmts = refactor.findAstNodes(null, ts.SyntaxKind.VariableStatement); + + refactor.prependNode(varStmts[0], 'const idx = 1;\n'); + refactor.appendNode(varStmts[0], '\nconst str = "str";'); + refactor.removeNode(varStmts[1]); + refactor.replaceNode(varStmts[2], 'const key = "key";'); + + expect(refactor.sourceText).toMatch( + /const idx = 1;\s*const value = 42;\s*const str = "str";\s*const key = "key";/ + ); + }); + }); + + describe('insertImport', () => { + beforeEach(() => { + const host = new WebpackCompilerHost({}, ''); + host.writeFile('/file.ts', ` + import { something } from 'some-module'; + const value = something; + `, false); + + refactor = new TypeScriptFileRefactor('/file.ts', host); + }); + + 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';` + ); + }); + + it('should work when there are no imports', () => { + const host = new WebpackCompilerHost({}, ''); + host.writeFile('/file.ts', `const value = 42;`, false); + refactor = new TypeScriptFileRefactor('/file.ts', host); + + refactor.insertImport('symbolName', 'modulePath'); + + expect(refactor.sourceText).toContain( + `import {symbolName} from 'modulePath';` + ); + }); + }); + + describe('transpile', () => { + beforeEach(() => { + host = new WebpackCompilerHost({}, ''); + host.writeFile('/file.ts', fileContent, false); + }); + + it('should work without program', () => { + refactor = new TypeScriptFileRefactor('/file.ts', host); + const varStmts = refactor.findAstNodes(null, ts.SyntaxKind.VariableStatement); + + refactor.prependNode(varStmts[0], 'const idx = 1;\n'); + refactor.appendNode(varStmts[0], '\nconst str = "str";'); + refactor.removeNode(varStmts[1]); + refactor.replaceNode(varStmts[2], 'const key = "key";'); + + const transpileOutput = refactor.transpile(); + + expect(transpileOutput.outputText).toMatch( + /var idx = 1;\s*var value = 42;\s*var str = "str";\s*var key = "key";/ + ); + }); + + it('should work without program', () => { + // add comments to constructor about the possible modes + // need to update program with right file contents, as well as update whenever we do + // new sourcefile stuff + const programManager = new ProgramManager(['/file.ts'], compilerOptions, host); + refactor = new TypeScriptFileRefactor('/file.ts', host, programManager); + const varStmts = refactor.findAstNodes(null, ts.SyntaxKind.VariableStatement); + + refactor.prependNode(varStmts[0], 'const idx = 1;\n'); + refactor.appendNode(varStmts[0], '\nconst str = "str";'); + refactor.removeNode(varStmts[1]); + refactor.replaceNode(varStmts[2], 'const key = "key";'); + + const transpileOutput = refactor.transpile(); + + expect(transpileOutput.outputText).toMatch( + /var idx = 1;\s*var value = 42;\s*var str = "str";\s*var key = "key";/ + ); + }); + }); + + describe('update program', () => { + beforeEach(() => { + host = new WebpackCompilerHost({}, ''); + host.writeFile('/file.ts', ` + export const value = 42; + `, false); + }); + + it('should update the program by file path', () => { + host.writeFile('/file2.ts', ` + import { value } from './file'; + console.log(value); + `, false); + + const programManager = new ProgramManager(['/file.ts'], compilerOptions, host); + refactor = new TypeScriptFileRefactor('/file2.ts', host, programManager); + refactor.appendNode(refactor.getLastNode(), 'console.log(43);'); + + const output = refactor.transpile().outputText; + expect(output).toContain('console.log(43)'); + }); + + it('should update the program by file source', () => { + const programManager = new ProgramManager(['/file.ts'], compilerOptions, host); + refactor = new TypeScriptFileRefactor('/file2.ts', host, programManager, ` + import { value } from './file'; + console.log(value); + `); + refactor.appendNode(refactor.getLastNode(), 'console.log(43);'); + + const output = refactor.transpile().outputText; + expect(output).toContain('console.log(43)'); + }); + }); + + fdescribe('sourcemaps', () => { + beforeEach(() => { + // Do a bunch of transforms. + const varStmts = refactor.findAstNodes(null, ts.SyntaxKind.VariableStatement); + + refactor.prependNode(varStmts[0], 'const idx = 1;'); + refactor.appendNode(varStmts[0], 'const str = "str";'); + refactor.removeNode(varStmts[1]); + refactor.replaceNode(varStmts[2], 'const key = "key";'); + }); + + 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 TypeScriptFileRefactor('/file.ts', host, programManager); + const transpileOutput = refactor.transpile(); + + expect(transpileOutput.sourceMap).toBeNull(); + }); + }); + }); +}); diff --git a/packages/@ngtools/webpack/src/refactor.ts b/packages/@ngtools/webpack/src/refactor.ts index 57fc1449fb21..508cbeb21f09 100644 --- a/packages/@ngtools/webpack/src/refactor.ts +++ b/packages/@ngtools/webpack/src/refactor.ts @@ -1,10 +1,11 @@ // TODO: move this in its own package. import * as path from 'path'; import * as ts from 'typescript'; -import {SourceMapConsumer, SourceMapGenerator} from 'source-map'; +import { SourceMapConsumer, SourceMapGenerator } from 'source-map'; const MagicString = require('magic-string'); +import { ProgramManager } from './program_manager'; export interface TranspileOutput { outputText: string; @@ -12,11 +13,11 @@ export interface TranspileOutput { } -function resolve(filePath: string, _host: ts.CompilerHost, program: ts.Program) { - if (path.isAbsolute(filePath)) { +function resolve(filePath: string, _host: ts.CompilerHost, programManager: ProgramManager) { + if (path.isAbsolute(filePath) || !programManager) { return filePath; } - const compilerOptions = program.getCompilerOptions(); + const compilerOptions = programManager.program.getCompilerOptions(); const basePath = compilerOptions.baseUrl || compilerOptions.rootDir; if (!basePath) { throw new Error(`Trying to resolve '${filePath}' without a basePath.`); @@ -30,52 +31,53 @@ export class TypeScriptFileRefactor { private _sourceFile: ts.SourceFile; private _sourceString: any; private _sourceText: string; + private _compilerOptions: ts.CompilerOptions = {}; private _changed = false; get fileName() { return this._fileName; } get sourceFile() { return this._sourceFile; } get sourceText() { return this._sourceString.toString(); } - constructor(fileName: string, - _host: ts.CompilerHost, - private _program?: ts.Program, - source?: string | null) { - fileName = resolve(fileName, _host, _program!).replace(/\\/g, '/'); + 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 file: ${fileName}.`); + } + + fileName = resolve(fileName, _host, _programManager!).replace(/\\/g, '/'); this._fileName = fileName; - if (_program) { - if (source) { - this._sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true); - } else { - this._sourceFile = _program.getSourceFile(fileName); + + // When there is a program, manage files inside it. + if (_programManager && _programManager.program) { + this._compilerOptions = _programManager.program.getCompilerOptions(); + if (source || !_programManager.hasFile(fileName)) { + this._host.writeFile(fileName, source || _host.readFile(fileName), false); + this._programManager.update([fileName]); } - } - if (!this._sourceFile) { + this._sourceFile = _programManager.program.getSourceFile(fileName); + } else { + // Otherwise just create a sourcefile to use for lookups. + // ts.transpileModule will then use the source string directly. this._sourceFile = ts.createSourceFile(fileName, source || _host.readFile(fileName), ts.ScriptTarget.Latest, true); } - this._sourceText = this._sourceFile.getFullText(this._sourceFile); - this._sourceString = new MagicString(this._sourceText); - } - /** - * Collates the diagnostic messages for the current source file - */ - getDiagnostics(typeCheck = true): ts.Diagnostic[] { - if (!this._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)); + // 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}.`); } - diagnostics = diagnostics.concat( - this._program.getSyntacticDiagnostics(this._sourceFile), - typeCheck ? this._program.getSemanticDiagnostics(this._sourceFile) : []); - return diagnostics; + this._sourceText = this._sourceFile.getFullText(this._sourceFile); + this._sourceString = new MagicString(this._sourceText); } + // Lookups. + /** * 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. @@ -84,10 +86,12 @@ export class TypeScriptFileRefactor { * @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 = Infinity): ts.Node[] { + findAstNodes( + node: ts.Node | null, + kind: ts.SyntaxKind, + recursive = false, + max = Infinity + ): ts.Node[] { if (max == 0) { return []; } @@ -128,15 +132,45 @@ 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); + getFirstNode(): ts.Node | null { + const syntaxList = this.findFirstAstNode(null, ts.SyntaxKind.SyntaxList); + return (syntaxList && syntaxList.getChildCount() > 0) ? syntaxList.getChildAt(0) : null; } - append(node: ts.Node, text: string): void { - this._sourceString.appendLeft(node.getEnd(), text); + + getLastNode(): ts.Node | null { + const syntaxList = this.findFirstAstNode(null, ts.SyntaxKind.SyntaxList); + const childCount = syntaxList.getChildCount(); + return childCount > 0 ? syntaxList.getChildAt(childCount - 1) : null; } - prependBefore(node: ts.Node, text: string) { + // Transforms. + + appendNode(node: ts.Node, text: string): void { + this._sourceString.appendRight(node.getEnd(), text); + this._changed = true; + } + + prependNode(node: ts.Node, text: string) { this._sourceString.appendLeft(node.getStart(), text); + this._changed = true; + } + + removeNode(node: ts.Node) { + this._sourceString.remove(node.getStart(this._sourceFile), node.getEnd()); + this._changed = true; + } + + removeNodes(...nodes: ts.Node[]) { + nodes.forEach(node => node && this.removeNode(node)); + } + + replaceNode(node: ts.Node, replacement: string) { + let replaceSymbolName: boolean = node.kind === ts.SyntaxKind.Identifier; + this._sourceString.overwrite(node.getStart(this._sourceFile), + node.getEnd(), + replacement, + { storeName: replaceSymbolName }); + this._changed = true; } insertImport(symbolName: string, modulePath: string): void { @@ -146,7 +180,7 @@ export class TypeScriptFileRefactor { .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; + && (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'`. @@ -173,55 +207,106 @@ 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], + this.appendNode(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}';`); + const newImport = `import {${symbolName}} from '${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); + } } } - removeNode(node: ts.Node) { - this._sourceString.remove(node.getStart(this._sourceFile), node.getEnd()); - this._changed = true; - } + /** + * 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) : []); - removeNodes(...nodes: Array) { - nodes.forEach(node => node && this.removeNode(node)); + return diagnostics; } - replaceNode(node: ts.Node, replacement: string) { - let replaceSymbolName: boolean = node.kind === ts.SyntaxKind.Identifier; - this._sourceString.overwrite(node.getStart(this._sourceFile), - node.getEnd(), - replacement, - { storeName: replaceSymbolName }); - this._changed = true; - } + /** + * Transpiles the file with any changes added. + */ + transpile(compilerOptions: ts.CompilerOptions = {}): TranspileOutput { + const source = this.sourceText; - sourceMatch(re: RegExp) { - return this._sourceText.match(re) !== null; - } + let result: ts.TranspileOutput = { + outputText: undefined, + sourceMapText: undefined + }; - 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: '' - }), - fileName: this._fileName - }); + // Use program.emit() if we have a program, otherwise use ts.transpileModule(). + if (this._programManager) { + + // Custom writeFile to catch output from emit. + const writeFile = (fileName: string, data: string) => { + if (path.extname(fileName) === '.js') { + if (result.outputText !== undefined) { + // This really shouldn't happen, so error out if it does. + throw new Error(`Double JS emit for ${this._fileName}.`); + } + result.outputText = data; + } else if (path.extname(fileName) === '.map') { + result.sourceMapText = data; + } + }; + + if (this._changed) { + // Write the refactored file back to the host and update the program. + this._host.writeFile(this._fileName, source, false); + this._programManager.update(); + this._sourceFile = this._programManager.program.getSourceFile(this._fileName); + } + + const { emitSkipped } = this._programManager.program.emit(this._sourceFile, writeFile); + if (emitSkipped) { + throw new Error(`${this._fileName} emit failed.`); + } + + if (result.outputText === undefined) { + // Something went wrong in reading the emitted file; + throw new Error(`Could not retrieve emitted TypeScript for ${this._fileName}.`); + } + } else { + result = ts.transpileModule(source, { + compilerOptions: Object.assign({}, + compilerOptions, + { + sourceMap: true, + inlineSources: false, + inlineSourceMap: false, + sourceRoot: '' + }), + fileName: this._fileName + }); + } + + // Process the sourcemaps. if (result.sourceMapText) { const sourceMapJson = JSON.parse(result.sourceMapText); - sourceMapJson.sources = [ this._fileName ]; + sourceMapJson.sources = [this._fileName]; const consumer = new SourceMapConsumer(sourceMapJson); const map = SourceMapGenerator.fromSourceMap(consumer); + + // Chain sourcemaps back to original source. if (this._changed) { const sourceMap = this._sourceString.generateMap({ file: path.basename(this._fileName.replace(/\.ts$/, '.js')), @@ -232,12 +317,10 @@ export class TypeScriptFileRefactor { } const sourceMap = map.toJSON(); - const fileName = process.platform.startsWith('win') - ? this._fileName.replace(/\//g, '\\') - : this._fileName; - sourceMap.sources = [ fileName ]; + const fileName = this._fileName.replace(/\//g, path.sep); + sourceMap.sources = [fileName]; sourceMap.file = path.basename(fileName, '.ts') + '.js'; - sourceMap.sourcesContent = [ this._sourceText ]; + sourceMap.sourcesContent = [this._sourceText]; return { outputText: result.outputText, sourceMap }; } else { diff --git a/tests/e2e/tests/build/rebuild.ts b/tests/e2e/tests/build/rebuild.ts index c69be2658a9a..470e07eb7a79 100644 --- a/tests/e2e/tests/build/rebuild.ts +++ b/tests/e2e/tests/build/rebuild.ts @@ -65,7 +65,8 @@ export default function() { export class AppModule { } `)) // Should trigger a rebuild with a new bundle. - .then(() => waitForAnyProcessOutputToMatch(validBundleRegEx, 10000)) + // Adding lazy chunks can trigger a longer rebuild due to extra dependencies. + .then(() => waitForAnyProcessOutputToMatch(validBundleRegEx, 20000)) // Count the bundles. .then(({ stdout }) => { let newNumberOfChunks = stdout.split(chunkRegExp).length;