From 017234566e4e7e644f3937ab62d0ff56679b20d9 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 9 Jul 2018 09:18:40 -0400 Subject: [PATCH 01/10] refactor(@ngtools/webpack): support file replacement without copying --- .../webpack/src/angular_compiler_plugin.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/ngtools/webpack/src/angular_compiler_plugin.ts b/packages/ngtools/webpack/src/angular_compiler_plugin.ts index f6f2c8fe5a07..909fbe80c145 100644 --- a/packages/ngtools/webpack/src/angular_compiler_plugin.ts +++ b/packages/ngtools/webpack/src/angular_compiler_plugin.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import { dirname, normalize, resolve, virtualFs } from '@angular-devkit/core'; +import { NodeJsSyncHost } from '@angular-devkit/core/node'; import { ChildProcess, ForkOptions, fork } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; @@ -288,11 +289,21 @@ export class AngularCompilerPlugin { this._contextElementDependencyConstructor = options.contextElementDependencyConstructor || require('webpack/lib/dependencies/ContextElementDependency'); + + let host: virtualFs.Host = options.host || new NodeJsSyncHost(); + if (options.hostReplacementPaths) { + const aliasHost = new virtualFs.AliasHost(host); + for (const from in options.hostReplacementPaths) { + aliasHost.aliases.set(normalize(from), normalize(options.hostReplacementPaths[from])); + } + host = aliasHost; + } + // Create the webpack compiler host. const webpackCompilerHost = new WebpackCompilerHost( this._compilerOptions, this._basePath, - this._options.host, + host, ); webpackCompilerHost.enableCaching(); @@ -306,17 +317,6 @@ export class AngularCompilerPlugin { tsHost: webpackCompilerHost, }) as CompilerHost & WebpackCompilerHost; - // Override some files in the FileSystem with paths from the actual file system. - if (this._options.hostReplacementPaths) { - for (const filePath of Object.keys(this._options.hostReplacementPaths)) { - const replacementFilePath = this._options.hostReplacementPaths[filePath]; - const content = this._compilerHost.readFile(replacementFilePath); - if (content) { - this._compilerHost.writeFile(filePath, content, false); - } - } - } - // Resolve mainPath if provided. if (options.mainPath) { this._mainPath = this._compilerHost.resolve(options.mainPath); From d9ba54b1a91efa23559de6c714ef86b74a4d1332 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 9 Jul 2018 09:20:24 -0400 Subject: [PATCH 02/10] refactor(@angular-devkit/build-angular): use plugin for file replacement --- .../angular-cli-files/models/build-options.ts | 8 +- .../models/webpack-configs/typescript.ts | 10 +- .../build_angular/src/browser/index.ts | 5 +- .../build_angular/src/dev-server/index.ts | 5 +- .../build_angular/src/karma/index.ts | 5 +- .../build_angular/src/server/index.ts | 7 +- .../src/utils/add-file-replacements.ts | 74 --------------- .../build_angular/src/utils/index.ts | 2 +- .../src/utils/normalize-file-replacements.ts | 92 +++++++++++++++++++ 9 files changed, 122 insertions(+), 86 deletions(-) delete mode 100644 packages/angular_devkit/build_angular/src/utils/add-file-replacements.ts create mode 100644 packages/angular_devkit/build_angular/src/utils/normalize-file-replacements.ts diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts index c0f719e20bb3..a81e728e45de 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts @@ -10,7 +10,12 @@ // tslint:disable-next-line:no-implicit-dependencies import * as ts from 'typescript'; -import { AssetPatternObject, Budget, ExtraEntryPoint } from '../../browser/schema'; +import { + AssetPatternObject, + Budget, + CurrentFileReplacement, + ExtraEntryPoint, +} from '../../browser/schema'; export interface BuildOptions { optimization: boolean; @@ -58,6 +63,7 @@ export interface BuildOptions { stylePreprocessorOptions?: { includePaths: string[] }; lazyModules: string[]; platform?: 'browser' | 'server'; + fileReplacements: CurrentFileReplacement[]; } export interface WebpackTestOptions extends BuildOptions { diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts index 6db7273b6cc4..65fb92697a2a 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts @@ -7,7 +7,7 @@ */ // tslint:disable // TODO: cleanup this file, it's copied as is from Angular CLI. -import { tags, virtualFs } from '@angular-devkit/core'; +import { virtualFs } from '@angular-devkit/core'; import { Stats } from 'fs'; import * as path from 'path'; import { @@ -62,6 +62,13 @@ function _createAotPlugin( } } + const hostReplacementPaths: { [replace: string]: string } = {}; + if (buildOptions.fileReplacements) { + for (const replacement of buildOptions.fileReplacements) { + hostReplacementPaths[replacement.replace] = replacement.with; + } + } + const pluginOptions: AngularCompilerPluginOptions = { mainPath: useMain ? path.join(root, buildOptions.main) : undefined, ...i18nFileAndFormat, @@ -70,6 +77,7 @@ function _createAotPlugin( missingTranslation: buildOptions.i18nMissingTranslation, sourceMap: buildOptions.sourceMap, additionalLazyModules, + hostReplacementPaths, nameLazyFiles: buildOptions.namedChunks, forkTypeChecker: buildOptions.forkTypeChecker, contextElementDependencyConstructor: require('webpack/lib/dependencies/ContextElementDependency'), diff --git a/packages/angular_devkit/build_angular/src/browser/index.ts b/packages/angular_devkit/build_angular/src/browser/index.ts index 9dff620ff51a..a4c12abc36a9 100644 --- a/packages/angular_devkit/build_angular/src/browser/index.ts +++ b/packages/angular_devkit/build_angular/src/browser/index.ts @@ -34,7 +34,7 @@ import { statsToString, statsWarningsToString, } from '../angular-cli-files/utilities/stats'; -import { addFileReplacements, normalizeAssetPatterns } from '../utils'; +import { normalizeAssetPatterns, normalizeFileReplacements } from '../utils'; import { AssetPatternObject, BrowserBuilderSchema, CurrentFileReplacement } from './schema'; const webpackMerge = require('webpack-merge'); @@ -64,7 +64,8 @@ export class BrowserBuilder implements Builder { concatMap(() => options.deleteOutputPath ? this._deleteOutputDir(root, normalize(options.outputPath), this.context.host) : of(null)), - concatMap(() => addFileReplacements(root, host, options.fileReplacements)), + concatMap(() => normalizeFileReplacements(options.fileReplacements, host, root)), + tap(fileReplacements => options.fileReplacements = fileReplacements), concatMap(() => normalizeAssetPatterns( options.assets, host, root, projectRoot, builderConfig.sourceRoot)), // Replace the assets in options with the normalized version. diff --git a/packages/angular_devkit/build_angular/src/dev-server/index.ts b/packages/angular_devkit/build_angular/src/dev-server/index.ts index d368aab2bc3c..70c3d8f58533 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/index.ts +++ b/packages/angular_devkit/build_angular/src/dev-server/index.ts @@ -25,7 +25,7 @@ import * as WebpackDevServer from 'webpack-dev-server'; import { checkPort } from '../angular-cli-files/utilities/check-port'; import { BrowserBuilder, NormalizedBrowserBuilderSchema, getBrowserLoggingCb } from '../browser/'; import { BrowserBuilderSchema } from '../browser/schema'; -import { addFileReplacements, normalizeAssetPatterns } from '../utils'; +import { normalizeAssetPatterns, normalizeFileReplacements } from '../utils'; const opn = require('opn'); @@ -78,7 +78,8 @@ export class DevServerBuilder implements Builder { tap((port) => options.port = port), concatMap(() => this._getBrowserOptions(options)), tap((opts) => browserOptions = opts), - concatMap(() => addFileReplacements(root, host, browserOptions.fileReplacements)), + concatMap(() => normalizeFileReplacements(browserOptions.fileReplacements, host, root)), + tap(fileReplacements => browserOptions.fileReplacements = fileReplacements), concatMap(() => normalizeAssetPatterns( browserOptions.assets, host, root, projectRoot, builderConfig.sourceRoot)), // Replace the assets in options with the normalized version. diff --git a/packages/angular_devkit/build_angular/src/karma/index.ts b/packages/angular_devkit/build_angular/src/karma/index.ts index e880dfde8c39..09474bf35f70 100644 --- a/packages/angular_devkit/build_angular/src/karma/index.ts +++ b/packages/angular_devkit/build_angular/src/karma/index.ts @@ -27,7 +27,7 @@ import { import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig'; import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module'; import { AssetPatternObject, CurrentFileReplacement } from '../browser/schema'; -import { addFileReplacements, normalizeAssetPatterns } from '../utils'; +import { normalizeAssetPatterns, normalizeFileReplacements } from '../utils'; import { KarmaBuilderSchema } from './schema'; const webpackMerge = require('webpack-merge'); @@ -47,7 +47,8 @@ export class KarmaBuilder implements Builder { const host = new virtualFs.AliasHost(this.context.host as virtualFs.Host); return of(null).pipe( - concatMap(() => addFileReplacements(root, host, options.fileReplacements)), + concatMap(() => normalizeFileReplacements(options.fileReplacements, host, root)), + tap(fileReplacements => options.fileReplacements = fileReplacements), concatMap(() => normalizeAssetPatterns( options.assets, host, root, projectRoot, builderConfig.sourceRoot)), // Replace the assets in options with the normalized version. diff --git a/packages/angular_devkit/build_angular/src/server/index.ts b/packages/angular_devkit/build_angular/src/server/index.ts index b70f6715f8be..140ad0e6ae79 100644 --- a/packages/angular_devkit/build_angular/src/server/index.ts +++ b/packages/angular_devkit/build_angular/src/server/index.ts @@ -16,7 +16,7 @@ import { WebpackBuilder } from '@angular-devkit/build-webpack'; import { Path, getSystemPath, normalize, resolve, virtualFs } from '@angular-devkit/core'; import { Stats } from 'fs'; import { Observable, concat, of } from 'rxjs'; -import { concatMap, last } from 'rxjs/operators'; +import { concatMap, last, tap } from 'rxjs/operators'; import * as ts from 'typescript'; // tslint:disable-line:no-implicit-dependencies import { WebpackConfigOptions } from '../angular-cli-files/models/build-options'; import { @@ -30,7 +30,7 @@ import { import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig'; import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module'; import { getBrowserLoggingCb } from '../browser'; -import { addFileReplacements } from '../utils'; +import { normalizeFileReplacements } from '../utils'; import { BuildWebpackServerSchema } from './schema'; const webpackMerge = require('webpack-merge'); @@ -51,7 +51,8 @@ export class ServerBuilder implements Builder { concatMap(() => options.deleteOutputPath ? this._deleteOutputDir(root, normalize(options.outputPath), this.context.host) : of(null)), - concatMap(() => addFileReplacements(root, host, options.fileReplacements)), + concatMap(() => normalizeFileReplacements(options.fileReplacements, host, root)), + tap(fileReplacements => options.fileReplacements = fileReplacements), concatMap(() => { const webpackConfig = this.buildWebpackConfig(root, projectRoot, host, options); diff --git a/packages/angular_devkit/build_angular/src/utils/add-file-replacements.ts b/packages/angular_devkit/build_angular/src/utils/add-file-replacements.ts deleted file mode 100644 index f776160f0328..000000000000 --- a/packages/angular_devkit/build_angular/src/utils/add-file-replacements.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @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 { BaseException, Path, join, normalize, virtualFs } from '@angular-devkit/core'; -import { Observable, forkJoin, of } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; -import { - CurrentFileReplacement, - DeprecatedFileReplacment, - FileReplacement, -} from '../browser/schema'; - - -export class MissingFileReplacementException extends BaseException { - constructor(path: String) { - super(`The ${path} path in file replacements does not exist.`); - } -} - -// Note: This method changes the file replacements in place. -export function addFileReplacements( - root: Path, - host: virtualFs.AliasHost, - fileReplacements: FileReplacement[], -): Observable { - - if (fileReplacements.length === 0) { - return of(null); - } - - // Normalize the legacy format into the current one. - for (const fileReplacement of fileReplacements) { - const currentFormat = fileReplacement as CurrentFileReplacement; - const maybeOldFormat = fileReplacement as DeprecatedFileReplacment; - - if (maybeOldFormat.src && maybeOldFormat.replaceWith) { - currentFormat.replace = maybeOldFormat.src; - currentFormat.with = maybeOldFormat.replaceWith; - } - } - - const normalizedFileReplacements = fileReplacements as CurrentFileReplacement[]; - - // Ensure all the replacements exist. - const errorOnFalse = (path: string) => tap((exists: boolean) => { - if (!exists) { - throw new MissingFileReplacementException(path); - } - }); - - const existObservables = normalizedFileReplacements - .map(replacement => [ - host.exists(join(root, replacement.replace)).pipe(errorOnFalse(replacement.replace)), - host.exists(join(root, replacement.with)).pipe(errorOnFalse(replacement.with)), - ]) - .reduce((prev, curr) => prev.concat(curr), []); - - return forkJoin(existObservables).pipe( - tap(() => { - normalizedFileReplacements.forEach(replacement => { - host.aliases.set( - join(root, normalize(replacement.replace)), - join(root, normalize(replacement.with)), - ); - }); - }), - map(() => null), - ); -} diff --git a/packages/angular_devkit/build_angular/src/utils/index.ts b/packages/angular_devkit/build_angular/src/utils/index.ts index edfd8da8167f..488c3f4c9e8d 100644 --- a/packages/angular_devkit/build_angular/src/utils/index.ts +++ b/packages/angular_devkit/build_angular/src/utils/index.ts @@ -7,5 +7,5 @@ */ export * from './run-module-as-observable-fork'; -export * from './add-file-replacements'; +export * from './normalize-file-replacements'; export * from './normalize-asset-patterns'; diff --git a/packages/angular_devkit/build_angular/src/utils/normalize-file-replacements.ts b/packages/angular_devkit/build_angular/src/utils/normalize-file-replacements.ts new file mode 100644 index 000000000000..9345e2aa85d9 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/utils/normalize-file-replacements.ts @@ -0,0 +1,92 @@ +/** + * @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 { + BaseException, + Path, + getSystemPath, + join, + normalize, + virtualFs, +} from '@angular-devkit/core'; +import { Observable, from, of } from 'rxjs'; +import { concat, concatMap, ignoreElements, map, mergeMap, tap, toArray } from 'rxjs/operators'; +import { + CurrentFileReplacement, + DeprecatedFileReplacment, + FileReplacement, +} from '../browser/schema'; + + +export class MissingFileReplacementException extends BaseException { + constructor(path: String) { + super(`The ${path} path in file replacements does not exist.`); + } +} + +export interface NormalizedFileReplacement { + replace: Path; + with: Path; +} + +export function normalizeFileReplacements( + fileReplacements: FileReplacement[], + host: virtualFs.Host, + root: Path, +): Observable { + if (fileReplacements.length === 0) { + return of([]); + } + + // Ensure all the replacements exist. + const errorOnFalse = (path: Path) => tap((exists: boolean) => { + if (!exists) { + throw new MissingFileReplacementException(getSystemPath(path)); + } + }); + + return from(fileReplacements).pipe( + map(replacement => normalizeFileReplacement(replacement, root)), + concatMap(normalized => { + return from([normalized.replace, normalized.with]).pipe( + mergeMap(path => host.exists(path).pipe(errorOnFalse(path))), + ignoreElements(), + concat(of(normalized)), + ); + }), + toArray(), + ); +} + +function normalizeFileReplacement( + fileReplacement: FileReplacement, + root?: Path, +): NormalizedFileReplacement { + const currentFormat = fileReplacement as CurrentFileReplacement; + const maybeOldFormat = fileReplacement as DeprecatedFileReplacment; + + let replacePath: Path; + let withPath: Path; + if (maybeOldFormat.src && maybeOldFormat.replaceWith) { + replacePath = normalize(maybeOldFormat.src); + withPath = normalize(maybeOldFormat.replaceWith); + } else { + replacePath = normalize(currentFormat.replace); + withPath = normalize(currentFormat.with); + } + + // TODO: For 7.x should this only happen if not absolute? + if (root) { + replacePath = join(root, replacePath); + } + if (root) { + withPath = join(root, withPath); + } + + return { replace: replacePath, with: withPath }; +} From 32c487d97e41f8517af96ef69b6372443afdd7d7 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 9 Jul 2018 12:05:35 -0400 Subject: [PATCH 03/10] fix(@ngtools/webpack): support watching file replacements --- .../webpack/src/angular_compiler_plugin.ts | 15 ++- .../src/virtual_file_system_decorator.ts | 101 +++++++++++++++--- 2 files changed, 98 insertions(+), 18 deletions(-) diff --git a/packages/ngtools/webpack/src/angular_compiler_plugin.ts b/packages/ngtools/webpack/src/angular_compiler_plugin.ts index 909fbe80c145..c9d872d47e69 100644 --- a/packages/ngtools/webpack/src/angular_compiler_plugin.ts +++ b/packages/ngtools/webpack/src/angular_compiler_plugin.ts @@ -5,7 +5,7 @@ * 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 { dirname, normalize, resolve, virtualFs } from '@angular-devkit/core'; +import { Path, dirname, normalize, resolve, virtualFs } from '@angular-devkit/core'; import { NodeJsSyncHost } from '@angular-devkit/core/node'; import { ChildProcess, ForkOptions, fork } from 'child_process'; import * as fs from 'fs'; @@ -617,8 +617,21 @@ export class AngularCompilerPlugin { this._compilerHost, ); compilerWithFileSystems.inputFileSystem = inputDecorator; + + let replacements: Map | undefined; + if (this._options.hostReplacementPaths) { + replacements = new Map(); + for (const replace in this._options.hostReplacementPaths) { + replacements.set( + normalize(replace), + normalize(this._options.hostReplacementPaths[replace]), + ); + } + } + compilerWithFileSystems.watchFileSystem = new VirtualWatchFileSystemDecorator( inputDecorator, + replacements, ); }); diff --git a/packages/ngtools/webpack/src/virtual_file_system_decorator.ts b/packages/ngtools/webpack/src/virtual_file_system_decorator.ts index 205a87e2c391..bf122ca54c77 100644 --- a/packages/ngtools/webpack/src/virtual_file_system_decorator.ts +++ b/packages/ngtools/webpack/src/virtual_file_system_decorator.ts @@ -5,6 +5,7 @@ * 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 { Path, getSystemPath, normalize } from '@angular-devkit/core'; import { Stats } from 'fs'; import { WebpackCompilerHost } from './compiler_host'; import { Callback, InputFileSystem, NodeWatchFileSystemInterface } from './webpack'; @@ -105,26 +106,52 @@ export class VirtualFileSystemDecorator implements InputFileSystem { } export class VirtualWatchFileSystemDecorator extends NodeWatchFileSystem { - constructor(private _virtualInputFileSystem: VirtualFileSystemDecorator) { + constructor( + private _virtualInputFileSystem: VirtualFileSystemDecorator, + private _replacements?: Map, + ) { super(_virtualInputFileSystem); } watch( - files: any, // tslint:disable-line:no-any - dirs: any, // tslint:disable-line:no-any - missing: any, // tslint:disable-line:no-any - startTime: any, // tslint:disable-line:no-any - options: any, // tslint:disable-line:no-any + files: string[], + dirs: string[], + missing: string[], + startTime: number | undefined, + options: {}, callback: any, // tslint:disable-line:no-any - callbackUndelayed: any, // tslint:disable-line:no-any + callbackUndelayed: (filename: string, timestamp: number) => void, ) { + 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]); + } + } + + return 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 = ( - err: any, // tslint:disable-line:no-any - filesModified: any, // tslint:disable-line:no-any - contextModified: any, // tslint:disable-line:no-any - missingModified: any, // tslint:disable-line:no-any - fileTimestamps: { [k: string]: number }, - contextTimestamps: { [k: string]: number }, + 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() @@ -132,11 +159,51 @@ export class VirtualWatchFileSystemDecorator extends NodeWatchFileSystem { path: fileName, mtime: +this._virtualInputFileSystem.statSync(fileName).mtime, })); - virtualFilesStats.forEach(stats => fileTimestamps[stats.path] = +stats.mtime); - callback(err, filesModified, contextModified, missingModified, fileTimestamps, - contextTimestamps); + 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), + ); + }; + + const mapReplacements = (original: string[]): string[] => { + if (!this._replacements) { + return original; + } + const replacements = this._replacements; + + return original.map(file => { + const replacement = replacements.get(normalize(file)); + if (replacement) { + const fullReplacement = getSystemPath(replacement); + reverseReplacements.set(fullReplacement, file); + + return fullReplacement; + } else { + return file; + } + }); }; - return super.watch(files, dirs, missing, startTime, options, newCallback, callbackUndelayed); + 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()), + }; } } From 208a2b8014b8ca898c9f96d0b344b9d30755d897 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 10 Jul 2018 13:08:25 -0400 Subject: [PATCH 04/10] test(@angular/cli): add E2E test for file replacement watching --- .../e2e/tests/build/rebuild-replacements.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/legacy-cli/e2e/tests/build/rebuild-replacements.ts diff --git a/tests/legacy-cli/e2e/tests/build/rebuild-replacements.ts b/tests/legacy-cli/e2e/tests/build/rebuild-replacements.ts new file mode 100644 index 000000000000..6d1fd83cf8f4 --- /dev/null +++ b/tests/legacy-cli/e2e/tests/build/rebuild-replacements.ts @@ -0,0 +1,31 @@ +import { getGlobalVariable } from '../../utils/env'; +import { appendToFile } from '../../utils/fs'; +import { + execAndWaitForOutputToMatch, + killAllProcesses, + waitForAnyProcessOutputToMatch, +} from '../../utils/process'; + +const webpackGoodRegEx = /: Compiled successfully./; + +export default async function() { + if (process.platform.startsWith('win')) { + return; + } + + let error; + try { + await execAndWaitForOutputToMatch('ng', ['serve', '--prod'], webpackGoodRegEx); + + // Should trigger a rebuild. + await appendToFile('src/environments/environment.prod.ts', `console.log('PROD');`); + await waitForAnyProcessOutputToMatch(webpackGoodRegEx, 30000); + } catch (e) { + error = e; + } + + killAllProcesses(); + if (error) { + throw error; + } +} From ffa2636c191abf9396444a882860fb8f0762cc42 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 10 Jul 2018 15:06:29 -0400 Subject: [PATCH 05/10] fix(@angular-devkit/core): allow use of ResolverHost --- packages/angular_devkit/core/src/virtual-fs/host/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/angular_devkit/core/src/virtual-fs/host/index.ts b/packages/angular_devkit/core/src/virtual-fs/host/index.ts index c044890aeb76..f7c963dcfc86 100644 --- a/packages/angular_devkit/core/src/virtual-fs/host/index.ts +++ b/packages/angular_devkit/core/src/virtual-fs/host/index.ts @@ -16,6 +16,7 @@ export * from './record'; export * from './safe'; export * from './scoped'; export * from './sync'; +export * from './resolver'; import * as test from './test'; From b9ee21ee2980c3c52adacaa304eb09665fe4cc7d Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 10 Jul 2018 15:14:48 -0400 Subject: [PATCH 06/10] refactor(@ngtools/webpack): support function-based host file replacement --- .../webpack/src/angular_compiler_plugin.ts | 40 +++++++++++++------ .../src/virtual_file_system_decorator.ts | 23 +++++++---- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/packages/ngtools/webpack/src/angular_compiler_plugin.ts b/packages/ngtools/webpack/src/angular_compiler_plugin.ts index c9d872d47e69..7b4286c8e897 100644 --- a/packages/ngtools/webpack/src/angular_compiler_plugin.ts +++ b/packages/ngtools/webpack/src/angular_compiler_plugin.ts @@ -5,7 +5,7 @@ * 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 { Path, dirname, normalize, resolve, virtualFs } from '@angular-devkit/core'; +import { Path, dirname, getSystemPath, normalize, resolve, virtualFs } from '@angular-devkit/core'; import { NodeJsSyncHost } from '@angular-devkit/core/node'; import { ChildProcess, ForkOptions, fork } from 'child_process'; import * as fs from 'fs'; @@ -77,7 +77,7 @@ export interface AngularCompilerPluginOptions { entryModule?: string; mainPath?: string; skipCodeGeneration?: boolean; - hostReplacementPaths?: { [path: string]: string }; + hostReplacementPaths?: { [path: string]: string } | ((path: string) => string); forkTypeChecker?: boolean; // TODO: remove singleFileIncludes for 2.0, this is just to support old projects that did not // include 'polyfills.ts' in `tsconfig.spec.json'. @@ -292,11 +292,20 @@ export class AngularCompilerPlugin { let host: virtualFs.Host = options.host || new NodeJsSyncHost(); if (options.hostReplacementPaths) { - const aliasHost = new virtualFs.AliasHost(host); - for (const from in options.hostReplacementPaths) { - aliasHost.aliases.set(normalize(from), normalize(options.hostReplacementPaths[from])); + if (typeof options.hostReplacementPaths == 'function') { + const replacementResolver = options.hostReplacementPaths; + host = new class extends virtualFs.ResolverHost { + _resolve(path: Path) { + return normalize(replacementResolver(getSystemPath(path))); + } + }(host); + } else { + const aliasHost = new virtualFs.AliasHost(host); + for (const from in options.hostReplacementPaths) { + aliasHost.aliases.set(normalize(from), normalize(options.hostReplacementPaths[from])); + } + host = aliasHost; } - host = aliasHost; } // Create the webpack compiler host. @@ -618,14 +627,19 @@ export class AngularCompilerPlugin { ); compilerWithFileSystems.inputFileSystem = inputDecorator; - let replacements: Map | undefined; + let replacements: Map | ((path: Path) => Path) | undefined; if (this._options.hostReplacementPaths) { - replacements = new Map(); - for (const replace in this._options.hostReplacementPaths) { - replacements.set( - normalize(replace), - normalize(this._options.hostReplacementPaths[replace]), - ); + if (typeof this._options.hostReplacementPaths === 'function') { + const replacementResolver = this._options.hostReplacementPaths; + replacements = path => normalize(replacementResolver(getSystemPath(path))); + } else { + replacements = new Map(); + for (const replace in this._options.hostReplacementPaths) { + replacements.set( + normalize(replace), + normalize(this._options.hostReplacementPaths[replace]), + ); + } } } diff --git a/packages/ngtools/webpack/src/virtual_file_system_decorator.ts b/packages/ngtools/webpack/src/virtual_file_system_decorator.ts index bf122ca54c77..0efe9895978a 100644 --- a/packages/ngtools/webpack/src/virtual_file_system_decorator.ts +++ b/packages/ngtools/webpack/src/virtual_file_system_decorator.ts @@ -108,7 +108,7 @@ export class VirtualFileSystemDecorator implements InputFileSystem { export class VirtualWatchFileSystemDecorator extends NodeWatchFileSystem { constructor( private _virtualInputFileSystem: VirtualFileSystemDecorator, - private _replacements?: Map, + private _replacements?: Map | ((path: Path) => Path), ) { super(_virtualInputFileSystem); } @@ -177,14 +177,23 @@ export class VirtualWatchFileSystemDecorator extends NodeWatchFileSystem { const replacements = this._replacements; return original.map(file => { - const replacement = replacements.get(normalize(file)); - if (replacement) { - const fullReplacement = getSystemPath(replacement); - reverseReplacements.set(fullReplacement, file); + if (typeof replacements === 'function') { + const replacement = getSystemPath(replacements(normalize(file))); + if (replacement !== file) { + reverseReplacements.set(replacement, file); + } - return fullReplacement; + return replacement; } else { - return file; + const replacement = replacements.get(normalize(file)); + if (replacement) { + const fullReplacement = getSystemPath(replacement); + reverseReplacements.set(fullReplacement, file); + + return fullReplacement; + } else { + return file; + } } }); }; From 043ecdec3b77b73544571bc2d925d4e087a0c149 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 11 Jul 2018 10:02:19 -0400 Subject: [PATCH 07/10] fix(@angular-devkit/build-angular): fix webpack hostadapter return types --- .../utils/webpack-file-system-host-adapter.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/utils/webpack-file-system-host-adapter.ts b/packages/angular_devkit/build_angular/src/utils/webpack-file-system-host-adapter.ts index 461d1ce88d77..0b95101bb98f 100644 --- a/packages/angular_devkit/build_angular/src/utils/webpack-file-system-host-adapter.ts +++ b/packages/angular_devkit/build_angular/src/utils/webpack-file-system-host-adapter.ts @@ -93,9 +93,9 @@ export class WebpackFileSystemHostAdapter implements InputFileSystem { return this._doHostCall(this._host.list(normalize('/' + path)), callback); } - readFile(path: string, callback: Callback): void { + readFile(path: string, callback: Callback): void { const o = this._host.read(normalize('/' + path)).pipe( - map(content => virtualFs.fileBufferToString(content)), + map(content => Buffer.from(content)), ); return this._doHostCall(o, callback); @@ -134,15 +134,21 @@ export class WebpackFileSystemHostAdapter implements InputFileSystem { return this._syncHost.list(normalize('/' + path)); } - readFileSync(path: string): string { + readFileSync(path: string): Buffer { if (!this._syncHost) { this._syncHost = new virtualFs.SyncDelegateHost(this._host); } - return virtualFs.fileBufferToString(this._syncHost.read(normalize('/' + path))); + return Buffer.from(this._syncHost.read(normalize('/' + path))); } - readJsonSync(path: string): string { - return JSON.parse(this.readFileSync(path)); + readJsonSync(path: string): {} { + if (!this._syncHost) { + this._syncHost = new virtualFs.SyncDelegateHost(this._host); + } + + const data = this._syncHost.read(normalize('/' + path)); + + return JSON.parse(virtualFs.fileBufferToString(data)); } readlinkSync(path: string): string { const err: NodeJS.ErrnoException = new Error('Not a symlink.'); From d592cad511e629aa686bed95feb4224d4f779acf Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 11 Jul 2018 10:03:18 -0400 Subject: [PATCH 08/10] refactor(@ngtools/webpack): use webpack input filesystem as default host --- packages/ngtools/webpack/package.json | 1 + .../webpack/src/angular_compiler_plugin.ts | 95 +++++++++---------- .../src/virtual_file_system_decorator.ts | 4 +- .../ngtools/webpack/src/webpack-input-host.ts | 95 +++++++++++++++++++ packages/ngtools/webpack/src/webpack.ts | 4 +- 5 files changed, 143 insertions(+), 56 deletions(-) create mode 100644 packages/ngtools/webpack/src/webpack-input-host.ts diff --git a/packages/ngtools/webpack/package.json b/packages/ngtools/webpack/package.json index 4994d170a917..f80fc090b473 100644 --- a/packages/ngtools/webpack/package.json +++ b/packages/ngtools/webpack/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "@angular-devkit/core": "0.0.0", + "rxjs": "^6.0.0", "tree-kill": "^1.0.0", "webpack-sources": "^1.1.0" }, diff --git a/packages/ngtools/webpack/src/angular_compiler_plugin.ts b/packages/ngtools/webpack/src/angular_compiler_plugin.ts index 7b4286c8e897..bc6151078dfa 100644 --- a/packages/ngtools/webpack/src/angular_compiler_plugin.ts +++ b/packages/ngtools/webpack/src/angular_compiler_plugin.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ import { Path, dirname, getSystemPath, normalize, resolve, virtualFs } from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; import { ChildProcess, ForkOptions, fork } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; @@ -58,6 +57,7 @@ import { NodeWatchFileSystemInterface, NormalModuleFactoryRequest, } from './webpack'; +import { WebpackInputHost } from './webpack-input-host'; const treeKill = require('tree-kill'); @@ -289,48 +289,6 @@ export class AngularCompilerPlugin { this._contextElementDependencyConstructor = options.contextElementDependencyConstructor || require('webpack/lib/dependencies/ContextElementDependency'); - - let host: virtualFs.Host = options.host || new NodeJsSyncHost(); - if (options.hostReplacementPaths) { - if (typeof options.hostReplacementPaths == 'function') { - const replacementResolver = options.hostReplacementPaths; - host = new class extends virtualFs.ResolverHost { - _resolve(path: Path) { - return normalize(replacementResolver(getSystemPath(path))); - } - }(host); - } else { - const aliasHost = new virtualFs.AliasHost(host); - for (const from in options.hostReplacementPaths) { - aliasHost.aliases.set(normalize(from), normalize(options.hostReplacementPaths[from])); - } - host = aliasHost; - } - } - - // Create the webpack compiler host. - const webpackCompilerHost = new WebpackCompilerHost( - this._compilerOptions, - this._basePath, - host, - ); - webpackCompilerHost.enableCaching(); - - // Create and set a new WebpackResourceLoader. - this._resourceLoader = new WebpackResourceLoader(); - webpackCompilerHost.setResourceLoader(this._resourceLoader); - - // Use the WebpackCompilerHost with a resource loader to create an AngularCompilerHost. - this._compilerHost = createCompilerHost({ - options: this._compilerOptions, - tsHost: webpackCompilerHost, - }) as CompilerHost & WebpackCompilerHost; - - // Resolve mainPath if provided. - if (options.mainPath) { - this._mainPath = this._compilerHost.resolve(options.mainPath); - } - // Use entryModule if available in options, otherwise resolve it from mainPath after program // creation. if (this._options.entryModule) { @@ -621,28 +579,61 @@ export class AngularCompilerPlugin { watchFileSystem: NodeWatchFileSystemInterface, }; - const inputDecorator = new VirtualFileSystemDecorator( + let host: virtualFs.Host = this._options.host || new WebpackInputHost( compilerWithFileSystems.inputFileSystem, - this._compilerHost, ); - compilerWithFileSystems.inputFileSystem = inputDecorator; let replacements: Map | ((path: Path) => Path) | undefined; if (this._options.hostReplacementPaths) { - if (typeof this._options.hostReplacementPaths === 'function') { + if (typeof this._options.hostReplacementPaths == 'function') { const replacementResolver = this._options.hostReplacementPaths; replacements = path => normalize(replacementResolver(getSystemPath(path))); + host = new class extends virtualFs.ResolverHost { + _resolve(path: Path) { + return normalize(replacementResolver(getSystemPath(path))); + } + }(host); } else { replacements = new Map(); - for (const replace in this._options.hostReplacementPaths) { - replacements.set( - normalize(replace), - normalize(this._options.hostReplacementPaths[replace]), - ); + const aliasHost = new virtualFs.AliasHost(host); + for (const from in this._options.hostReplacementPaths) { + const normalizedFrom = normalize(from); + const normalizedWith = normalize(this._options.hostReplacementPaths[from]); + aliasHost.aliases.set(normalizedFrom, normalizedWith); + replacements.set(normalizedFrom, normalizedWith); } + host = aliasHost; } } + // Create the webpack compiler host. + const webpackCompilerHost = new WebpackCompilerHost( + this._compilerOptions, + this._basePath, + host, + ); + webpackCompilerHost.enableCaching(); + + // Create and set a new WebpackResourceLoader. + this._resourceLoader = new WebpackResourceLoader(); + webpackCompilerHost.setResourceLoader(this._resourceLoader); + + // Use the WebpackCompilerHost with a resource loader to create an AngularCompilerHost. + this._compilerHost = createCompilerHost({ + options: this._compilerOptions, + tsHost: webpackCompilerHost, + }) as CompilerHost & WebpackCompilerHost; + + // Resolve mainPath if provided. + if (this._options.mainPath) { + this._mainPath = this._compilerHost.resolve(this._options.mainPath); + } + + const inputDecorator = new VirtualFileSystemDecorator( + compilerWithFileSystems.inputFileSystem, + this._compilerHost, + ); + compilerWithFileSystems.inputFileSystem = inputDecorator; compilerWithFileSystems.watchFileSystem = new VirtualWatchFileSystemDecorator( inputDecorator, replacements, diff --git a/packages/ngtools/webpack/src/virtual_file_system_decorator.ts b/packages/ngtools/webpack/src/virtual_file_system_decorator.ts index 0efe9895978a..aedf56665b6f 100644 --- a/packages/ngtools/webpack/src/virtual_file_system_decorator.ts +++ b/packages/ngtools/webpack/src/virtual_file_system_decorator.ts @@ -52,7 +52,7 @@ export class VirtualFileSystemDecorator implements InputFileSystem { this._inputFileSystem.readdir(path, callback); } - readFile(path: string, callback: Callback): void { + readFile(path: string, callback: Callback): void { const result = this._readFileSync(path); if (result) { callback(null, result); @@ -79,7 +79,7 @@ export class VirtualFileSystemDecorator implements InputFileSystem { return this._inputFileSystem.readdirSync(path); } - readFileSync(path: string): string | Buffer { + readFileSync(path: string): Buffer { const result = this._readFileSync(path); return result || this._inputFileSystem.readFileSync(path); diff --git a/packages/ngtools/webpack/src/webpack-input-host.ts b/packages/ngtools/webpack/src/webpack-input-host.ts new file mode 100644 index 000000000000..41201d1e88dc --- /dev/null +++ b/packages/ngtools/webpack/src/webpack-input-host.ts @@ -0,0 +1,95 @@ +/** + * @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 { Path, PathFragment, fragment, getSystemPath, virtualFs } from '@angular-devkit/core'; +import { Stats } from 'fs'; +import { Observable, throwError } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { InputFileSystem } from './webpack'; + +// Host is used instead of ReadonlyHost due to most decorators only supporting Hosts +export class WebpackInputHost implements virtualFs.Host { + + constructor(public readonly inputFileSystem: InputFileSystem) { } + + get capabilities(): virtualFs.HostCapabilities { + return { synchronous: true }; + } + + write(_path: Path, _content: virtualFs.FileBufferLike) { + return throwError(new Error('Not supported.')); + } + + delete(_path: Path) { + return throwError(new Error('Not supported.')); + } + + rename(_from: Path, _to: Path) { + return throwError(new Error('Not supported.')); + } + + read(path: Path): Observable { + return new Observable(obs => { + // TODO: remove this try+catch when issue https://github.com/ReactiveX/rxjs/issues/3740 is + // fixed. + try { + const data = this.inputFileSystem.readFileSync(getSystemPath(path)); + obs.next(new Uint8Array(data).buffer as ArrayBuffer); + obs.complete(); + } catch (e) { + obs.error(e); + } + }); + } + + list(path: Path): Observable { + return new Observable(obs => { + // TODO: remove this try+catch when issue https://github.com/ReactiveX/rxjs/issues/3740 is + // fixed. + try { + const names = this.inputFileSystem.readdirSync(getSystemPath(path)); + obs.next(names.map(name => fragment(name))); + obs.complete(); + } catch (err) { + obs.error(err); + } + }); + } + + exists(path: Path): Observable { + return this.stat(path).pipe(map(stats => stats != null)); + } + + isDirectory(path: Path): Observable { + return this.stat(path).pipe(map(stats => stats != null && stats.isDirectory())); + } + + isFile(path: Path): Observable { + return this.stat(path).pipe(map(stats => stats != null && stats.isFile())); + } + + stat(path: Path): Observable { + return new Observable(obs => { + try { + const stats = this.inputFileSystem.statSync(getSystemPath(path)); + obs.next(stats); + obs.complete(); + } catch (e) { + if (e.code === 'ENOENT') { + obs.next(null); + obs.complete(); + } else { + obs.error(e); + } + } + }); + } + + watch(_path: Path, _options?: virtualFs.HostWatchOptions): null { + return null; + } +} diff --git a/packages/ngtools/webpack/src/webpack.ts b/packages/ngtools/webpack/src/webpack.ts index b857051ee20c..e9035fb7b9e6 100644 --- a/packages/ngtools/webpack/src/webpack.ts +++ b/packages/ngtools/webpack/src/webpack.ts @@ -22,12 +22,12 @@ export interface NormalModuleFactoryRequest { export interface InputFileSystem { stat(path: string, callback: Callback): void; readdir(path: string, callback: Callback): void; - readFile(path: string, callback: Callback): void; + readFile(path: string, callback: Callback): void; readJson(path: string, callback: Callback): void; readlink(path: string, callback: Callback): void; statSync(path: string): Stats; readdirSync(path: string): string[]; - readFileSync(path: string): string | Buffer; + readFileSync(path: string): Buffer; // tslint:disable-next-line:no-any readJsonSync(path: string): any; readlinkSync(path: string): string; From 4016933af647a8c1f9062de9dbef24768133760c Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 11 Jul 2018 10:05:35 -0400 Subject: [PATCH 09/10] refactor(@angular-devkit/build-angular): allow webpack plugin to use default host --- .../angular-cli-files/models/webpack-configs/typescript.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts index 65fb92697a2a..9583208abab8 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts @@ -28,9 +28,9 @@ const webpackLoader: string = g['_DevKitIsLocal'] function _createAotPlugin( wco: WebpackConfigOptions, options: any, - host: virtualFs.Host, + _host: virtualFs.Host, useMain = true, - extract = false + extract = false, ) { const { root, buildOptions } = wco; options.compilerOptions = options.compilerOptions || {}; @@ -82,7 +82,6 @@ function _createAotPlugin( forkTypeChecker: buildOptions.forkTypeChecker, contextElementDependencyConstructor: require('webpack/lib/dependencies/ContextElementDependency'), ...options, - host, }; return new AngularCompilerPlugin(pluginOptions); } From 838b2f4eb8688994a68f27b565b7726f080a93da Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 11 Jul 2018 12:33:10 -0400 Subject: [PATCH 10/10] test(@angular-devkit/build-angular): add file replacement watch test courtesy of @filipesilva --- .../test/browser/replacements_spec_large.ts | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/angular_devkit/build_angular/test/browser/replacements_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/replacements_spec_large.ts index f96ac4a10a0a..f03555356f3f 100644 --- a/packages/angular_devkit/build_angular/test/browser/replacements_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/browser/replacements_spec_large.ts @@ -8,7 +8,7 @@ import { runTargetSpec } from '@angular-devkit/architect/testing'; import { join, normalize, virtualFs } from '@angular-devkit/core'; -import { tap } from 'rxjs/operators'; +import { debounceTime, take, tap } from 'rxjs/operators'; import { browserTargetSpec, host } from '../utils'; @@ -100,4 +100,45 @@ describe('Browser Builder file replacements', () => { runTargetSpec(host, browserTargetSpec, overrides) .subscribe(undefined, () => done(), done.fail); }); + + it('file replacements work with watch mode', (done) => { + const overrides = { + fileReplacements: [ + { + replace: normalize('/src/meaning.ts'), + with: normalize('/src/meaning-too.ts'), + }, + ], + watch: true, + }; + + let buildNumber = 0; + + runTargetSpec(host, browserTargetSpec, overrides, 45000).pipe( + debounceTime(1000), + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'main.js'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(fileName)); + buildNumber += 1; + + switch (buildNumber) { + case 1: + expect(content).toMatch(/meaning\s*=\s*42/); + expect(content).not.toMatch(/meaning\s*=\s*10/); + host.writeMultipleFiles({ + 'src/meaning-too.ts': 'export var meaning = 84;', + }); + break; + + case 2: + expect(content).toMatch(/meaning\s*=\s*84/); + expect(content).not.toMatch(/meaning\s*=\s*42/); + break; + } + }), + take(2), + ).toPromise().then(() => done(), done.fail); + }); + });