| | |
| | | */ |
| | | "use strict"; |
| | | |
| | | const { EventEmitter } = require("events"); |
| | | const fs = require("fs"); |
| | | const path = require("path"); |
| | | const { EventEmitter } = require("events"); |
| | | const reducePlan = require("./reducePlan"); |
| | | |
| | | /** @typedef {import("fs").FSWatcher} FSWatcher */ |
| | | /** @typedef {import("./index").EventType} EventType */ |
| | | |
| | | const IS_OSX = require("os").platform() === "darwin"; |
| | | const IS_WIN = require("os").platform() === "win32"; |
| | | |
| | | const SUPPORTS_RECURSIVE_WATCHING = IS_OSX || IS_WIN; |
| | | |
| | | // Use 20 for OSX to make `FSWatcher.close` faster |
| | | // https://github.com/nodejs/node/issues/29949 |
| | | const watcherLimit = |
| | | // @ts-expect-error avoid additional checks |
| | | +process.env.WATCHPACK_WATCHER_LIMIT || (IS_OSX ? 20 : 10000); |
| | | |
| | | const recursiveWatcherLogging = !!process.env |
| | | .WATCHPACK_RECURSIVE_WATCHER_LOGGING; |
| | | const recursiveWatcherLogging = Boolean( |
| | | process.env.WATCHPACK_RECURSIVE_WATCHER_LOGGING, |
| | | ); |
| | | |
| | | let isBatch = false; |
| | | let watcherCount = 0; |
| | |
| | | /** @type {Map<Watcher, RecursiveWatcher | DirectWatcher>} */ |
| | | const underlyingWatcher = new Map(); |
| | | |
| | | /** |
| | | * @param {string} filePath file path |
| | | * @returns {NodeJS.ErrnoException} new error with file path in the message |
| | | */ |
| | | function createEPERMError(filePath) { |
| | | const error = new Error(`Operation not permitted: ${filePath}`); |
| | | const error = |
| | | /** @type {NodeJS.ErrnoException} */ |
| | | (new Error(`Operation not permitted: ${filePath}`)); |
| | | error.code = "EPERM"; |
| | | return error; |
| | | } |
| | | |
| | | /** |
| | | * @param {FSWatcher} watcher watcher |
| | | * @param {string} filePath a file path |
| | | * @param {(type: "rename" | "change", filename: string) => void} handleChangeEvent function to handle change |
| | | * @returns {(type: "rename" | "change", filename: string) => void} handler of change event |
| | | */ |
| | | function createHandleChangeEvent(watcher, filePath, handleChangeEvent) { |
| | | return (type, filename) => { |
| | | // TODO: After Node.js v22, fs.watch(dir) and deleting a dir will trigger the rename change event. |
| | |
| | | } |
| | | |
| | | class DirectWatcher { |
| | | /** |
| | | * @param {string} filePath file path |
| | | */ |
| | | constructor(filePath) { |
| | | this.filePath = filePath; |
| | | this.watchers = new Set(); |
| | | /** @type {FSWatcher | undefined} */ |
| | | this.watcher = undefined; |
| | | try { |
| | | const watcher = fs.watch(filePath); |
| | |
| | | for (const w of this.watchers) { |
| | | w.emit("change", type, filename); |
| | | } |
| | | } |
| | | }, |
| | | ); |
| | | watcher.on("change", handleChangeEvent); |
| | | watcher.on("error", error => { |
| | | watcher.on("error", (error) => { |
| | | for (const w of this.watchers) { |
| | | w.emit("error", error); |
| | | } |
| | |
| | | watcherCount++; |
| | | } |
| | | |
| | | /** |
| | | * @param {Watcher} watcher a watcher |
| | | */ |
| | | add(watcher) { |
| | | underlyingWatcher.set(watcher, this); |
| | | this.watchers.add(watcher); |
| | | } |
| | | |
| | | /** |
| | | * @param {Watcher} watcher a watcher |
| | | */ |
| | | remove(watcher) { |
| | | this.watchers.delete(watcher); |
| | | if (this.watchers.size === 0) { |
| | |
| | | } |
| | | } |
| | | |
| | | /** @typedef {Set<Watcher>} WatcherSet */ |
| | | |
| | | class RecursiveWatcher { |
| | | /** |
| | | * @param {string} rootPath a root path |
| | | */ |
| | | constructor(rootPath) { |
| | | this.rootPath = rootPath; |
| | | /** @type {Map<Watcher, string>} */ |
| | | this.mapWatcherToPath = new Map(); |
| | | /** @type {Map<string, Set<Watcher>>} */ |
| | | /** @type {Map<string, WatcherSet>} */ |
| | | this.mapPathToWatchers = new Map(); |
| | | this.watcher = undefined; |
| | | try { |
| | | const watcher = fs.watch(rootPath, { |
| | | recursive: true |
| | | recursive: true, |
| | | }); |
| | | this.watcher = watcher; |
| | | watcher.on("change", (type, filename) => { |
| | | if (!filename) { |
| | | if (recursiveWatcherLogging) { |
| | | process.stderr.write( |
| | | `[watchpack] dispatch ${type} event in recursive watcher (${this.rootPath}) to all watchers\n` |
| | | `[watchpack] dispatch ${type} event in recursive watcher (${this.rootPath}) to all watchers\n`, |
| | | ); |
| | | } |
| | | for (const w of this.mapWatcherToPath.keys()) { |
| | | w.emit("change", type); |
| | | w.emit("change", /** @type {EventType} */ (type)); |
| | | } |
| | | } else { |
| | | const dir = path.dirname(filename); |
| | | const dir = path.dirname(/** @type {string} */ (filename)); |
| | | const watchers = this.mapPathToWatchers.get(dir); |
| | | if (recursiveWatcherLogging) { |
| | | process.stderr.write( |
| | |
| | | this.rootPath |
| | | }) for '${filename}' to ${ |
| | | watchers ? watchers.size : 0 |
| | | } watchers\n` |
| | | } watchers\n`, |
| | | ); |
| | | } |
| | | if (watchers === undefined) return; |
| | | for (const w of watchers) { |
| | | w.emit("change", type, path.basename(filename)); |
| | | w.emit( |
| | | "change", |
| | | /** @type {EventType} */ (type), |
| | | path.basename(/** @type {string} */ (filename)), |
| | | ); |
| | | } |
| | | } |
| | | }); |
| | | watcher.on("error", error => { |
| | | watcher.on("error", (error) => { |
| | | for (const w of this.mapWatcherToPath.keys()) { |
| | | w.emit("error", error); |
| | | } |
| | |
| | | watcherCount++; |
| | | if (recursiveWatcherLogging) { |
| | | process.stderr.write( |
| | | `[watchpack] created recursive watcher at ${rootPath}\n` |
| | | `[watchpack] created recursive watcher at ${rootPath}\n`, |
| | | ); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * @param {string} filePath a file path |
| | | * @param {Watcher} watcher a watcher |
| | | */ |
| | | add(filePath, watcher) { |
| | | underlyingWatcher.set(watcher, this); |
| | | const subpath = filePath.slice(this.rootPath.length + 1) || "."; |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * @param {Watcher} watcher a watcher |
| | | */ |
| | | remove(watcher) { |
| | | const subpath = this.mapWatcherToPath.get(watcher); |
| | | if (!subpath) return; |
| | | this.mapWatcherToPath.delete(watcher); |
| | | const set = this.mapPathToWatchers.get(subpath); |
| | | const set = /** @type {WatcherSet} */ (this.mapPathToWatchers.get(subpath)); |
| | | set.delete(watcher); |
| | | if (set.size === 0) { |
| | | this.mapPathToWatchers.delete(subpath); |
| | |
| | | if (this.watcher) this.watcher.close(); |
| | | if (recursiveWatcherLogging) { |
| | | process.stderr.write( |
| | | `[watchpack] closed recursive watcher at ${this.rootPath}\n` |
| | | `[watchpack] closed recursive watcher at ${this.rootPath}\n`, |
| | | ); |
| | | } |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * @typedef {object} WatcherEvents |
| | | * @property {(eventType: EventType, filename?: string) => void} change change event |
| | | * @property {(err: unknown) => void} error error event |
| | | */ |
| | | |
| | | /** |
| | | * @extends {EventEmitter<{ [K in keyof WatcherEvents]: Parameters<WatcherEvents[K]> }>} |
| | | */ |
| | | class Watcher extends EventEmitter { |
| | | constructor() { |
| | | super(); |
| | | } |
| | | |
| | | close() { |
| | | if (pendingWatchers.has(this)) { |
| | | pendingWatchers.delete(this); |
| | | return; |
| | | } |
| | | const watcher = underlyingWatcher.get(this); |
| | | watcher.remove(this); |
| | | /** @type {RecursiveWatcher | DirectWatcher} */ |
| | | (watcher).remove(this); |
| | | underlyingWatcher.delete(this); |
| | | } |
| | | } |
| | | |
| | | const createDirectWatcher = filePath => { |
| | | /** |
| | | * @param {string} filePath a file path |
| | | * @returns {DirectWatcher} a directory watcher |
| | | */ |
| | | const createDirectWatcher = (filePath) => { |
| | | const existing = directWatchers.get(filePath); |
| | | if (existing !== undefined) return existing; |
| | | const w = new DirectWatcher(filePath); |
| | |
| | | return w; |
| | | }; |
| | | |
| | | const createRecursiveWatcher = rootPath => { |
| | | /** |
| | | * @param {string} rootPath a root path |
| | | * @returns {RecursiveWatcher} a recursive watcher |
| | | */ |
| | | const createRecursiveWatcher = (rootPath) => { |
| | | const existing = recursiveWatchers.get(rootPath); |
| | | if (existing !== undefined) return existing; |
| | | const w = new RecursiveWatcher(rootPath); |
| | |
| | | const execute = () => { |
| | | /** @type {Map<string, Watcher[] | Watcher>} */ |
| | | const map = new Map(); |
| | | /** |
| | | * @param {Watcher} watcher a watcher |
| | | * @param {string} filePath a file path |
| | | */ |
| | | const addWatcher = (watcher, filePath) => { |
| | | const entry = map.get(filePath); |
| | | if (entry === undefined) { |
| | |
| | | } |
| | | }; |
| | | |
| | | exports.watch = filePath => { |
| | | module.exports.Watcher = Watcher; |
| | | |
| | | /** |
| | | * @param {() => void} fn a function |
| | | */ |
| | | module.exports.batch = (fn) => { |
| | | isBatch = true; |
| | | try { |
| | | fn(); |
| | | } finally { |
| | | isBatch = false; |
| | | execute(); |
| | | } |
| | | }; |
| | | |
| | | module.exports.createHandleChangeEvent = createHandleChangeEvent; |
| | | |
| | | module.exports.getNumberOfWatchers = () => watcherCount; |
| | | |
| | | /** |
| | | * @param {string} filePath a file path |
| | | * @returns {Watcher} watcher |
| | | */ |
| | | module.exports.watch = (filePath) => { |
| | | const watcher = new Watcher(); |
| | | // Find an existing watcher |
| | | const directWatcher = directWatchers.get(filePath); |
| | |
| | | return watcher; |
| | | }; |
| | | |
| | | exports.batch = fn => { |
| | | isBatch = true; |
| | | try { |
| | | fn(); |
| | | } finally { |
| | | isBatch = false; |
| | | execute(); |
| | | } |
| | | }; |
| | | |
| | | exports.getNumberOfWatchers = () => { |
| | | return watcherCount; |
| | | }; |
| | | |
| | | exports.createHandleChangeEvent = createHandleChangeEvent; |
| | | exports.watcherLimit = watcherLimit; |
| | | module.exports.watcherLimit = watcherLimit; |