UNPKG

webpack

Version:

Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.

324 lines (304 loc) • 9.88 kB
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const util = require("util"); const memoize = require("../util/memoize"); // watchpack is only reachable from `watch`, so non-watch builds never load it const getWatchpack = memoize(() => require("watchpack")); /** @typedef {InstanceType<import("watchpack")>} Watchpack */ /** @import { TimeInfoEntries, WatchOptions } from "watchpack" */ /** * @import { * InputFileSystem, * WatchMethod, * Changes, * Removals * } from "../fs/fs" */ // Watchpack pools DirectoryWatchers per options object identity; interning by // value lets a MultiCompiler's children share one pool instead of one scan each. /** @type {Map<string, WatchOptions>} */ const canonicalWatchOptions = new Map(); /** @type {WeakMap<RegExp, string>} */ const regExpKeys = new WeakMap(); let nextRegExpKey = 0; // A RegExp keys by reference, so two configs spelling the same one inline do // not share a pool. /** * @param {WatchOptions["ignored"]} ignored the `ignored` option * @returns {string} key describing it, or "" when it cannot be keyed */ const ignoredKey = (ignored) => { if (ignored === undefined || ignored === null) return "-"; if (typeof ignored === "string") return `s${ignored}`; if (Array.isArray(ignored)) return `a${JSON.stringify(ignored)}`; if (!(ignored instanceof RegExp)) return ""; let key = regExpKeys.get(ignored); if (key === undefined) { key = `r${nextRegExpKey++}`; regExpKeys.set(ignored, key); } return key; }; // Snapshot rather than keep the caller's object: mutating it later would hand // the next caller options it never asked for. /** * @param {WatchOptions} options watch options * @returns {WatchOptions} a shared object equal to `options`, or `options` */ const getCanonicalWatchOptions = (options) => { const ignored = ignoredKey(options.ignored); if (ignored === "") return options; const key = `${options.aggregateTimeout}|${Boolean( options.followSymlinks )}|${options.poll}|${ignored}`; const canonical = canonicalWatchOptions.get(key); if (canonical !== undefined) return canonical; /** @type {WatchOptions} */ const snapshot = { aggregateTimeout: options.aggregateTimeout, followSymlinks: options.followSymlinks, ignored: Array.isArray(options.ignored) ? [...options.ignored] : options.ignored, poll: options.poll }; canonicalWatchOptions.set(key, snapshot); return snapshot; }; /** @typedef {Iterable<string> & { has(item: string): boolean }} Lookup */ /** * Returns the items as they are when they have `has`, else as a set, which * also keeps a later change by the caller or a one-shot iterable from mattering. * @param {Iterable<string>} items items * @returns {Lookup} the items, as something with `has` */ const toLookup = (items) => typeof (/** @type {Set<string>} */ (items).has) === "function" ? /** @type {Set<string>} */ (items) : new Set(items); class NodeWatchFileSystem { /** * Creates an instance of NodeWatchFileSystem. * @param {InputFileSystem} inputFileSystem input filesystem */ constructor(inputFileSystem) { /** @type {InputFileSystem} */ this.inputFileSystem = inputFileSystem; /** @type {WatchOptions} */ this.watcherOptions = { aggregateTimeout: 0 }; // `watch` replaces this with a watcher built from its own options /** @type {Watchpack | null} */ this.watcher = null; /** @type {{ files: Lookup, directories: Lookup, missing: Lookup } | undefined} */ this._watched = undefined; } /** * Purges what the previous call watched and this one does not: an edit to a * path nobody watches is never reported, so its cached stat and content would * be read stale once the path is watched again. * @param {Lookup} files files to watch * @param {Lookup} directories directories to watch * @param {Lookup} missing missing paths to watch * @returns {void} */ _purgeUnwatched(files, directories, missing) { const previous = this._watched; this._watched = { files, directories, missing }; const fs = this.inputFileSystem; if (previous === undefined || !fs || !fs.purge) return; /** @type {string[]} */ const dropped = []; for (const list of [ previous.files, previous.directories, previous.missing ]) { for (const item of list) { if (!files.has(item) && !directories.has(item) && !missing.has(item)) { dropped.push(item); } } } // a prefix purge scans the whole cache once per path if (dropped.length > 0) fs.purge(dropped, { exact: true }); } /** @type {WatchMethod} */ watch( files, directories, missing, startTime, options, callback, callbackUndelayed ) { if (!files || typeof files[Symbol.iterator] !== "function") { throw new Error("Invalid arguments: 'files'"); } if (!directories || typeof directories[Symbol.iterator] !== "function") { throw new Error("Invalid arguments: 'directories'"); } if (!missing || typeof missing[Symbol.iterator] !== "function") { throw new Error("Invalid arguments: 'missing'"); } if (typeof callback !== "function") { throw new Error("Invalid arguments: 'callback'"); } if (typeof startTime !== "number" && startTime) { throw new Error("Invalid arguments: 'startTime'"); } if (typeof options !== "object") { throw new Error("Invalid arguments: 'options'"); } if (typeof callbackUndelayed !== "function" && callbackUndelayed) { throw new Error("Invalid arguments: 'callbackUndelayed'"); } const filesLookup = toLookup(files); const directoriesLookup = toLookup(directories); const missingLookup = toLookup(missing); this._purgeUnwatched(filesLookup, directoriesLookup, missingLookup); const oldWatcher = this.watcher; this.watcher = new (getWatchpack())(getCanonicalWatchOptions(options)); if (callbackUndelayed) { this.watcher.once("change", callbackUndelayed); } const fetchTimeInfo = () => { /** @type {TimeInfoEntries} */ const fileTimeInfoEntries = new Map(); /** @type {TimeInfoEntries} */ const contextTimeInfoEntries = new Map(); if (this.watcher) { this.watcher.collectTimeInfoEntries( fileTimeInfoEntries, contextTimeInfoEntries ); } return { fileTimeInfoEntries, contextTimeInfoEntries }; }; // Watchpack reports a watched directory whenever its contents change, and // `fs.purge(dir)` matches by prefix, wiping every file's stat cache with it. // Only its own entry is purged; the batch's file events carry the rest. /** * @param {Changes | null | undefined} changes changes set * @param {Removals | null | undefined} removals removals set */ const purgeChanges = (changes, removals) => { const fs = this.inputFileSystem; if (!fs || !fs.purge) return; if (changes) { for (const item of changes) { if (directoriesLookup.has(item)) { fs.purge(item, { exact: true }); } else { fs.purge(item); } } } if (removals) { for (const item of removals) { fs.purge(item); } } }; this.watcher.once( "aggregated", /** * Handles the callback logic for this hook. * @param {Changes} changes changes * @param {Removals} removals removals */ (changes, removals) => { // pause emitting events (avoids clearing aggregated changes and removals on timeout) /** @type {Watchpack} */ (this.watcher).pause(); purgeChanges(changes, removals); const { fileTimeInfoEntries, contextTimeInfoEntries } = fetchTimeInfo(); callback( null, fileTimeInfoEntries, contextTimeInfoEntries, changes, removals ); } ); this.watcher.watch({ files: filesLookup, directories: directoriesLookup, missing: missingLookup, startTime }); if (oldWatcher) { oldWatcher.close(); } return { close: () => { if (this.watcher) { this.watcher.close(); this.watcher = null; } }, pause: () => { if (this.watcher) { this.watcher.pause(); } }, getAggregatedRemovals: util.deprecate( () => { const items = this.watcher && this.watcher.aggregatedRemovals; const fs = this.inputFileSystem; if (items && fs && fs.purge) { for (const item of items) { fs.purge(item); } } return items; }, "Watcher.getAggregatedRemovals is deprecated in favor of Watcher.getInfo since that's more performant.", "DEP_WEBPACK_WATCHER_GET_AGGREGATED_REMOVALS" ), getAggregatedChanges: util.deprecate( () => { const items = this.watcher && this.watcher.aggregatedChanges; const fs = this.inputFileSystem; if (items && fs && fs.purge) { for (const item of items) { fs.purge(item); } } return items; }, "Watcher.getAggregatedChanges is deprecated in favor of Watcher.getInfo since that's more performant.", "DEP_WEBPACK_WATCHER_GET_AGGREGATED_CHANGES" ), getFileTimeInfoEntries: util.deprecate( () => fetchTimeInfo().fileTimeInfoEntries, "Watcher.getFileTimeInfoEntries is deprecated in favor of Watcher.getInfo since that's more performant.", "DEP_WEBPACK_WATCHER_FILE_TIME_INFO_ENTRIES" ), getContextTimeInfoEntries: util.deprecate( () => fetchTimeInfo().contextTimeInfoEntries, "Watcher.getContextTimeInfoEntries is deprecated in favor of Watcher.getInfo since that's more performant.", "DEP_WEBPACK_WATCHER_CONTEXT_TIME_INFO_ENTRIES" ), getInfo: () => { const removals = this.watcher && this.watcher.aggregatedRemovals; const changes = this.watcher && this.watcher.aggregatedChanges; purgeChanges(changes, removals); const { fileTimeInfoEntries, contextTimeInfoEntries } = fetchTimeInfo(); return { changes, removals, fileTimeInfoEntries, contextTimeInfoEntries }; } }; } } module.exports = NodeWatchFileSystem;