diff --git a/packages/@ngtools/webpack/src/compiler_host.ts b/packages/@ngtools/webpack/src/compiler_host.ts index 2a9255909fe6..e8f49d645bce 100644 --- a/packages/@ngtools/webpack/src/compiler_host.ts +++ b/packages/@ngtools/webpack/src/compiler_host.ts @@ -146,70 +146,6 @@ export class WebpackCompilerHost implements ts.CompilerHost { this._cache = true; } - populateWebpackResolver(resolver: any) { - const fs = resolver.fileSystem; - if (!this.dirty) { - return; - } - - /** - * storageDataSetter is a temporary hack to address these two issues: - * - https://github.com/angular/angular-cli/issues/7113 - * - https://github.com/angular/angular-cli/issues/7136 - * - * This way we set values correctly in both a Map (enhanced-resove>=3.4.0) and - * object (enhanced-resolve >= 3.1.0 <3.4.0). - * - * The right solution is to create a virtual filesystem by decorating the filesystem, - * instead of injecting data into the private cache of the filesystem. - * - * Doing it the right way should fix other related bugs, but meanwhile we hack it since: - * - it's affecting a lot of users. - * - the real solution is non-trivial. - */ - function storageDataSetter(data: Map | {[k: string]: any}, k: string, v: any) { - - if (data instanceof Map) { - data.set(k, v); - } else { - data[k] = v; - } - } - - - - const isWindows = process.platform.startsWith('win'); - for (const fileName of this.getChangedFilePaths()) { - const stats = this._files[fileName]; - if (stats) { - // If we're on windows, we need to populate with the proper path separator. - const path = isWindows ? fileName.replace(/\//g, '\\') : fileName; - // fs._statStorage.data[path] = [null, stats]; - // fs._readFileStorage.data[path] = [null, stats.content]; - storageDataSetter(fs._statStorage.data, path, [null, stats]); - storageDataSetter(fs._readFileStorage.data, path, [null, stats.content]); - } else { - // Support removing files as well. - const path = isWindows ? fileName.replace(/\//g, '\\') : fileName; - // fs._statStorage.data[path] = [new Error(), null]; - // fs._readFileStorage.data[path] = [new Error(), null]; - storageDataSetter(fs._statStorage.data, path, [new Error(), null]); - storageDataSetter(fs._readFileStorage.data, path, [new Error(), null]); - } - } - for (const dirName of Object.keys(this._changedDirs)) { - const stats = this._directories[dirName]; - const dirs = this.getDirectories(dirName); - const files = this.getFiles(dirName); - // If we're on windows, we need to populate with the proper path separator. - const path = isWindows ? dirName.replace(/\//g, '\\') : dirName; - // fs._statStorage.data[path] = [null, stats]; - // fs._readdirStorage.data[path] = [null, files.concat(dirs)]; - storageDataSetter(fs._statStorage.data, path, [null, stats]); - storageDataSetter(fs._readFileStorage.data, path, [null, files.concat(dirs)]); - } - } - resetChangedFileTracker() { this._changedFiles = Object.create(null); this._changedDirs = Object.create(null); @@ -226,9 +162,9 @@ export class WebpackCompilerHost implements ts.CompilerHost { } } - fileExists(fileName: string): boolean { + fileExists(fileName: string, delegate = true): boolean { fileName = this._resolve(fileName); - return this._files[fileName] != null || this._delegate.fileExists(fileName); + return this._files[fileName] != null || (delegate && this._delegate.fileExists(fileName)); } readFile(fileName: string): string { @@ -247,10 +183,17 @@ export class WebpackCompilerHost implements ts.CompilerHost { return stats.content; } - directoryExists(directoryName: string): boolean { + // Does not delegate, use with `fileExists/directoryExists()`. + stat(path: string): VirtualStats { + path = this._resolve(path); + return this._files[path] || this._directories[path]; + } + + directoryExists(directoryName: string, delegate = true): boolean { directoryName = this._resolve(directoryName); return (this._directories[directoryName] != null) - || (this._delegate.directoryExists != undefined + || (delegate + && this._delegate.directoryExists != undefined && this._delegate.directoryExists(directoryName)); } diff --git a/packages/@ngtools/webpack/src/plugin.ts b/packages/@ngtools/webpack/src/plugin.ts index 12261cc786dd..1840d2dfe704 100644 --- a/packages/@ngtools/webpack/src/plugin.ts +++ b/packages/@ngtools/webpack/src/plugin.ts @@ -6,6 +6,7 @@ import * as SourceMap from 'source-map'; const {__NGTOOLS_PRIVATE_API_2, VERSION} = require('@angular/compiler-cli'); const ContextElementDependency = require('webpack/lib/dependencies/ContextElementDependency'); +const NodeWatchFileSystem = require('webpack/lib/node/NodeWatchFileSystem'); import {WebpackResourceLoader} from './resource_loader'; import {WebpackCompilerHost} from './compiler_host'; @@ -13,6 +14,7 @@ import {resolveEntryModuleFromMain} from './entry_resolver'; import {Tapable} from './webpack'; import {PathsPlugin} from './paths-plugin'; import {findLazyRoutes, LazyRouteMap} from './lazy_routes'; +import {VirtualFileSystemDecorator} from './virtual_file_system_decorator'; /** @@ -308,16 +310,18 @@ export class AotPlugin implements Tapable { apply(compiler: any) { this._compiler = compiler; + // Decorate inputFileSystem to serve contents of CompilerHost. + // Use decorated inputFileSystem in watchFileSystem. + compiler.plugin('environment', () => { + compiler.inputFileSystem = new VirtualFileSystemDecorator( + compiler.inputFileSystem, this._compilerHost); + compiler.watchFileSystem = new NodeWatchFileSystem(compiler.inputFileSystem); + }); + compiler.plugin('invalid', () => { // Turn this off as soon as a file becomes invalid and we're about to start a rebuild. this._firstRun = false; this._diagnoseFiles = {}; - - if (compiler.watchFileSystem.watcher) { - compiler.watchFileSystem.watcher.once('aggregated', (changes: string[]) => { - changes.forEach((fileName: string) => this._compilerHost.invalidate(fileName)); - }); - } }); // Add lazy modules to the context module for @angular/core/src/linker @@ -543,10 +547,6 @@ export class AotPlugin implements Tapable { } } }) - .then(() => { - // Populate the file system cache with the virtual module. - this._compilerHost.populateWebpackResolver(this._compiler.resolvers.normal); - }) .then(() => { // We need to run the `listLazyRoutes` the first time because it also navigates libraries // and other things that we might miss using the findLazyRoutesInAst. diff --git a/packages/@ngtools/webpack/src/virtual_file_system_decorator.ts b/packages/@ngtools/webpack/src/virtual_file_system_decorator.ts new file mode 100644 index 000000000000..e980df9df4a4 --- /dev/null +++ b/packages/@ngtools/webpack/src/virtual_file_system_decorator.ts @@ -0,0 +1,107 @@ +import { Stats } from 'fs'; +import { InputFileSystem, Callback } from './webpack'; +import { WebpackCompilerHost } from './compiler_host'; + + +export class VirtualFileSystemDecorator implements InputFileSystem { + constructor( + private _inputFileSystem: InputFileSystem, + private _webpackCompilerHost: WebpackCompilerHost + ) { } + + private _readFileSync(path: string): string | null { + if (this._webpackCompilerHost.fileExists(path, false)) { + return this._webpackCompilerHost.readFile(path); + } + + return null; + } + + private _statSync(path: string): Stats | null { + if (this._webpackCompilerHost.fileExists(path, false) + || this._webpackCompilerHost.directoryExists(path, false)) { + return this._webpackCompilerHost.stat(path); + } + + return null; + } + + private _readDirSync(path: string): string[] | null { + if (this._webpackCompilerHost.directoryExists(path, false)) { + const dirs = this._webpackCompilerHost.getDirectories(path); + const files = this._webpackCompilerHost.getFiles(path); + return files.concat(dirs); + } + + return null; + } + + stat(path: string, callback: Callback): void { + const result = this._statSync(path); + if (result) { + callback(null, result); + } else { + this._inputFileSystem.stat(path, callback); + } + } + + readdir(path: string, callback: Callback): void { + const result = this._readDirSync(path); + if (result) { + callback(null, result); + } else { + this._inputFileSystem.readdir(path, callback); + } + } + + readFile(path: string, callback: Callback): void { + const result = this._readFileSync(path); + if (result) { + callback(null, result); + } else { + this._inputFileSystem.readFile(path, callback); + } + } + + readJson(path: string, callback: Callback): void { + this._inputFileSystem.readJson(path, callback); + } + + readlink(path: string, callback: Callback): void { + this._inputFileSystem.readlink(path, callback); + } + + statSync(path: string): Stats { + const result = this._statSync(path); + return result || this._inputFileSystem.statSync(path); + } + + readdirSync(path: string): string[] { + const result = this._readDirSync(path); + return result || this._inputFileSystem.readdirSync(path); + } + + readFileSync(path: string): string { + const result = this._readFileSync(path); + return result || this._inputFileSystem.readFileSync(path); + } + + readJsonSync(path: string): string { + return this._inputFileSystem.readJsonSync(path); + } + + readlinkSync(path: string): string { + return this._inputFileSystem.readlinkSync(path); + } + + purge(changes?: string[] | string): void { + if (typeof changes === 'string') { + this._webpackCompilerHost.invalidate(changes); + } else if (Array.isArray(changes)) { + changes.forEach((fileName: string) => this._webpackCompilerHost.invalidate(fileName)); + } + if (this._inputFileSystem.purge) { + this._inputFileSystem.purge(changes); + } + } +} diff --git a/packages/@ngtools/webpack/src/webpack.ts b/packages/@ngtools/webpack/src/webpack.ts index e374f8001ca9..32d23501c8a8 100644 --- a/packages/@ngtools/webpack/src/webpack.ts +++ b/packages/@ngtools/webpack/src/webpack.ts @@ -1,3 +1,5 @@ +import { Stats } from 'fs'; + // Declarations for (some) Webpack types. Only what's needed. export interface Request { @@ -60,3 +62,16 @@ export interface LoaderContext { readonly query: any; } +export interface InputFileSystem { + stat(path: string, callback: Callback): void; + readdir(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; + readJsonSync(path: string): string; + readlinkSync(path: string): string; + purge(changes?: string[] | string): void; +}