From d9073427575c8af3f21d162e924d5f1e480004db Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 17 Aug 2020 21:07:09 -0400 Subject: [PATCH 1/4] refactor(@angular-devkit/build-angular): adjust Webpack configuration based on version 4/5 This change adds helpers to allow the Webpack configuration to be automatically adjusted based on the Webpack version in use. These helpers are then used to provide configuration compatibility for Webpack 5 which has several differences to the Webpack 4 configuration. Behavior is maintained between the two versions so that external option behavior is not altered. --- .../build_angular/src/utils/process-bundle.ts | 15 ++++++---- .../src/utils/webpack-diagnostics.ts | 15 ++-------- .../src/utils/webpack-version.ts | 28 +++++++++++++++++++ .../src/webpack/configs/browser.ts | 2 ++ .../src/webpack/configs/common.ts | 23 +++++++++------ .../src/webpack/configs/server.ts | 14 +++++++++- 6 files changed, 70 insertions(+), 27 deletions(-) create mode 100644 packages/angular_devkit/build_angular/src/utils/webpack-version.ts diff --git a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts index 9715e4562e8e..a1e0c53bd968 100644 --- a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts +++ b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts @@ -32,6 +32,7 @@ import { } from 'webpack-sources'; import { allowMangle, allowMinify, shouldBeautify } from './environment-options'; import { I18nOptions } from './i18n-options'; +import { isWebpackFiveOrHigher } from './webpack-version'; type LocalizeUtilities = typeof import('@angular/localize/src/tools/src/source_file_utils'); @@ -219,21 +220,25 @@ async function mergeSourceMaps( filename: string, fast = false, ): Promise { - if (fast) { + // Webpack 5 terser sourcemaps currently fail merging with the high-quality method + // TODO_WEBPACK_5: Investigate high-quality sourcemap merge failures + if (fast || isWebpackFiveOrHigher()) { return mergeSourceMapsFast(inputSourceMap, resultSourceMap); } // SourceMapSource produces high-quality sourcemaps - // The last argument is not yet in the typings - // tslint:disable-next-line: no-any - return new (SourceMapSource as any)( + // Final sourcemap will always be available when providing the input sourcemaps + // tslint:disable-next-line: no-non-null-assertion + const finalSourceMap = new SourceMapSource( resultCode, filename, resultSourceMap, inputCode, inputSourceMap, true, - ).map(); + ).map()!; + + return finalSourceMap; } async function mergeSourceMapsFast(first: RawSourceMap, second: RawSourceMap) { diff --git a/packages/angular_devkit/build_angular/src/utils/webpack-diagnostics.ts b/packages/angular_devkit/build_angular/src/utils/webpack-diagnostics.ts index 7e7fe746e66d..d7ea3b4c44d6 100644 --- a/packages/angular_devkit/build_angular/src/utils/webpack-diagnostics.ts +++ b/packages/angular_devkit/build_angular/src/utils/webpack-diagnostics.ts @@ -6,21 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ import * as webpack from 'webpack'; +import { isWebpackFiveOrHigher } from './webpack-version'; const WebpackError = require('webpack/lib/WebpackError'); -const isWebpackFiveOrHigher = (() => { - if (typeof webpack.version === 'string') { - const versionParts = webpack.version.split('.'); - if (versionParts[0] && Number(versionParts[0]) >= 5) { - return true; - } - } - - return false; -})(); export function addWarning(compilation: webpack.compilation.Compilation, message: string): void { - if (isWebpackFiveOrHigher) { + if (isWebpackFiveOrHigher()) { compilation.warnings.push(new WebpackError(message)); } else { // Allows building with either Webpack 4 or 5+ types @@ -30,7 +21,7 @@ export function addWarning(compilation: webpack.compilation.Compilation, message } export function addError(compilation: webpack.compilation.Compilation, message: string): void { - if (isWebpackFiveOrHigher) { + if (isWebpackFiveOrHigher()) { compilation.errors.push(new WebpackError(message)); } else { // Allows building with either Webpack 4 or 5+ types diff --git a/packages/angular_devkit/build_angular/src/utils/webpack-version.ts b/packages/angular_devkit/build_angular/src/utils/webpack-version.ts new file mode 100644 index 000000000000..3cb37086265c --- /dev/null +++ b/packages/angular_devkit/build_angular/src/utils/webpack-version.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as webpack from 'webpack'; + +let cachedIsWebpackFiveOrHigher: boolean | undefined; +export function isWebpackFiveOrHigher(): boolean { + if (cachedIsWebpackFiveOrHigher === undefined) { + cachedIsWebpackFiveOrHigher = false; + if (typeof webpack.version === 'string') { + const versionParts = webpack.version.split('.'); + if (versionParts[0] && Number(versionParts[0]) >= 5) { + cachedIsWebpackFiveOrHigher = true; + } + } + } + + return cachedIsWebpackFiveOrHigher; +} + +// tslint:disable-next-line: no-any +export function withWebpackFourOrFive(webpackFourValue: T, webpackFiveValue: R): any { + return isWebpackFiveOrHigher() ? webpackFiveValue : webpackFourValue; +} diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts b/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts index f325a11917b3..9c7826e502af 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts @@ -7,6 +7,7 @@ */ import * as webpack from 'webpack'; import { WebpackConfigOptions } from '../../utils/build-options'; +import { withWebpackFourOrFive } from '../../utils/webpack-version'; import { CommonJsUsageWarnPlugin } from '../plugins'; import { getSourceMapDevTool } from '../utils/helpers'; @@ -71,6 +72,7 @@ export function getBrowserConfig(wco: WebpackConfigOptions): webpack.Configurati resolve: { mainFields: ['es2015', 'browser', 'module', 'main'], }, + ...withWebpackFourOrFive({}, { target: ['web', 'es5'] }), output: { crossOriginLoading, }, diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts index 4b734c4af204..117cbd7b574c 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts @@ -37,6 +37,7 @@ import { shouldBeautify, } from '../../utils/environment-options'; import { findAllNodeModules } from '../../utils/find-up'; +import { isWebpackFiveOrHigher, withWebpackFourOrFive } from '../../utils/webpack-version'; import { BundleBudgetPlugin, DedupeModuleResolvePlugin, @@ -317,7 +318,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration { ); } - if (buildOptions.namedChunks) { + if (buildOptions.namedChunks && !isWebpackFiveOrHigher()) { extraPlugins.push(new NamedLazyChunksPlugin()); } @@ -475,9 +476,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration { extensions: ['.ts', '.tsx', '.mjs', '.js'], symlinks: !buildOptions.preserveSymlinks, modules: [wco.tsConfig.options.baseUrl || projectRoot, 'node_modules'], - plugins: [ - PnpWebpackPlugin, - ], + plugins: isWebpackFiveOrHigher() ? [] : [PnpWebpackPlugin], }, resolveLoader: { symlinks: !buildOptions.preserveSymlinks, @@ -488,12 +487,12 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration { 'node_modules', ...findAllNodeModules(__dirname, projectRoot), ], - plugins: [PnpWebpackPlugin.moduleLoader(module)], + plugins: isWebpackFiveOrHigher() ? [] : [PnpWebpackPlugin.moduleLoader(module)], }, context: root, entry: entryPoints, output: { - futureEmitAssets: true, + ...withWebpackFourOrFive({ futureEmitAssets: true }, {}), path: path.resolve(root, buildOptions.outputPath), publicPath: buildOptions.deployUrl, filename: `[name]${targetInFileName}${hashFormat.chunk}.js`, @@ -501,7 +500,10 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration { watch: buildOptions.watch, watchOptions: { poll: buildOptions.poll, - ignored: buildOptions.poll === undefined ? undefined : /[\\\/]node_modules[\\\/]/, + ignored: + buildOptions.poll === undefined + ? undefined + : withWebpackFourOrFive(/[\\\/]node_modules[\\\/]/, 'node_modules/**'), }, performance: { hints: false, @@ -586,9 +588,12 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration { }, optimization: { minimizer: extraMinimizers, - moduleIds: 'hashed', - noEmitOnErrors: true, + moduleIds: withWebpackFourOrFive('hashed', 'deterministic'), + ...withWebpackFourOrFive({}, buildOptions.namedChunks ? { chunkIds: 'named' } : {}), + ...withWebpackFourOrFive({ noEmitOnErrors: true }, { emitOnErrors: false }), }, + // TODO_WEBPACK_5: Investigate non-working cache in development builds + ...withWebpackFourOrFive({}, { cache: false }), plugins: [ // Always replace the context for the System.import in angular/core to prevent warnings. // https://github.com/angular/angular/issues/11580 diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/server.ts b/packages/angular_devkit/build_angular/src/webpack/configs/server.ts index 6f643256f653..bbbfa72f04e2 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/server.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/server.ts @@ -8,8 +8,14 @@ import { isAbsolute } from 'path'; import { Configuration, ContextReplacementPlugin } from 'webpack'; import { WebpackConfigOptions } from '../../utils/build-options'; +import { isWebpackFiveOrHigher } from '../../utils/webpack-version'; import { getSourceMapDevTool } from '../utils/helpers'; +type ExternalHookWebpack5 = ( + data: { context: string; request: string }, + callback: (err?: Error, result?: string) => void, +) => void; + /** * Returns a partial Webpack configuration specific to creating a bundle for node * @param wco Options which include the build options and app config @@ -29,7 +35,13 @@ export function getServerConfig(wco: WebpackConfigOptions): Configuration { const externals: Configuration['externals'] = [...externalDependencies]; if (!bundleDependencies) { - externals.push(externalizePackages); + if (isWebpackFiveOrHigher()) { + const hook: ExternalHookWebpack5 = ({ context, request }, callback) => + externalizePackages(request, context, callback); + externals.push(hook); + } else { + externals.push(externalizePackages as unknown as ExternalHookWebpack5); + } } const config: Configuration = { From 56039b5a41e23d3fe3e8b9336bc7387ad6a415d1 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 8 Sep 2020 18:21:40 -0400 Subject: [PATCH 2/4] refactor(@angular-devkit/build-angular): temporarily disable Webpack 5 license extraction The license extraction plugin is currently causing errors when building/serving with Webpack 5. This change temporarily disables the option when Webpack 5 is used until further investigation can take place and to unblock initial Webpack 5 support. --- .../build_angular/src/webpack/configs/browser.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts b/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts index 9c7826e502af..591d6efe2360 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts @@ -7,7 +7,7 @@ */ import * as webpack from 'webpack'; import { WebpackConfigOptions } from '../../utils/build-options'; -import { withWebpackFourOrFive } from '../../utils/webpack-version'; +import { isWebpackFiveOrHigher, withWebpackFourOrFive } from '../../utils/webpack-version'; import { CommonJsUsageWarnPlugin } from '../plugins'; import { getSourceMapDevTool } from '../utils/helpers'; @@ -38,7 +38,13 @@ export function getBrowserConfig(wco: WebpackConfigOptions): webpack.Configurati })); } - if (extractLicenses) { + // TODO_WEBPACK_5: Investigate build/serve issues with the `license-webpack-plugin` package + if (extractLicenses && isWebpackFiveOrHigher()) { + wco.logger.warn( + 'WARNING: License extraction is currently disabled when using Webpack 5. ' + + 'This is temporary and will be corrected in a future update.', + ); + } else if (extractLicenses) { const LicenseWebpackPlugin = require('license-webpack-plugin').LicenseWebpackPlugin; extraPlugins.push(new LicenseWebpackPlugin({ stats: { From 27a3786b0a34cc6a44b2f20b42e92d1c03a49072 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 18 Sep 2020 03:29:26 -0400 Subject: [PATCH 3/4] refactor(@ngtools/webpack): adjust virtual file watching for Webpack 5 The Webpack 5 filesystem watch function has a different return object type than Webpack 4. This change adjusts the behavior based on the used Webpack version. --- .../src/virtual_file_system_decorator.ts | 256 ++++++++++++------ 1 file changed, 169 insertions(+), 87 deletions(-) diff --git a/packages/ngtools/webpack/src/virtual_file_system_decorator.ts b/packages/ngtools/webpack/src/virtual_file_system_decorator.ts index 54428d8ed50a..484060f5e0fb 100644 --- a/packages/ngtools/webpack/src/virtual_file_system_decorator.ts +++ b/packages/ngtools/webpack/src/virtual_file_system_decorator.ts @@ -10,6 +10,7 @@ import { Stats } from 'fs'; import { InputFileSystem } from 'webpack'; import { WebpackCompilerHost } from './compiler_host'; import { NodeWatchFileSystemInterface } from './webpack'; +import { isWebpackFiveOrHigher } from './webpack-version'; export const NodeWatchFileSystem: NodeWatchFileSystemInterface = require( 'webpack/lib/node/NodeWatchFileSystem'); @@ -112,106 +113,187 @@ export class VirtualWatchFileSystemDecorator extends NodeWatchFileSystem { super(_virtualInputFileSystem); } - watch = ( - files: Iterable, - dirs: Iterable, - missing: Iterable, - startTime: number, - options: {}, - callback: Parameters[5], - callbackUndelayed: (filename: string, timestamp: number) => void, - ): ReturnType => { - const reverseReplacements = new Map(); - const reverseTimestamps = (map: Map) => { - for (const entry of Array.from(map.entries())) { - const original = reverseReplacements.get(entry[0]); - if (original) { - map.set(original, entry[1]); - map.delete(entry[0]); + mapReplacements( + original: Iterable, + reverseReplacements: Map, + ): Iterable { + if (!this._replacements) { + return original; + } + const replacements = this._replacements; + + return [...original].map(file => { + if (typeof replacements === 'function') { + const replacement = getSystemPath(replacements(normalize(file))); + if (replacement !== file) { + reverseReplacements.set(replacement, file); } - } - return map; - }; + return replacement; + } else { + const replacement = replacements.get(normalize(file)); + if (replacement) { + const fullReplacement = getSystemPath(replacement); + reverseReplacements.set(fullReplacement, file); + + return fullReplacement; + } else { + return file; + } + } + }); + } - const newCallbackUndelayed = (filename: string, timestamp: number) => { - const original = reverseReplacements.get(filename); + reverseTimestamps( + map: Map, + reverseReplacements: Map, + ): Map { + for (const entry of Array.from(map.entries())) { + const original = reverseReplacements.get(entry[0]); if (original) { - this._virtualInputFileSystem.purge(original); - callbackUndelayed(original, timestamp); - } else { - callbackUndelayed(filename, timestamp); + map.set(original, entry[1]); + map.delete(entry[0]); } - }; + } + + return map; + } - const newCallback: Parameters[5] = ( - err: Error | null, - filesModified: string[], - contextModified: string[], - missingModified: string[], - fileTimestamps: Map, - contextTimestamps: Map, - ) => { - // Update fileTimestamps with timestamps from virtual files. - const virtualFilesStats = this._virtualInputFileSystem.getVirtualFilesPaths() - .map((fileName) => ({ - path: fileName, - mtime: +this._virtualInputFileSystem.statSync(fileName).mtime, - })); - virtualFilesStats.forEach(stats => fileTimestamps.set(stats.path, +stats.mtime)); - callback( - err, - filesModified.map(value => reverseReplacements.get(value) || value), - contextModified.map(value => reverseReplacements.get(value) || value), - missingModified.map(value => reverseReplacements.get(value) || value), - reverseTimestamps(fileTimestamps), - reverseTimestamps(contextTimestamps), + createWebpack4Watch() { + return ( + files: Iterable, + dirs: Iterable, + missing: Iterable, + startTime: number, + options: {}, + callback: Parameters[5], + callbackUndelayed: (filename: string, timestamp: number) => void, + ): ReturnType => { + const reverseReplacements = new Map(); + + const newCallbackUndelayed = (filename: string, timestamp: number) => { + const original = reverseReplacements.get(filename); + if (original) { + this._virtualInputFileSystem.purge(original); + callbackUndelayed(original, timestamp); + } else { + callbackUndelayed(filename, timestamp); + } + }; + + const newCallback: Parameters[5] = ( + err: Error | null, + filesModified: string[], + contextModified: string[], + missingModified: string[], + fileTimestamps: Map, + contextTimestamps: Map, + ) => { + // Update fileTimestamps with timestamps from virtual files. + const virtualFilesStats = this._virtualInputFileSystem.getVirtualFilesPaths() + .map((fileName) => ({ + path: fileName, + mtime: +this._virtualInputFileSystem.statSync(fileName).mtime, + })); + virtualFilesStats.forEach(stats => fileTimestamps.set(stats.path, +stats.mtime)); + callback( + err, + filesModified.map(value => reverseReplacements.get(value) || value), + contextModified.map(value => reverseReplacements.get(value) || value), + missingModified.map(value => reverseReplacements.get(value) || value), + this.reverseTimestamps(fileTimestamps, reverseReplacements), + this.reverseTimestamps(contextTimestamps, reverseReplacements), + ); + }; + + const watcher = super.watch( + this.mapReplacements(files, reverseReplacements), + this.mapReplacements(dirs, reverseReplacements), + this.mapReplacements(missing, reverseReplacements), + startTime, + options, + newCallback, + newCallbackUndelayed, ); - }; - const mapReplacements = (original: Iterable): Iterable => { - if (!this._replacements) { - return original; - } - const replacements = this._replacements; + return { + close: () => watcher.close(), + pause: () => watcher.pause(), + getFileTimestamps: () => + this.reverseTimestamps(watcher.getFileTimestamps(), reverseReplacements), + getContextTimestamps: () => + this.reverseTimestamps(watcher.getContextTimestamps(), reverseReplacements), + }; + }; + } - return [...original].map(file => { - if (typeof replacements === 'function') { - const replacement = getSystemPath(replacements(normalize(file))); - if (replacement !== file) { - reverseReplacements.set(replacement, file); - } + createWebpack5Watch() { + return ( + files: Iterable, + dirs: Iterable, + missing: Iterable, + startTime: number, + options: {}, + callback: Parameters[5], + callbackUndelayed: (filename: string, timestamp: number) => void, + ): ReturnType => { + const reverseReplacements = new Map(); - return replacement; + const newCallbackUndelayed = (filename: string, timestamp: number) => { + const original = reverseReplacements.get(filename); + if (original) { + this._virtualInputFileSystem.purge(original); + callbackUndelayed(original, timestamp); } else { - const replacement = replacements.get(normalize(file)); - if (replacement) { - const fullReplacement = getSystemPath(replacement); - reverseReplacements.set(fullReplacement, file); - - return fullReplacement; - } else { - return file; - } + callbackUndelayed(filename, timestamp); } - }); - }; + }; - const watcher = super.watch( - mapReplacements(files), - mapReplacements(dirs), - mapReplacements(missing), - startTime, - options, - newCallback, - newCallbackUndelayed, - ); - - return { - close: () => watcher.close(), - pause: () => watcher.pause(), - getFileTimestamps: () => reverseTimestamps(watcher.getFileTimestamps()), - getContextTimestamps: () => reverseTimestamps(watcher.getContextTimestamps()), + const newCallback = ( + err: Error, + // tslint:disable-next-line: no-any + fileTimeInfoEntries: Map, + // tslint:disable-next-line: no-any + contextTimeInfoEntries: Map, + missing: Set, + removals: Set, + ) => { + // Update fileTimestamps with timestamps from virtual files. + const virtualFilesStats = this._virtualInputFileSystem.getVirtualFilesPaths() + .map((fileName) => ({ + path: fileName, + mtime: +this._virtualInputFileSystem.statSync(fileName).mtime, + })); + virtualFilesStats.forEach(stats => fileTimeInfoEntries.set(stats.path, +stats.mtime)); + callback( + err, + this.reverseTimestamps(fileTimeInfoEntries, reverseReplacements), + this.reverseTimestamps(contextTimeInfoEntries, reverseReplacements), + new Set([...missing].map(value => reverseReplacements.get(value) || value)), + new Set([...removals].map(value => reverseReplacements.get(value) || value)), + ); + }; + + const watcher = super.watch( + this.mapReplacements(files, reverseReplacements), + this.mapReplacements(dirs, reverseReplacements), + this.mapReplacements(missing, reverseReplacements), + startTime, + options, + newCallback, + newCallbackUndelayed, + ); + + return { + close: () => watcher.close(), + pause: () => watcher.pause(), + getFileTimeInfoEntries: () => + this.reverseTimestamps(watcher.getFileTimeInfoEntries(), reverseReplacements), + getContextTimeInfoEntries: () => + this.reverseTimestamps(watcher.getContextTimeInfoEntries(), reverseReplacements), + }; }; } + + watch = isWebpackFiveOrHigher() ? this.createWebpack5Watch : this.createWebpack4Watch(); } From cf7364fa045f54302f1df0eb62753e258049d6b8 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 15 Sep 2020 15:23:15 -0400 Subject: [PATCH 4/4] test: add an experimental Webpack 5 E2E test --- tests/legacy-cli/e2e/tests/misc/webpack-5.ts | 33 ++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/legacy-cli/e2e/tests/misc/webpack-5.ts diff --git a/tests/legacy-cli/e2e/tests/misc/webpack-5.ts b/tests/legacy-cli/e2e/tests/misc/webpack-5.ts new file mode 100644 index 000000000000..1a1dea49b88c --- /dev/null +++ b/tests/legacy-cli/e2e/tests/misc/webpack-5.ts @@ -0,0 +1,33 @@ +import { rimraf } from '../../utils/fs'; +import { killAllProcesses, ng, silentYarn } from '../../utils/process'; +import { ngServe, updateJsonFile } from '../../utils/project'; + +export default async function() { + // Setup project for yarn usage + await rimraf('node_modules'); + await updateJsonFile('package.json', (json) => { + json.resolutions = { webpack: '5.0.0-beta.30' }; + }); + await silentYarn(); + await silentYarn('webdriver-update'); + + // Ensure webpack 5 is used + const { stdout } = await silentYarn('list', '--pattern', 'webpack'); + if (!/\swebpack@5/.test(stdout)) { + throw new Error('Expected Webpack 5 to be installed.'); + } + if (/\swebpack@4/.test(stdout)) { + throw new Error('Expected Webpack 4 to not be installed.'); + } + + // Execute the CLI with Webpack 5 + await ng('test', '--watch=false'); + await ng('build'); + await ng('build', '--prod'); + await ng('e2e'); + try { + await ngServe(); + } finally { + killAllProcesses(); + } +}