diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..cce0279 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +package.json +package-lock.json diff --git a/lib/DirectoryWatcher.js b/lib/DirectoryWatcher.js index 6512e32..75b2e33 100644 --- a/lib/DirectoryWatcher.js +++ b/lib/DirectoryWatcher.js @@ -10,6 +10,17 @@ const path = require("path"); const watchEventSource = require("./watchEventSource"); +/** @typedef {import("./index").NormalizedWatchOptions} NormalizedWatchOptions */ +/** @typedef {import("./index").EventType} EventType */ +/** @typedef {import("./index").TimeInfoEntries} TimeInfoEntries */ +/** @typedef {import("./index").Entry} Entry */ +/** @typedef {import("./index").ExistanceOnlyTimeEntry} ExistanceOnlyTimeEntry */ +/** @typedef {import("./index").OnlySafeTimeEntry} OnlySafeTimeEntry */ +/** @typedef {import("./index").EventMap} EventMap */ +/** @typedef {import("./getWatcherManager").WatcherManager} WatcherManager */ +/** @typedef {import("./watchEventSource").Watcher} EventSourceWatcher */ + +/** @type {ExistanceOnlyTimeEntry} */ const EXISTANCE_ONLY_TIME_ENTRY = Object.freeze({}); let FS_ACCURACY = 2000; @@ -19,14 +30,24 @@ const IS_WIN = require("os").platform() === "win32"; const { WATCHPACK_POLLING } = process.env; const FORCE_POLLING = + // @ts-expect-error avoid additional checks `${+WATCHPACK_POLLING}` === WATCHPACK_POLLING ? +WATCHPACK_POLLING : Boolean(WATCHPACK_POLLING) && WATCHPACK_POLLING !== "false"; +/** + * @param {string} str string + * @returns {string} lower cased string + */ function withoutCase(str) { return str.toLowerCase(); } +/** + * @param {number} times times + * @param {() => void} callback callback + * @returns {() => void} result + */ function needCalls(times, callback) { return function needCallsCallback() { if (--times === 0) { @@ -35,6 +56,9 @@ function needCalls(times, callback) { }; } +/** + * @param {Entry} entry entry + */ function fixupEntryAccuracy(entry) { if (entry.accuracy > FS_ACCURACY) { entry.safeTime = entry.safeTime - entry.accuracy + FS_ACCURACY; @@ -42,6 +66,9 @@ function fixupEntryAccuracy(entry) { } } +/** + * @param {number=} mtime mtime + */ function ensureFsAccuracy(mtime) { if (!mtime) return; if (FS_ACCURACY > 1 && mtime % 1 !== 0) FS_ACCURACY = 1; @@ -50,14 +77,44 @@ function ensureFsAccuracy(mtime) { else if (FS_ACCURACY > 1000 && mtime % 1000 !== 0) FS_ACCURACY = 1000; } +/** + * @typedef {object} FileWatcherEvents + * @property {(type: EventType) => void} initial-missing initial missing event + * @property {(mtime: number, type: EventType, initial: boolean) => void} change change event + * @property {(type: EventType) => void} remove remove event + * @property {() => void} closed closed event + */ + +/** + * @typedef {object} DirectoryWatcherEvents + * @property {(type: EventType) => void} initial-missing initial missing event + * @property {((file: string, mtime: number, type: EventType, initial: boolean) => void)} change change event + * @property {(type: EventType) => void} remove remove event + * @property {() => void} closed closed event + */ + +/** + * @template {EventMap} T + * @extends {EventEmitter<{ [K in keyof T]: Parameters }>} + */ class Watcher extends EventEmitter { - constructor(directoryWatcher, filePath, startTime) { + /** + * @param {DirectoryWatcher} directoryWatcher a directory watcher + * @param {string} target a target to watch + * @param {number=} startTime start time + */ + constructor(directoryWatcher, target, startTime) { super(); this.directoryWatcher = directoryWatcher; - this.path = filePath; + this.path = target; this.startTime = startTime && +startTime; } + /** + * @param {number} mtime mtime + * @param {boolean} initial true when initial, otherwise false + * @returns {boolean} true of start time less than mtile, otherwise false + */ checkStartTime(mtime, initial) { const { startTime } = this; if (typeof startTime !== "number") return !initial; @@ -65,11 +122,28 @@ class Watcher extends EventEmitter { } close() { + // @ts-expect-error bad typing in EventEmitter this.emit("closed"); } } +/** @typedef {Set} InitialScanRemoved */ + +/** + * @typedef {object} WatchpackEvents + * @property {(target: string, mtime: string, type: EventType, initial: boolean) => void} change change event + * @property {() => void} closed closed event + */ + +/** + * @extends {EventEmitter<{ [K in keyof WatchpackEvents]: Parameters }>} + */ class DirectoryWatcher extends EventEmitter { + /** + * @param {WatcherManager} watcherManager a watcher manager + * @param {string} directoryPath directory path + * @param {NormalizedWatchOptions} options options + */ constructor(watcherManager, directoryPath, options) { super(); if (FORCE_POLLING) { @@ -80,28 +154,35 @@ class DirectoryWatcher extends EventEmitter { this.path = directoryPath; // safeTime is the point in time after which reading is safe to be unchanged // timestamp is a value that should be compared with another timestamp (mtime) - /** @type {Map} */ this.files = new Map(); /** @type {Map} */ this.filesWithoutCase = new Map(); + /** @type {Map | boolean>} */ this.directories = new Map(); this.lastWatchEvent = 0; this.initialScan = true; this.ignored = options.ignored || (() => false); this.nestedWatching = false; + /** @type {number | false} */ this.polledWatching = typeof options.poll === "number" ? options.poll : options.poll ? 5007 : false; + /** @type {undefined | NodeJS.Timeout} */ this.timeout = undefined; + /** @type {null | InitialScanRemoved} */ this.initialScanRemoved = new Set(); + /** @type {undefined | number} */ this.initialScanFinished = undefined; - /** @type {Map>} */ + /** @type {Map | Watcher>>} */ this.watchers = new Map(); + /** @type {Watcher | null} */ this.parentWatcher = null; this.refs = 0; + /** @type {Map} */ this._activeEvents = new Map(); this.closed = false; this.scanning = false; @@ -115,19 +196,22 @@ class DirectoryWatcher extends EventEmitter { createWatcher() { try { if (this.polledWatching) { - this.watcher = { + /** @type {EventSourceWatcher} */ + (this.watcher) = /** @type {EventSourceWatcher} */ ({ close: () => { if (this.timeout) { clearTimeout(this.timeout); this.timeout = undefined; } }, - }; + }); } else { if (IS_OSX) { this.watchInParentDirectory(); } - this.watcher = watchEventSource.watch(this.path); + this.watcher = + /** @type {EventSourceWatcher} */ + (watchEventSource.watch(this.path)); this.watcher.on("change", this.onWatchEvent.bind(this)); this.watcher.on("error", this.onWatcherError.bind(this)); } @@ -136,6 +220,11 @@ class DirectoryWatcher extends EventEmitter { } } + /** + * @template {(watcher: Watcher) => void} T + * @param {string} path path + * @param {T} fn function + */ forEachWatcher(path, fn) { const watchers = this.watchers.get(withoutCase(path)); if (watchers !== undefined) { @@ -145,16 +234,24 @@ class DirectoryWatcher extends EventEmitter { } } + /** + * @param {string} itemPath an item path + * @param {boolean} initial true when initial, otherwise false + * @param {EventType} type even type + */ setMissing(itemPath, initial, type) { if (this.initialScan) { - this.initialScanRemoved.add(itemPath); + /** @type {InitialScanRemoved} */ + (this.initialScanRemoved).add(itemPath); } const oldDirectory = this.directories.get(itemPath); if (oldDirectory) { - if (this.nestedWatching) oldDirectory.close(); + if (this.nestedWatching) { + /** @type {Watcher} */ + (oldDirectory).close(); + } this.directories.delete(itemPath); - this.forEachWatcher(itemPath, (w) => w.emit("remove", type)); if (!initial) { this.forEachWatcher(this.path, (w) => @@ -167,7 +264,7 @@ class DirectoryWatcher extends EventEmitter { if (oldFile) { this.files.delete(itemPath); const key = withoutCase(itemPath); - const count = this.filesWithoutCase.get(key) - 1; + const count = /** @type {number} */ (this.filesWithoutCase.get(key)) - 1; if (count <= 0) { this.filesWithoutCase.delete(key); this.forEachWatcher(itemPath, (w) => w.emit("remove", type)); @@ -183,12 +280,19 @@ class DirectoryWatcher extends EventEmitter { } } - setFileTime(filePath, mtime, initial, ignoreWhenEqual, type) { + /** + * @param {string} target a target to set file time + * @param {number} mtime mtime + * @param {boolean} initial true when initial, otherwise false + * @param {boolean} ignoreWhenEqual true to ignore when equal, otherwise false + * @param {EventType} type type + */ + setFileTime(target, mtime, initial, ignoreWhenEqual, type) { const now = Date.now(); - if (this.ignored(filePath)) return; + if (this.ignored(target)) return; - const old = this.files.get(filePath); + const old = this.files.get(target); let safeTime; let accuracy; @@ -210,14 +314,14 @@ class DirectoryWatcher extends EventEmitter { if (ignoreWhenEqual && old && old.timestamp === mtime) return; - this.files.set(filePath, { + this.files.set(target, { safeTime, accuracy, timestamp: mtime, }); if (!old) { - const key = withoutCase(filePath); + const key = withoutCase(target); const count = this.filesWithoutCase.get(key); this.filesWithoutCase.set(key, (count || 0) + 1); if (count !== undefined) { @@ -229,21 +333,27 @@ class DirectoryWatcher extends EventEmitter { this.doScan(false); } - this.forEachWatcher(filePath, (w) => { + this.forEachWatcher(target, (w) => { if (!initial || w.checkStartTime(safeTime, initial)) { w.emit("change", mtime, type); } }); } else if (!initial) { - this.forEachWatcher(filePath, (w) => w.emit("change", mtime, type)); + this.forEachWatcher(target, (w) => w.emit("change", mtime, type)); } this.forEachWatcher(this.path, (w) => { if (!initial || w.checkStartTime(safeTime, initial)) { - w.emit("change", filePath, safeTime, type, initial); + w.emit("change", target, safeTime, type, initial); } }); } + /** + * @param {string} directoryPath directory path + * @param {number} birthtime birthtime + * @param {boolean} initial true when initial, otherwise false + * @param {EventType} type even type + */ setDirectory(directoryPath, birthtime, initial, type) { if (this.ignored(directoryPath)) return; if (directoryPath === this.path) { @@ -279,18 +389,24 @@ class DirectoryWatcher extends EventEmitter { } } + /** + * @param {string} directoryPath directory path + */ createNestedWatcher(directoryPath) { const watcher = this.watcherManager.watchDirectory(directoryPath, 1); - watcher.on("change", (filePath, mtime, type, initial) => { + watcher.on("change", (target, mtime, type, initial) => { this.forEachWatcher(this.path, (w) => { if (!initial || w.checkStartTime(mtime, initial)) { - w.emit("change", filePath, mtime, type, initial); + w.emit("change", target, mtime, type, initial); } }); }); this.directories.set(directoryPath, watcher); } + /** + * @param {boolean} flag true when nested, otherwise false + */ setNestedWatching(flag) { if (this.nestedWatching !== Boolean(flag)) { this.nestedWatching = Boolean(flag); @@ -300,22 +416,30 @@ class DirectoryWatcher extends EventEmitter { } } else { for (const [directory, watcher] of this.directories) { - watcher.close(); + /** @type {Watcher} */ + (watcher).close(); this.directories.set(directory, true); } } } } - watch(filePath, startTime) { - const key = withoutCase(filePath); + /** + * @param {string} target a target to watch + * @param {number=} startTime start time + * @returns {Watcher | Watcher} watcher + */ + watch(target, startTime) { + const key = withoutCase(target); let watchers = this.watchers.get(key); if (watchers === undefined) { watchers = new Set(); this.watchers.set(key, watchers); } this.refs++; - const watcher = new Watcher(this, filePath, startTime); + const watcher = + /** @type {Watcher | Watcher} */ + (new Watcher(this, target, startTime)); watcher.on("closed", () => { if (--this.refs <= 0) { this.close(); @@ -324,12 +448,12 @@ class DirectoryWatcher extends EventEmitter { watchers.delete(watcher); if (watchers.size === 0) { this.watchers.delete(key); - if (this.path === filePath) this.setNestedWatching(false); + if (this.path === target) this.setNestedWatching(false); } }); watchers.add(watcher); let safeTime; - if (filePath === this.path) { + if (target === this.path) { this.setNestedWatching(true); safeTime = this.lastWatchEvent; for (const entry of this.files.values()) { @@ -337,7 +461,7 @@ class DirectoryWatcher extends EventEmitter { safeTime = Math.max(safeTime, entry.safeTime); } } else { - const entry = this.files.get(filePath); + const entry = this.files.get(target); if (entry) { fixupEntryAccuracy(entry); safeTime = entry.safeTime; @@ -346,19 +470,21 @@ class DirectoryWatcher extends EventEmitter { } } if (safeTime) { - if (safeTime >= startTime) { + if (startTime && safeTime >= startTime) { process.nextTick(() => { if (this.closed) return; - if (filePath === this.path) { - watcher.emit( + if (target === this.path) { + /** @type {Watcher} */ + (watcher).emit( "change", - filePath, + target, safeTime, "watch (outdated on attach)", true, ); } else { - watcher.emit( + /** @type {Watcher} */ + (watcher).emit( "change", safeTime, "watch (outdated on attach)", @@ -368,16 +494,23 @@ class DirectoryWatcher extends EventEmitter { }); } } else if (this.initialScan) { - if (this.initialScanRemoved.has(filePath)) { + if ( + /** @type {InitialScanRemoved} */ + (this.initialScanRemoved).has(target) + ) { process.nextTick(() => { if (this.closed) return; watcher.emit("remove"); }); } } else if ( - filePath !== this.path && - !this.directories.has(filePath) && - watcher.checkStartTime(this.initialScanFinished, false) + target !== this.path && + !this.directories.has(target) && + watcher.checkStartTime( + /** @type {number} */ + (this.initialScanFinished), + false, + ) ) { process.nextTick(() => { if (this.closed) return; @@ -387,6 +520,10 @@ class DirectoryWatcher extends EventEmitter { return watcher; } + /** + * @param {EventType} eventType event type + * @param {string=} filename filename + */ onWatchEvent(eventType, filename) { if (this.closed) return; if (!filename) { @@ -398,15 +535,15 @@ class DirectoryWatcher extends EventEmitter { return; } - const filePath = path.join(this.path, filename); - if (this.ignored(filePath)) return; + const target = path.join(this.path, filename); + if (this.ignored(target)) return; if (this._activeEvents.get(filename) === undefined) { this._activeEvents.set(filename, false); const checkStats = () => { if (this.closed) return; this._activeEvents.set(filename, false); - fs.lstat(filePath, (err, stats) => { + fs.lstat(target, (err, stats) => { if (this.closed) return; if (this._activeEvents.get(filename) === true) { process.nextTick(checkStats); @@ -431,20 +568,15 @@ class DirectoryWatcher extends EventEmitter { } this.lastWatchEvent = Date.now(); if (!stats) { - this.setMissing(filePath, false, eventType); + this.setMissing(target, false, eventType); } else if (stats.isDirectory()) { - this.setDirectory( - filePath, - +stats.birthtime || 1, - false, - eventType, - ); + this.setDirectory(target, +stats.birthtime || 1, false, eventType); } else if (stats.isFile() || stats.isSymbolicLink()) { if (stats.mtime) { - ensureFsAccuracy(stats.mtime); + ensureFsAccuracy(+stats.mtime); } this.setFileTime( - filePath, + target, +stats.mtime || +stats.ctime || 1, false, false, @@ -459,10 +591,18 @@ class DirectoryWatcher extends EventEmitter { } } + /** + * @param {unknown=} err error + */ onWatcherError(err) { if (this.closed) return; if (err) { - if (err.code !== "EPERM" && err.code !== "ENOENT") { + if ( + /** @type {NodeJS.ErrnoException} */ + (err).code !== "EPERM" && + /** @type {NodeJS.ErrnoException} */ + (err).code !== "ENOENT" + ) { // eslint-disable-next-line no-console console.error(`Watchpack Error (watcher): ${err}`); } @@ -470,6 +610,9 @@ class DirectoryWatcher extends EventEmitter { } } + /** + * @param {Error | NodeJS.ErrnoException=} err error + */ onStatsError(err) { if (err) { // eslint-disable-next-line no-console @@ -477,6 +620,9 @@ class DirectoryWatcher extends EventEmitter { } } + /** + * @param {Error | NodeJS.ErrnoException=} err error + */ onScanError(err) { if (err) { // eslint-disable-next-line no-console @@ -494,18 +640,21 @@ class DirectoryWatcher extends EventEmitter { } } + /** + * @param {string} reason a reason + */ onDirectoryRemoved(reason) { if (this.watcher) { this.watcher.close(); this.watcher = null; } this.watchInParentDirectory(); - const type = `directory-removed (${reason})`; + const type = /** @type {EventType} */ (`directory-removed (${reason})`); for (const directory of this.directories.keys()) { - this.setMissing(directory, null, type); + this.setMissing(directory, false, type); } for (const file of this.files.keys()) { - this.setMissing(file, null, type); + this.setMissing(file, false, type); } } @@ -517,7 +666,8 @@ class DirectoryWatcher extends EventEmitter { if (path.dirname(parentDir) === parentDir) return; this.parentWatcher = this.watcherManager.watchFile(this.path, 1); - this.parentWatcher.on("change", (mtime, type) => { + /** @type {Watcher} */ + (this.parentWatcher).on("change", (mtime, type) => { if (this.closed) return; // On non-osx platforms we don't need this watcher to detect @@ -537,12 +687,16 @@ class DirectoryWatcher extends EventEmitter { ); } }); - this.parentWatcher.on("remove", () => { + /** @type {Watcher} */ + (this.parentWatcher).on("remove", () => { this.onDirectoryRemoved("parent directory removed"); }); } } + /** + * @param {boolean} initial true when initial, otherwise false + */ doScan(initial) { if (this.scanning) { if (this.scanAgain) { @@ -660,7 +814,7 @@ class DirectoryWatcher extends EventEmitter { } if (stats.isFile() || stats.isSymbolicLink()) { if (stats.mtime) { - ensureFsAccuracy(stats.mtime); + ensureFsAccuracy(+stats.mtime); } this.setFileTime( itemPath, @@ -688,6 +842,9 @@ class DirectoryWatcher extends EventEmitter { }); } + /** + * @returns {Record} times + */ getTimes() { const obj = Object.create(null); let safeTime = this.lastWatchEvent; @@ -698,7 +855,9 @@ class DirectoryWatcher extends EventEmitter { } if (this.nestedWatching) { for (const w of this.directories.values()) { - const times = w.directoryWatcher.getTimes(); + const times = + /** @type {Watcher} */ + (w).directoryWatcher.getTimes(); for (const file of Object.keys(times)) { const time = times[file]; safeTime = Math.max(safeTime, time); @@ -720,6 +879,11 @@ class DirectoryWatcher extends EventEmitter { return obj; } + /** + * @param {TimeInfoEntries} fileTimestamps file timestamps + * @param {TimeInfoEntries} directoryTimestamps directory timestamps + * @returns {number} safe time + */ collectTimeInfoEntries(fileTimestamps, directoryTimestamps) { let safeTime = this.lastWatchEvent; for (const [file, entry] of this.files) { @@ -731,7 +895,8 @@ class DirectoryWatcher extends EventEmitter { for (const w of this.directories.values()) { safeTime = Math.max( safeTime, - w.directoryWatcher.collectTimeInfoEntries( + /** @type {Watcher} */ + (w).directoryWatcher.collectTimeInfoEntries( fileTimestamps, directoryTimestamps, ), @@ -775,7 +940,8 @@ class DirectoryWatcher extends EventEmitter { } if (this.nestedWatching) { for (const w of this.directories.values()) { - w.close(); + /** @type {Watcher} */ + (w).close(); } this.directories.clear(); } @@ -788,4 +954,5 @@ class DirectoryWatcher extends EventEmitter { } module.exports = DirectoryWatcher; +module.exports.Watcher = Watcher; module.exports.EXISTANCE_ONLY_TIME_ENTRY = EXISTANCE_ONLY_TIME_ENTRY; diff --git a/lib/LinkResolver.js b/lib/LinkResolver.js index 52fb7d6..8133cbd 100644 --- a/lib/LinkResolver.js +++ b/lib/LinkResolver.js @@ -15,12 +15,13 @@ if (process.platform === "win32") EXPECTED_ERRORS.add("UNKNOWN"); class LinkResolver { constructor() { + /** @type {Map} */ this.cache = new Map(); } /** * @param {string} file path to file or directory - * @returns {string[]} array of file and all symlinks contributed in the resolving process (first item is the resolved file) + * @returns {readonly string[]} array of file and all symlinks contributed in the resolving process (first item is the resolved file) */ resolve(file) { const cacheEntry = this.cache.get(file); @@ -93,7 +94,11 @@ class LinkResolver { this.cache.set(file, result); return result; } catch (err) { - if (!EXPECTED_ERRORS.has(err.code)) { + if ( + /** @type {NodeJS.ErrnoException} */ + (err).code && + !EXPECTED_ERRORS.has(/** @type {NodeJS.ErrnoException} */ (err).code) + ) { throw err; } // no link diff --git a/lib/getWatcherManager.js b/lib/getWatcherManager.js index 5616ab5..dc0e4af 100644 --- a/lib/getWatcherManager.js +++ b/lib/getWatcherManager.js @@ -7,12 +7,29 @@ const path = require("path"); const DirectoryWatcher = require("./DirectoryWatcher"); +/** @typedef {import("./index").NormalizedWatchOptions} NormalizedWatchOptions */ +/** @typedef {import("./index").EventMap} EventMap */ +/** @typedef {import("./DirectoryWatcher").DirectoryWatcherEvents} DirectoryWatcherEvents */ +/** @typedef {import("./DirectoryWatcher").FileWatcherEvents} FileWatcherEvents */ +/** + * @template {EventMap} T + * @typedef {import("./DirectoryWatcher").Watcher} Watcher + */ + class WatcherManager { + /** + * @param {NormalizedWatchOptions} options options + */ constructor(options) { this.options = options; + /** @type {Map} */ this.directoryWatchers = new Map(); } + /** + * @param {string} directory a directory + * @returns {DirectoryWatcher} a directory watcher + */ getDirectoryWatcher(directory) { const watcher = this.directoryWatchers.get(directory); if (watcher === undefined) { @@ -26,12 +43,22 @@ class WatcherManager { return watcher; } + /** + * @param {string} file file + * @param {number=} startTime start time + * @returns {Watcher | null} watcher or null if file has no directory + */ watchFile(file, startTime) { const directory = path.dirname(file); if (directory === file) return null; return this.getDirectoryWatcher(directory).watch(file, startTime); } + /** + * @param {string} directory directory + * @param {number=} startTime start time + * @returns {Watcher} watcher + */ watchDirectory(directory, startTime) { return this.getDirectoryWatcher(directory).watch(directory, startTime); } @@ -40,7 +67,7 @@ class WatcherManager { const watcherManagers = new WeakMap(); /** - * @param {object} options options + * @param {NormalizedWatchOptions} options options * @returns {WatcherManager} the watcher manager */ module.exports = (options) => { diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..181df63 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,553 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ +"use strict"; + +const getWatcherManager = require("./getWatcherManager"); +const LinkResolver = require("./LinkResolver"); +const { EventEmitter } = require("events"); +const globToRegExp = require("glob-to-regexp"); +const watchEventSource = require("./watchEventSource"); + +/** @typedef {import("./getWatcherManager").WatcherManager} WatcherManager */ +/** @typedef {import("./DirectoryWatcher")} DirectoryWatcher */ +/** @typedef {import("./DirectoryWatcher").DirectoryWatcherEvents} DirectoryWatcherEvents */ +/** @typedef {import("./DirectoryWatcher").FileWatcherEvents} FileWatcherEvents */ + +// eslint-disable-next-line jsdoc/no-restricted-syntax +/** @typedef {Record any>} EventMap */ + +/** + * @template {EventMap} T + * @typedef {import("./DirectoryWatcher").Watcher} Watcher + */ + +/** @typedef {(item: string) => boolean} IgnoredFunction */ +/** @typedef {string[] | RegExp | string | IgnoredFunction} Ignored */ + +/** + * @typedef {object} WatcherOptions + * @property {boolean=} followSymlinks true when need to resolve symlinks and watch symlink and real file, otherwise false + * @property {Ignored=} ignored ignore some files from watching (glob pattern or regexp) + * @property {number | boolean=} poll true when need to enable polling mode for watching, otherwise false + */ + +/** @typedef {WatcherOptions & { aggregateTimeout?: number }} WatchOptions */ + +/** + * @typedef {object} NormalizedWatchOptions + * @property {boolean} followSymlinks true when need to resolve symlinks and watch symlink and real file, otherwise false + * @property {IgnoredFunction} ignored ignore some files from watching (glob pattern or regexp) + * @property {number | boolean=} poll true when need to enable polling mode for watching, otherwise false + */ + +/** @typedef {`scan (${string})` | "change" | "rename" | `watch ${string}` | `directory-removed ${string}`} EventType */ +/** @typedef {{ safeTime: number, timestamp: number, accuracy: number }} Entry */ +/** @typedef {{ safeTime: number }} OnlySafeTimeEntry */ +/** @typedef {{}} ExistanceOnlyTimeEntry */ +/** @typedef {Map} TimeInfoEntries */ +/** @typedef {Set} Changes */ +/** @typedef {Set} Removals */ +/** @typedef {{ changes: Changes, removals: Removals }} Aggregated */ +/** @typedef {{ files?: Iterable, directories?: Iterable, missing?: Iterable, startTime?: number }} WatchMethodOptions */ +/** @typedef {Record} Times */ + +/** + * @param {MapIterator | MapIterator} watchers watchers + * @param {Set} set set + */ +function addWatchersToSet(watchers, set) { + for (const ww of watchers) { + const w = ww.watcher; + if (!set.has(w.directoryWatcher)) { + set.add(w.directoryWatcher); + } + } +} + +/** + * @param {string} ignored ignored + * @returns {string | undefined} resolved global to regexp + */ +const stringToRegexp = (ignored) => { + if (ignored.length === 0) { + return; + } + const { source } = globToRegExp(ignored, { globstar: true, extended: true }); + return `${source.slice(0, -1)}(?:$|\\/)`; +}; + +/** + * @param {Ignored=} ignored ignored + * @returns {(item: string) => boolean} ignored to function + */ +const ignoredToFunction = (ignored) => { + if (Array.isArray(ignored)) { + const stringRegexps = ignored.map((i) => stringToRegexp(i)).filter(Boolean); + if (stringRegexps.length === 0) { + return () => false; + } + const regexp = new RegExp(stringRegexps.join("|")); + return (item) => regexp.test(item.replace(/\\/g, "/")); + } else if (typeof ignored === "string") { + const stringRegexp = stringToRegexp(ignored); + if (!stringRegexp) { + return () => false; + } + const regexp = new RegExp(stringRegexp); + return (item) => regexp.test(item.replace(/\\/g, "/")); + } else if (ignored instanceof RegExp) { + return (item) => ignored.test(item.replace(/\\/g, "/")); + } else if (typeof ignored === "function") { + return ignored; + } else if (ignored) { + throw new Error(`Invalid option for 'ignored': ${ignored}`); + } else { + return () => false; + } +}; + +/** + * @param {WatchOptions} options options + * @returns {NormalizedWatchOptions} normalized options + */ +const normalizeOptions = (options) => ({ + followSymlinks: Boolean(options.followSymlinks), + ignored: ignoredToFunction(options.ignored), + poll: options.poll, +}); + +const normalizeCache = new WeakMap(); +/** + * @param {WatchOptions} options options + * @returns {NormalizedWatchOptions} normalized options + */ +const cachedNormalizeOptions = (options) => { + const cacheEntry = normalizeCache.get(options); + if (cacheEntry !== undefined) return cacheEntry; + const normalized = normalizeOptions(options); + normalizeCache.set(options, normalized); + return normalized; +}; + +class WatchpackFileWatcher { + /** + * @param {Watchpack} watchpack watchpack + * @param {Watcher} watcher watcher + * @param {string | string[]} files files + */ + constructor(watchpack, watcher, files) { + /** @type {string[]} */ + this.files = Array.isArray(files) ? files : [files]; + this.watcher = watcher; + watcher.on("initial-missing", (type) => { + for (const file of this.files) { + if (!watchpack._missing.has(file)) { + watchpack._onRemove(file, file, type); + } + } + }); + watcher.on("change", (mtime, type, _initial) => { + for (const file of this.files) { + watchpack._onChange(file, mtime, file, type); + } + }); + watcher.on("remove", (type) => { + for (const file of this.files) { + watchpack._onRemove(file, file, type); + } + }); + } + + /** + * @param {string | string[]} files files + */ + update(files) { + if (!Array.isArray(files)) { + if (this.files.length !== 1) { + this.files = [files]; + } else if (this.files[0] !== files) { + this.files[0] = files; + } + } else { + this.files = files; + } + } + + close() { + this.watcher.close(); + } +} + +class WatchpackDirectoryWatcher { + /** + * @param {Watchpack} watchpack watchpack + * @param {Watcher} watcher watcher + * @param {string} directories directories + */ + constructor(watchpack, watcher, directories) { + /** @type {string[]} */ + this.directories = Array.isArray(directories) ? directories : [directories]; + this.watcher = watcher; + watcher.on("initial-missing", (type) => { + for (const item of this.directories) { + watchpack._onRemove(item, item, type); + } + }); + watcher.on("change", (file, mtime, type, _initial) => { + for (const item of this.directories) { + watchpack._onChange(item, mtime, file, type); + } + }); + watcher.on("remove", (type) => { + for (const item of this.directories) { + watchpack._onRemove(item, item, type); + } + }); + } + + /** + * @param {string | string[]} directories directories + */ + update(directories) { + if (!Array.isArray(directories)) { + if (this.directories.length !== 1) { + this.directories = [directories]; + } else if (this.directories[0] !== directories) { + this.directories[0] = directories; + } + } else { + this.directories = directories; + } + } + + close() { + this.watcher.close(); + } +} + +/** + * @typedef {object} WatchpackEvents + * @property {(file: string, mtime: number, type: EventType) => void} change change event + * @property {(file: string, type: EventType) => void} remove remove event + * @property {(changes: Changes, removals: Removals) => void} aggregated aggregated event + */ + +/** + * @extends {EventEmitter<{ [K in keyof WatchpackEvents]: Parameters }>} + */ +class Watchpack extends EventEmitter { + /** + * @param {WatchOptions} options options + */ + constructor(options) { + super(); + if (!options) options = {}; + /** @type {WatchOptions} */ + this.options = options; + this.aggregateTimeout = + typeof options.aggregateTimeout === "number" + ? options.aggregateTimeout + : 200; + /** @type {NormalizedWatchOptions} */ + this.watcherOptions = cachedNormalizeOptions(options); + /** @type {WatcherManager} */ + this.watcherManager = getWatcherManager(this.watcherOptions); + /** @type {Map} */ + this.fileWatchers = new Map(); + /** @type {Map} */ + this.directoryWatchers = new Map(); + /** @type {Set} */ + this._missing = new Set(); + this.startTime = undefined; + this.paused = false; + /** @type {Changes} */ + this.aggregatedChanges = new Set(); + /** @type {Removals} */ + this.aggregatedRemovals = new Set(); + /** @type {undefined | NodeJS.Timeout} */ + this.aggregateTimer = undefined; + this._onTimeout = this._onTimeout.bind(this); + } + + /** + * @overload + * @param {Iterable} arg1 files + * @param {Iterable} arg2 directories + * @param {number=} arg3 startTime + * @returns {void} + */ + /** + * @overload + * @param {WatchMethodOptions} arg1 watch options + * @returns {void} + */ + /** + * @param {Iterable | WatchMethodOptions} arg1 files + * @param {Iterable=} arg2 directories + * @param {number=} arg3 startTime + * @returns {void} + */ + watch(arg1, arg2, arg3) { + /** @type {Iterable | undefined} */ + let files; + /** @type {Iterable | undefined} */ + let directories; + /** @type {Iterable | undefined} */ + let missing; + /** @type {number | undefined} */ + let startTime; + if (!arg2) { + ({ + files = [], + directories = [], + missing = [], + startTime, + } = /** @type {WatchMethodOptions} */ (arg1)); + } else { + files = /** @type {Iterable} */ (arg1); + directories = /** @type {Iterable} */ (arg2); + missing = []; + startTime = /** @type {number} */ (arg3); + } + this.paused = false; + const { fileWatchers, directoryWatchers } = this; + const { ignored } = this.watcherOptions; + /** + * @param {string} path path + * @returns {boolean} true when need to filter, otherwise false + */ + const filter = (path) => !ignored(path); + /** + * @template K, V + * @param {Map} map map + * @param {K} key key + * @param {V} item item + */ + const addToMap = (map, key, item) => { + const list = map.get(key); + if (list === undefined) { + map.set(key, item); + } else if (Array.isArray(list)) { + list.push(item); + } else { + map.set(key, [list, item]); + } + }; + const fileWatchersNeeded = new Map(); + const directoryWatchersNeeded = new Map(); + /** @type {Set} */ + const missingFiles = new Set(); + if (this.watcherOptions.followSymlinks) { + const resolver = new LinkResolver(); + for (const file of files) { + if (filter(file)) { + for (const innerFile of resolver.resolve(file)) { + if (file === innerFile || filter(innerFile)) { + addToMap(fileWatchersNeeded, innerFile, file); + } + } + } + } + for (const file of missing) { + if (filter(file)) { + for (const innerFile of resolver.resolve(file)) { + if (file === innerFile || filter(innerFile)) { + missingFiles.add(file); + addToMap(fileWatchersNeeded, innerFile, file); + } + } + } + } + for (const dir of directories) { + if (filter(dir)) { + let first = true; + for (const innerItem of resolver.resolve(dir)) { + if (filter(innerItem)) { + addToMap( + first ? directoryWatchersNeeded : fileWatchersNeeded, + innerItem, + dir, + ); + } + first = false; + } + } + } + } else { + for (const file of files) { + if (filter(file)) { + addToMap(fileWatchersNeeded, file, file); + } + } + for (const file of missing) { + if (filter(file)) { + missingFiles.add(file); + addToMap(fileWatchersNeeded, file, file); + } + } + for (const dir of directories) { + if (filter(dir)) { + addToMap(directoryWatchersNeeded, dir, dir); + } + } + } + // Close unneeded old watchers + // and update existing watchers + for (const [key, w] of fileWatchers) { + const needed = fileWatchersNeeded.get(key); + if (needed === undefined) { + w.close(); + fileWatchers.delete(key); + } else { + w.update(needed); + fileWatchersNeeded.delete(key); + } + } + for (const [key, w] of directoryWatchers) { + const needed = directoryWatchersNeeded.get(key); + if (needed === undefined) { + w.close(); + directoryWatchers.delete(key); + } else { + w.update(needed); + directoryWatchersNeeded.delete(key); + } + } + // Create new watchers and install handlers on these watchers + watchEventSource.batch(() => { + for (const [key, files] of fileWatchersNeeded) { + const watcher = this.watcherManager.watchFile(key, startTime); + if (watcher) { + fileWatchers.set(key, new WatchpackFileWatcher(this, watcher, files)); + } + } + for (const [key, directories] of directoryWatchersNeeded) { + const watcher = this.watcherManager.watchDirectory(key, startTime); + if (watcher) { + directoryWatchers.set( + key, + new WatchpackDirectoryWatcher(this, watcher, directories), + ); + } + } + }); + this._missing = missingFiles; + this.startTime = startTime; + } + + close() { + this.paused = true; + if (this.aggregateTimer) clearTimeout(this.aggregateTimer); + for (const w of this.fileWatchers.values()) w.close(); + for (const w of this.directoryWatchers.values()) w.close(); + this.fileWatchers.clear(); + this.directoryWatchers.clear(); + } + + pause() { + this.paused = true; + if (this.aggregateTimer) clearTimeout(this.aggregateTimer); + } + + /** + * @returns {Record} times + */ + getTimes() { + /** @type {Set} */ + const directoryWatchers = new Set(); + addWatchersToSet(this.fileWatchers.values(), directoryWatchers); + addWatchersToSet(this.directoryWatchers.values(), directoryWatchers); + /** @type {Record} */ + const obj = Object.create(null); + for (const w of directoryWatchers) { + const times = w.getTimes(); + for (const file of Object.keys(times)) obj[file] = times[file]; + } + return obj; + } + + /** + * @returns {TimeInfoEntries} time info entries + */ + getTimeInfoEntries() { + /** @type {TimeInfoEntries} */ + const map = new Map(); + this.collectTimeInfoEntries(map, map); + return map; + } + + /** + * @param {TimeInfoEntries} fileTimestamps file timestamps + * @param {TimeInfoEntries} directoryTimestamps directory timestamps + */ + collectTimeInfoEntries(fileTimestamps, directoryTimestamps) { + /** @type {Set} */ + const allWatchers = new Set(); + addWatchersToSet(this.fileWatchers.values(), allWatchers); + addWatchersToSet(this.directoryWatchers.values(), allWatchers); + for (const w of allWatchers) { + w.collectTimeInfoEntries(fileTimestamps, directoryTimestamps); + } + } + + /** + * @returns {Aggregated} aggregated info + */ + getAggregated() { + if (this.aggregateTimer) { + clearTimeout(this.aggregateTimer); + this.aggregateTimer = undefined; + } + const changes = this.aggregatedChanges; + const removals = this.aggregatedRemovals; + this.aggregatedChanges = new Set(); + this.aggregatedRemovals = new Set(); + return { changes, removals }; + } + + /** + * @param {string} item item + * @param {number} mtime mtime + * @param {string} file file + * @param {EventType} type type + */ + _onChange(item, mtime, file, type) { + file = file || item; + if (!this.paused) { + this.emit("change", file, mtime, type); + if (this.aggregateTimer) clearTimeout(this.aggregateTimer); + this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout); + } + this.aggregatedRemovals.delete(item); + this.aggregatedChanges.add(item); + } + + /** + * @param {string} item item + * @param {string} file file + * @param {EventType} type type + */ + _onRemove(item, file, type) { + file = file || item; + if (!this.paused) { + this.emit("remove", file, type); + if (this.aggregateTimer) clearTimeout(this.aggregateTimer); + this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout); + } + this.aggregatedChanges.delete(item); + this.aggregatedRemovals.add(item); + } + + _onTimeout() { + this.aggregateTimer = undefined; + const changes = this.aggregatedChanges; + const removals = this.aggregatedRemovals; + this.aggregatedChanges = new Set(); + this.aggregatedRemovals = new Set(); + this.emit("aggregated", changes, removals); + } +} + +module.exports = Watchpack; diff --git a/lib/reducePlan.js b/lib/reducePlan.js index 4b6bc5d..a6ced7f 100644 --- a/lib/reducePlan.js +++ b/lib/reducePlan.js @@ -8,27 +8,27 @@ const path = require("path"); /** * @template T - * @typedef {Object} TreeNode - * @property {string} filePath - * @property {TreeNode} parent - * @property {TreeNode[]} children - * @property {number} entries - * @property {boolean} active - * @property {T[] | T | undefined} value + * @typedef {object} TreeNode + * @property {string} target target + * @property {TreeNode} parent parent + * @property {TreeNode[]} children children + * @property {number} entries number of entries + * @property {boolean} active true when active, otherwise false + * @property {T[] | T | undefined} value value */ /** * @template T - * @param {Map} plan plan + * @param {number} limit limit * @returns {Map>} the new plan */ module.exports = (plan, limit) => { const treeMap = new Map(); // Convert to tree - for (const [filePath, value] of plan) { - treeMap.set(filePath, { - filePath, + for (const [target, value] of plan) { + treeMap.set(target, { + target, parent: undefined, children: undefined, entries: 1, @@ -39,12 +39,12 @@ module.exports = (plan, limit) => { let currentCount = treeMap.size; // Create parents and calculate sum of entries for (const node of treeMap.values()) { - const parentPath = path.dirname(node.filePath); - if (parentPath !== node.filePath) { + const parentPath = path.dirname(node.target); + if (parentPath !== node.target) { let parent = treeMap.get(parentPath); if (parent === undefined) { parent = { - filePath: parentPath, + target: parentPath, parent: undefined, children: [node], entries: node.entries, @@ -120,10 +120,10 @@ module.exports = (plan, limit) => { if (node.value) { if (Array.isArray(node.value)) { for (const item of node.value) { - map.set(item, node.filePath); + map.set(item, node.target); } } else { - map.set(node.value, node.filePath); + map.set(node.value, node.target); } } if (node.children) { @@ -132,7 +132,7 @@ module.exports = (plan, limit) => { } } } - newPlan.set(rootNode.filePath, map); + newPlan.set(rootNode.target, map); } return newPlan; }; diff --git a/lib/watchEventSource.js b/lib/watchEventSource.js index a140dfd..fdee480 100644 --- a/lib/watchEventSource.js +++ b/lib/watchEventSource.js @@ -9,6 +9,9 @@ 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"; @@ -17,6 +20,7 @@ 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 = Boolean( @@ -38,12 +42,24 @@ const directWatchers = new Map(); /** @type {Map} */ 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. @@ -66,9 +82,13 @@ function createHandleChangeEvent(watcher, filePath, handleChangeEvent) { } 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); @@ -99,11 +119,17 @@ class DirectWatcher { 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) { @@ -118,12 +144,17 @@ class DirectWatcher { } } +/** @typedef {Set} WatcherSet */ + class RecursiveWatcher { + /** + * @param {string} rootPath a root path + */ constructor(rootPath) { this.rootPath = rootPath; /** @type {Map} */ this.mapWatcherToPath = new Map(); - /** @type {Map>} */ + /** @type {Map} */ this.mapPathToWatchers = new Map(); this.watcher = undefined; try { @@ -139,10 +170,10 @@ class RecursiveWatcher { ); } 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( @@ -155,7 +186,11 @@ class RecursiveWatcher { } 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)), + ); } } }); @@ -179,6 +214,10 @@ class RecursiveWatcher { } } + /** + * @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) || "."; @@ -193,11 +232,14 @@ class RecursiveWatcher { } } + /** + * @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); @@ -219,18 +261,36 @@ class RecursiveWatcher { } } +/** + * @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 }>} + */ 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); } } +/** + * @param {string} filePath a file path + * @returns {DirectWatcher} a directory watcher + */ const createDirectWatcher = (filePath) => { const existing = directWatchers.get(filePath); if (existing !== undefined) return existing; @@ -239,6 +299,10 @@ const createDirectWatcher = (filePath) => { return w; }; +/** + * @param {string} rootPath a root path + * @returns {RecursiveWatcher} a recursive watcher + */ const createRecursiveWatcher = (rootPath) => { const existing = recursiveWatchers.get(rootPath); if (existing !== undefined) return existing; @@ -250,6 +314,10 @@ const createRecursiveWatcher = (rootPath) => { const execute = () => { /** @type {Map} */ 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) { @@ -330,6 +398,10 @@ const execute = () => { } }; +/** + * @param {string} filePath a file path + * @returns {Watcher} watcher + */ module.exports.watch = (filePath) => { const watcher = new Watcher(); // Find an existing watcher @@ -355,6 +427,9 @@ module.exports.watch = (filePath) => { return watcher; }; +/** + * @param {() => void} fn a function + */ module.exports.batch = (fn) => { isBatch = true; try { @@ -368,3 +443,4 @@ module.exports.batch = (fn) => { module.exports.getNumberOfWatchers = () => watcherCount; module.exports.createHandleChangeEvent = createHandleChangeEvent; module.exports.watcherLimit = watcherLimit; +module.exports.Watcher = Watcher; diff --git a/lib/watchpack.js b/lib/watchpack.js index 1f10d8d..9cff13b 100644 --- a/lib/watchpack.js +++ b/lib/watchpack.js @@ -1,393 +1,8 @@ /* MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra + Author Alexander Akait @akexander-akait */ "use strict"; -const getWatcherManager = require("./getWatcherManager"); -const LinkResolver = require("./LinkResolver"); -const { EventEmitter } = require("events"); -const globToRegExp = require("glob-to-regexp"); -const watchEventSource = require("./watchEventSource"); - -const EMPTY_ARRAY = []; -const EMPTY_OPTIONS = {}; - -function addWatchersToSet(watchers, set) { - for (const ww of watchers) { - const w = ww.watcher; - if (!set.has(w.directoryWatcher)) { - set.add(w.directoryWatcher); - } - } -} - -const stringToRegexp = (ignored) => { - if (ignored.length === 0) { - return; - } - const { source } = globToRegExp(ignored, { globstar: true, extended: true }); - return `${source.slice(0, -1)}(?:$|\\/)`; -}; - -const ignoredToFunction = (ignored) => { - if (Array.isArray(ignored)) { - const stringRegexps = ignored.map((i) => stringToRegexp(i)).filter(Boolean); - if (stringRegexps.length === 0) { - return () => false; - } - const regexp = new RegExp(stringRegexps.join("|")); - return (item) => regexp.test(item.replace(/\\/g, "/")); - } else if (typeof ignored === "string") { - const stringRegexp = stringToRegexp(ignored); - if (!stringRegexp) { - return () => false; - } - const regexp = new RegExp(stringRegexp); - return (item) => regexp.test(item.replace(/\\/g, "/")); - } else if (ignored instanceof RegExp) { - return (item) => ignored.test(item.replace(/\\/g, "/")); - } else if (typeof ignored === "function") { - return ignored; - } else if (ignored) { - throw new Error(`Invalid option for 'ignored': ${ignored}`); - } else { - return () => false; - } -}; - -const normalizeOptions = (options) => ({ - followSymlinks: Boolean(options.followSymlinks), - ignored: ignoredToFunction(options.ignored), - poll: options.poll, -}); - -const normalizeCache = new WeakMap(); -const cachedNormalizeOptions = (options) => { - const cacheEntry = normalizeCache.get(options); - if (cacheEntry !== undefined) return cacheEntry; - const normalized = normalizeOptions(options); - normalizeCache.set(options, normalized); - return normalized; -}; - -class WatchpackFileWatcher { - constructor(watchpack, watcher, files) { - this.files = Array.isArray(files) ? files : [files]; - this.watcher = watcher; - watcher.on("initial-missing", (type) => { - for (const file of this.files) { - if (!watchpack._missing.has(file)) { - watchpack._onRemove(file, file, type); - } - } - }); - watcher.on("change", (mtime, type) => { - for (const file of this.files) { - watchpack._onChange(file, mtime, file, type); - } - }); - watcher.on("remove", (type) => { - for (const file of this.files) { - watchpack._onRemove(file, file, type); - } - }); - } - - update(files) { - if (!Array.isArray(files)) { - if (this.files.length !== 1) { - this.files = [files]; - } else if (this.files[0] !== files) { - this.files[0] = files; - } - } else { - this.files = files; - } - } - - close() { - this.watcher.close(); - } -} - -class WatchpackDirectoryWatcher { - constructor(watchpack, watcher, directories) { - this.directories = Array.isArray(directories) ? directories : [directories]; - this.watcher = watcher; - watcher.on("initial-missing", (type) => { - for (const item of this.directories) { - watchpack._onRemove(item, item, type); - } - }); - watcher.on("change", (file, mtime, type) => { - for (const item of this.directories) { - watchpack._onChange(item, mtime, file, type); - } - }); - watcher.on("remove", (type) => { - for (const item of this.directories) { - watchpack._onRemove(item, item, type); - } - }); - } - - update(directories) { - if (!Array.isArray(directories)) { - if (this.directories.length !== 1) { - this.directories = [directories]; - } else if (this.directories[0] !== directories) { - this.directories[0] = directories; - } - } else { - this.directories = directories; - } - } - - close() { - this.watcher.close(); - } -} - -class Watchpack extends EventEmitter { - constructor(options) { - super(); - if (!options) options = EMPTY_OPTIONS; - this.options = options; - this.aggregateTimeout = - typeof options.aggregateTimeout === "number" - ? options.aggregateTimeout - : 200; - this.watcherOptions = cachedNormalizeOptions(options); - this.watcherManager = getWatcherManager(this.watcherOptions); - this.fileWatchers = new Map(); - this.directoryWatchers = new Map(); - this._missing = new Set(); - this.startTime = undefined; - this.paused = false; - this.aggregatedChanges = new Set(); - this.aggregatedRemovals = new Set(); - this.aggregateTimer = undefined; - this._onTimeout = this._onTimeout.bind(this); - } - - watch(arg1, arg2, arg3) { - let files; - let directories; - let missing; - let startTime; - if (!arg2) { - ({ - files = EMPTY_ARRAY, - directories = EMPTY_ARRAY, - missing = EMPTY_ARRAY, - startTime, - } = arg1); - } else { - files = arg1; - directories = arg2; - missing = EMPTY_ARRAY; - startTime = arg3; - } - this.paused = false; - const { fileWatchers, directoryWatchers } = this; - const { ignored } = this.watcherOptions; - const filter = (path) => !ignored(path); - const addToMap = (map, key, item) => { - const list = map.get(key); - if (list === undefined) { - map.set(key, item); - } else if (Array.isArray(list)) { - list.push(item); - } else { - map.set(key, [list, item]); - } - }; - const fileWatchersNeeded = new Map(); - const directoryWatchersNeeded = new Map(); - const missingFiles = new Set(); - if (this.watcherOptions.followSymlinks) { - const resolver = new LinkResolver(); - for (const file of files) { - if (filter(file)) { - for (const innerFile of resolver.resolve(file)) { - if (file === innerFile || filter(innerFile)) { - addToMap(fileWatchersNeeded, innerFile, file); - } - } - } - } - for (const file of missing) { - if (filter(file)) { - for (const innerFile of resolver.resolve(file)) { - if (file === innerFile || filter(innerFile)) { - missingFiles.add(file); - addToMap(fileWatchersNeeded, innerFile, file); - } - } - } - } - for (const dir of directories) { - if (filter(dir)) { - let first = true; - for (const innerItem of resolver.resolve(dir)) { - if (filter(innerItem)) { - addToMap( - first ? directoryWatchersNeeded : fileWatchersNeeded, - innerItem, - dir, - ); - } - first = false; - } - } - } - } else { - for (const file of files) { - if (filter(file)) { - addToMap(fileWatchersNeeded, file, file); - } - } - for (const file of missing) { - if (filter(file)) { - missingFiles.add(file); - addToMap(fileWatchersNeeded, file, file); - } - } - for (const dir of directories) { - if (filter(dir)) { - addToMap(directoryWatchersNeeded, dir, dir); - } - } - } - // Close unneeded old watchers - // and update existing watchers - for (const [key, w] of fileWatchers) { - const needed = fileWatchersNeeded.get(key); - if (needed === undefined) { - w.close(); - fileWatchers.delete(key); - } else { - w.update(needed); - fileWatchersNeeded.delete(key); - } - } - for (const [key, w] of directoryWatchers) { - const needed = directoryWatchersNeeded.get(key); - if (needed === undefined) { - w.close(); - directoryWatchers.delete(key); - } else { - w.update(needed); - directoryWatchersNeeded.delete(key); - } - } - // Create new watchers and install handlers on these watchers - watchEventSource.batch(() => { - for (const [key, files] of fileWatchersNeeded) { - const watcher = this.watcherManager.watchFile(key, startTime); - if (watcher) { - fileWatchers.set(key, new WatchpackFileWatcher(this, watcher, files)); - } - } - for (const [key, directories] of directoryWatchersNeeded) { - const watcher = this.watcherManager.watchDirectory(key, startTime); - if (watcher) { - directoryWatchers.set( - key, - new WatchpackDirectoryWatcher(this, watcher, directories), - ); - } - } - }); - this._missing = missingFiles; - this.startTime = startTime; - } - - close() { - this.paused = true; - if (this.aggregateTimer) clearTimeout(this.aggregateTimer); - for (const w of this.fileWatchers.values()) w.close(); - for (const w of this.directoryWatchers.values()) w.close(); - this.fileWatchers.clear(); - this.directoryWatchers.clear(); - } - - pause() { - this.paused = true; - if (this.aggregateTimer) clearTimeout(this.aggregateTimer); - } - - getTimes() { - const directoryWatchers = new Set(); - addWatchersToSet(this.fileWatchers.values(), directoryWatchers); - addWatchersToSet(this.directoryWatchers.values(), directoryWatchers); - const obj = Object.create(null); - for (const w of directoryWatchers) { - const times = w.getTimes(); - for (const file of Object.keys(times)) obj[file] = times[file]; - } - return obj; - } - - getTimeInfoEntries() { - const map = new Map(); - this.collectTimeInfoEntries(map, map); - return map; - } - - collectTimeInfoEntries(fileTimestamps, directoryTimestamps) { - const allWatchers = new Set(); - addWatchersToSet(this.fileWatchers.values(), allWatchers); - addWatchersToSet(this.directoryWatchers.values(), allWatchers); - const safeTime = { value: 0 }; - for (const w of allWatchers) { - w.collectTimeInfoEntries(fileTimestamps, directoryTimestamps, safeTime); - } - } - - getAggregated() { - if (this.aggregateTimer) { - clearTimeout(this.aggregateTimer); - this.aggregateTimer = undefined; - } - const changes = this.aggregatedChanges; - const removals = this.aggregatedRemovals; - this.aggregatedChanges = new Set(); - this.aggregatedRemovals = new Set(); - return { changes, removals }; - } - - _onChange(item, mtime, file, type) { - file = file || item; - if (!this.paused) { - this.emit("change", file, mtime, type); - if (this.aggregateTimer) clearTimeout(this.aggregateTimer); - this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout); - } - this.aggregatedRemovals.delete(item); - this.aggregatedChanges.add(item); - } - - _onRemove(item, file, type) { - file = file || item; - if (!this.paused) { - this.emit("remove", file, type); - if (this.aggregateTimer) clearTimeout(this.aggregateTimer); - this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout); - } - this.aggregatedChanges.delete(item); - this.aggregatedRemovals.add(item); - } - - _onTimeout() { - this.aggregateTimer = undefined; - const changes = this.aggregatedChanges; - const removals = this.aggregatedRemovals; - this.aggregatedChanges = new Set(); - this.aggregatedRemovals = new Set(); - this.emit("aggregated", changes, removals); - } -} - -module.exports = Watchpack; +// TODO remove this file in the next major release +module.exports = require("./index"); diff --git a/package-lock.json b/package-lock.json index 7b59589..806bd43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,10 @@ "@eslint/js": "^9.28.0", "@eslint/markdown": "^6.5.0", "@stylistic/eslint-plugin": "^4.4.1", + "@types/glob-to-regexp": "^0.4.4", + "@types/graceful-fs": "^4.1.9", + "@types/jest": "^27.5.1", + "@types/node": "^24.10.4", "coveralls": "^3.0.0", "eslint": "^9.28.0", "eslint-config-prettier": "^10.1.5", @@ -32,6 +36,7 @@ "prettier": "^3.5.3", "rimraf": "^2.6.2", "should": "^8.3.1", + "typescript": "^5.9.3", "write-file-atomic": "^3.0.1" }, "engines": { @@ -594,38 +599,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@pkgr/core": { "version": "0.2.7", "dev": true, @@ -673,6 +646,34 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/glob-to-regexp": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@types/glob-to-regexp/-/glob-to-regexp-0.4.4.tgz", + "integrity": "sha512-nDKoaKJYbnn1MZxUY0cA1bPmmgZbg0cTq7Rh13d0KWYNOiKbqoR+2d89SnRPszGh7ROzSwZ/GOjZ4jPbmmZ6Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jest": { + "version": "27.5.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", + "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-matcher-utils": "^27.0.0", + "pretty-format": "^27.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "dev": true, @@ -696,18 +697,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz", + "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/project-service": { - "version": "8.34.0", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz", + "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.34.0", - "@typescript-eslint/types": "^8.34.0", + "@typescript-eslint/tsconfig-utils": "^8.50.0", + "@typescript-eslint/types": "^8.50.0", "debug": "^4.3.4" }, "engines": { @@ -718,16 +731,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.34.0", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", + "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0" + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -738,7 +753,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.34.0", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz", + "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", "dev": true, "license": "MIT", "engines": { @@ -749,11 +766,13 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.34.0", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", "dev": true, "license": "MIT", "engines": { @@ -765,19 +784,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.34.0", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", + "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.34.0", - "@typescript-eslint/tsconfig-utils": "8.34.0", - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0", + "@typescript-eslint/project-service": "8.50.0", + "@typescript-eslint/tsconfig-utils": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", + "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "engines": { @@ -788,11 +808,13 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -801,6 +823,8 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -814,14 +838,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.34.0", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", + "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.34.0", - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/typescript-estree": "8.34.0" + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -832,16 +858,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.34.0", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", + "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.0", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.50.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1147,17 +1175,6 @@ "concat-map": "0.0.1" } }, - "node_modules/braces": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/browser-stdout": { "version": "1.3.1", "dev": true, @@ -1672,6 +1689,16 @@ "node": ">=0.3.1" } }, + "node_modules/diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, "node_modules/doctrine": { "version": "2.1.0", "dev": true, @@ -2505,32 +2532,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "dev": true, @@ -2541,14 +2542,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.19.1", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fault": { "version": "2.0.1", "dev": true, @@ -2561,26 +2554,33 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, "engines": { - "node": ">=16.0.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/fill-range": { - "version": "7.1.1", + "node_modules/file-entry-cache": { + "version": "8.0.0", "dev": true, "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "flat-cache": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=16.0.0" } }, "node_modules/find-cache-dir": { @@ -3361,14 +3361,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-number-object": { "version": "1.1.1", "dev": true, @@ -3681,6 +3673,48 @@ "node": ">=8" } }, + "node_modules/jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "dev": true, @@ -4104,14 +4138,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/micromark": { "version": "4.0.2", "dev": true, @@ -4662,29 +4688,6 @@ ], "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime-db": { "version": "1.52.0", "dev": true, @@ -5177,7 +5180,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -5247,6 +5252,34 @@ "node": ">=6.0.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/process-on-spawn": { "version": "1.1.0", "dev": true, @@ -5285,23 +5318,11 @@ "node": ">=0.6" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], "license": "MIT" }, "node_modules/reflect.getprototypeof": { @@ -5463,15 +5484,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/reusify": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rimraf": { "version": "2.7.1", "dev": true, @@ -5483,28 +5495,6 @@ "rimraf": "bin.js" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safe-array-concat": { "version": "1.1.3", "dev": true, @@ -6025,15 +6015,21 @@ "node": ">=8" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { - "node": ">=8.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/tough-cookie": { @@ -6050,6 +6046,8 @@ }, "node_modules/ts-api-utils": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { @@ -6205,10 +6203,11 @@ } }, "node_modules/typescript": { - "version": "5.8.3", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6234,6 +6233,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/unist-util-is": { "version": "6.0.0", "dev": true, diff --git a/package.json b/package.json index d1fd645..99993c1 100644 --- a/package.json +++ b/package.json @@ -2,21 +2,29 @@ "name": "watchpack", "version": "2.4.4", "description": "", - "main": "./lib/watchpack.js", + "main": "lib/index.js", "directories": { "test": "test" }, - "files": ["lib/"], + "types": "types/index.js", + "files": [ + "lib/" + ], "scripts": { - "lint": "npm run lint:code", + "lint": "npm run lint:code && npm run lint:types && npm run lint:declarations && npm run fmt:check", "lint:code": "eslint --cache .", + "lint:types": "tsc", + "lint:declarations": "npm run fix:declarations && git diff --exit-code ./types", + "fix": "npm run fix:code && npm run fix:declarations", + "fix:code": "npm run lint:code -- --fix", + "fix:declarations": "tsc --noEmit false --declaration --emitDeclarationOnly --outDir types && npm run fmt -- ./types", "fmt": "npm run fmt:base -- --log-level warn --write", "fmt:check": "npm run fmt:base -- --check", "fmt:base": "prettier --cache --ignore-unknown .", - "test:only": "mocha", - "test:coverage": "nyc --reporter=lcov node_modules/mocha/bin/_mocha", "pretest": "npm run lint", - "test": "mocha" + "test": "mocha", + "test:only": "mocha", + "test:coverage": "nyc --reporter=lcov node_modules/mocha/bin/_mocha" }, "repository": { "type": "git", @@ -32,6 +40,10 @@ "@eslint/js": "^9.28.0", "@eslint/markdown": "^6.5.0", "@stylistic/eslint-plugin": "^4.4.1", + "@types/glob-to-regexp": "^0.4.4", + "@types/graceful-fs": "^4.1.9", + "@types/jest": "^27.5.1", + "@types/node": "^24.10.4", "coveralls": "^3.0.0", "eslint": "^9.28.0", "eslint-config-prettier": "^10.1.5", @@ -48,6 +60,7 @@ "prettier": "^3.5.3", "rimraf": "^2.6.2", "should": "^8.3.1", + "typescript": "^5.9.3", "write-file-atomic": "^3.0.1" }, "dependencies": { diff --git a/playground/watch-folder.js b/playground/watch-folder.js index e8f028e..cacc658 100644 --- a/playground/watch-folder.js +++ b/playground/watch-folder.js @@ -5,6 +5,11 @@ const Watchpack = require("../"); const folder = path.join(__dirname, "folder"); +/** + * @param {string} name name + * @param {string[]} files files + * @param {string} folders folders + */ function startWatcher(name, files, folders) { const w = new Watchpack({ aggregateTimeout: 3000, diff --git a/test/Assumption.js b/test/Assumption.js index 66201f3..9ff79e2 100644 --- a/test/Assumption.js +++ b/test/Assumption.js @@ -41,6 +41,9 @@ describe("Assumption", function assumptionTest() { let maxDiffAfter = -Infinity; let sumDiffAfter = 0; + /** + * @returns {void} + */ function afterMeasure() { // eslint-disable-next-line no-console console.log( @@ -96,6 +99,9 @@ describe("Assumption", function assumptionTest() { let maxDiffAfter = -Infinity; let sumDiffAfter = 0; + /** + * @returns {void} + */ function afterMeasure() { // eslint-disable-next-line no-console console.log( @@ -116,6 +122,9 @@ describe("Assumption", function assumptionTest() { done(); } + /** + * @returns {void} + */ function checkMtime() { before = Date.now(); testHelper.file("a"); @@ -162,6 +171,9 @@ describe("Assumption", function assumptionTest() { let maxDiffAfter = -Infinity; let sumDiffAfter = 0; + /** + * @returns {void} + */ function afterMeasure() { // eslint-disable-next-line no-console console.log( @@ -182,6 +194,9 @@ describe("Assumption", function assumptionTest() { done(); } + /** + * @returns {void} + */ function checkMtime() { before = Date.now(); testHelper.file("a"); diff --git a/test/Casing.js b/test/Casing.js index 73e1785..693ee53 100644 --- a/test/Casing.js +++ b/test/Casing.js @@ -5,7 +5,7 @@ require("should"); const path = require("path"); const TestHelper = require("./helpers/TestHelper"); -const Watchpack = require("../lib/watchpack"); +const Watchpack = require("../lib"); const fixtures = path.join(__dirname, "fixtures"); const testHelper = new TestHelper(fixtures); diff --git a/test/DirectoryWatcher.js b/test/DirectoryWatcher.js index 882ed73..e424bf3 100644 --- a/test/DirectoryWatcher.js +++ b/test/DirectoryWatcher.js @@ -13,6 +13,12 @@ const testHelper = new TestHelper(fixtures); const openWatchers = []; +/** + * @constructor + * @param {string} directoryPath directory path + * @param {object} options options + * @returns {DirectoryWatcher} directory watcher + */ function DirectoryWatcher(directoryPath, options) { const directoryWatcher = new OrgDirectoryWatcher( getWatcherManager(options), diff --git a/test/ManyWatchers.js b/test/ManyWatchers.js index 9e185ea..ce70c44 100644 --- a/test/ManyWatchers.js +++ b/test/ManyWatchers.js @@ -5,7 +5,7 @@ require("should"); const path = require("path"); const TestHelper = require("./helpers/TestHelper"); -const Watchpack = require("../lib/watchpack"); +const Watchpack = require("../lib"); const watchEventSource = require("../lib/watchEventSource"); const should = require("should"); diff --git a/test/Watchpack.js b/test/Watchpack.js index 453e2cd..cec8024 100644 --- a/test/Watchpack.js +++ b/test/Watchpack.js @@ -5,7 +5,7 @@ require("should"); const path = require("path"); const TestHelper = require("./helpers/TestHelper"); -const Watchpack = require("../lib/watchpack"); +const Watchpack = require("../lib"); const fixtures = path.join(__dirname, "fixtures"); const testHelper = new TestHelper(fixtures); @@ -1307,6 +1307,12 @@ describe("Watchpack", function watchpackTest() { testHelper.tick(1000, done); }); + /** + * @param {string[]} files files + * @param {string[]} dirs dirs + * @param {(changes: string[]) => void} callback callback + * @param {() => void} ready ready callback + */ function expectWatchEvent(files, dirs, callback, ready) { const w = new Watchpack({ aggregateTimeout: 500, diff --git a/test/helpers/TestHelper.js b/test/helpers/TestHelper.js index 7f0041f..73b00e5 100644 --- a/test/helpers/TestHelper.js +++ b/test/helpers/TestHelper.js @@ -25,6 +25,10 @@ const checkAllWatcherClosed = () => { watchEventSource.getNumberOfWatchers().should.be.eql(0); }; +/** + * @param {string} testdir testdir + * @constructor + */ function TestHelper(testdir) { this.testdir = testdir; const self = this; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..29ae50a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "lib": ["es2018"], + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true, + "alwaysStrict": true, + "types": ["node"], + "esModuleInterop": true + }, + "include": ["lib/**/*.js"] +} diff --git a/tsconfig.types.json b/tsconfig.types.json new file mode 100644 index 0000000..1c66acf --- /dev/null +++ b/tsconfig.types.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/tsconfig.types.test.json b/tsconfig.types.test.json new file mode 100644 index 0000000..50d014a --- /dev/null +++ b/tsconfig.types.test.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "strict": false, + "noImplicitThis": true, + "alwaysStrict": true, + "strictNullChecks": true, + "types": ["node", "jest"] + }, + "include": ["test/*.js"] +} diff --git a/types/DirectoryWatcher.d.ts b/types/DirectoryWatcher.d.ts new file mode 100644 index 0000000..df01c18 --- /dev/null +++ b/types/DirectoryWatcher.d.ts @@ -0,0 +1,312 @@ +export = DirectoryWatcher; +/** @typedef {Set} InitialScanRemoved */ +/** + * @typedef {object} WatchpackEvents + * @property {(target: string, mtime: string, type: EventType, initial: boolean) => void} change change event + * @property {() => void} closed closed event + */ +/** + * @extends {EventEmitter<{ [K in keyof WatchpackEvents]: Parameters }>} + */ +declare class DirectoryWatcher extends EventEmitter<{ + /** + * change event + */ + change: [ + target: string, + mtime: string, + type: import("./index").EventType, + initial: boolean, + ]; + /** + * closed event + */ + closed: []; +}> { + /** + * @param {WatcherManager} watcherManager a watcher manager + * @param {string} directoryPath directory path + * @param {NormalizedWatchOptions} options options + */ + constructor( + watcherManager: WatcherManager, + directoryPath: string, + options: NormalizedWatchOptions, + ); + watcherManager: import("./getWatcherManager").WatcherManager; + options: import("./index").NormalizedWatchOptions; + path: string; + /** @type {Map} */ + files: Map; + /** @type {Map} */ + filesWithoutCase: Map; + /** @type {Map | boolean>} */ + directories: Map | boolean>; + lastWatchEvent: number; + initialScan: boolean; + ignored: import("./index").IgnoredFunction; + nestedWatching: boolean; + /** @type {number | false} */ + polledWatching: number | false; + /** @type {undefined | NodeJS.Timeout} */ + timeout: undefined | NodeJS.Timeout; + /** @type {null | InitialScanRemoved} */ + initialScanRemoved: null | InitialScanRemoved; + /** @type {undefined | number} */ + initialScanFinished: undefined | number; + /** @type {Map | Watcher>>} */ + watchers: Map< + string, + Set | Watcher> + >; + /** @type {Watcher | null} */ + parentWatcher: Watcher | null; + refs: number; + /** @type {Map} */ + _activeEvents: Map; + closed: boolean; + scanning: boolean; + scanAgain: boolean; + scanAgainInitial: boolean; + createWatcher(): void; + watcher: watchEventSource.Watcher | null | undefined; + /** + * @template {(watcher: Watcher) => void} T + * @param {string} path path + * @param {T} fn function + */ + forEachWatcher) => void>( + path: string, + fn: T, + ): void; + /** + * @param {string} itemPath an item path + * @param {boolean} initial true when initial, otherwise false + * @param {EventType} type even type + */ + setMissing(itemPath: string, initial: boolean, type: EventType): void; + /** + * @param {string} target a target to set file time + * @param {number} mtime mtime + * @param {boolean} initial true when initial, otherwise false + * @param {boolean} ignoreWhenEqual true to ignore when equal, otherwise false + * @param {EventType} type type + */ + setFileTime( + target: string, + mtime: number, + initial: boolean, + ignoreWhenEqual: boolean, + type: EventType, + ): void; + /** + * @param {string} directoryPath directory path + * @param {number} birthtime birthtime + * @param {boolean} initial true when initial, otherwise false + * @param {EventType} type even type + */ + setDirectory( + directoryPath: string, + birthtime: number, + initial: boolean, + type: EventType, + ): void; + /** + * @param {string} directoryPath directory path + */ + createNestedWatcher(directoryPath: string): void; + /** + * @param {boolean} flag true when nested, otherwise false + */ + setNestedWatching(flag: boolean): void; + /** + * @param {string} target a target to watch + * @param {number=} startTime start time + * @returns {Watcher | Watcher} watcher + */ + watch( + target: string, + startTime?: number | undefined, + ): Watcher | Watcher; + /** + * @param {EventType} eventType event type + * @param {string=} filename filename + */ + onWatchEvent(eventType: EventType, filename?: string | undefined): void; + /** + * @param {unknown=} err error + */ + onWatcherError(err?: unknown | undefined): void; + /** + * @param {Error | NodeJS.ErrnoException=} err error + */ + onStatsError(err?: (Error | NodeJS.ErrnoException) | undefined): void; + /** + * @param {Error | NodeJS.ErrnoException=} err error + */ + onScanError(err?: (Error | NodeJS.ErrnoException) | undefined): void; + onScanFinished(): void; + /** + * @param {string} reason a reason + */ + onDirectoryRemoved(reason: string): void; + watchInParentDirectory(): void; + /** + * @param {boolean} initial true when initial, otherwise false + */ + doScan(initial: boolean): void; + /** + * @returns {Record} times + */ + getTimes(): Record; + /** + * @param {TimeInfoEntries} fileTimestamps file timestamps + * @param {TimeInfoEntries} directoryTimestamps directory timestamps + * @returns {number} safe time + */ + collectTimeInfoEntries( + fileTimestamps: TimeInfoEntries, + directoryTimestamps: TimeInfoEntries, + ): number; + close(): void; +} +declare namespace DirectoryWatcher { + export { + Watcher, + EXISTANCE_ONLY_TIME_ENTRY, + NormalizedWatchOptions, + EventType, + TimeInfoEntries, + Entry, + ExistanceOnlyTimeEntry, + OnlySafeTimeEntry, + EventMap, + WatcherManager, + EventSourceWatcher, + FileWatcherEvents, + DirectoryWatcherEvents, + InitialScanRemoved, + WatchpackEvents, + }; +} +import { EventEmitter } from "events"; +/** + * @typedef {object} FileWatcherEvents + * @property {(type: EventType) => void} initial-missing initial missing event + * @property {(mtime: number, type: EventType, initial: boolean) => void} change change event + * @property {(type: EventType) => void} remove remove event + * @property {() => void} closed closed event + */ +/** + * @typedef {object} DirectoryWatcherEvents + * @property {(type: EventType) => void} initial-missing initial missing event + * @property {((file: string, mtime: number, type: EventType, initial: boolean) => void)} change change event + * @property {(type: EventType) => void} remove remove event + * @property {() => void} closed closed event + */ +/** + * @template {EventMap} T + * @extends {EventEmitter<{ [K in keyof T]: Parameters }>} + */ +declare class Watcher extends EventEmitter<{ + [K in keyof T]: Parameters; +}> { + /** + * @param {DirectoryWatcher} directoryWatcher a directory watcher + * @param {string} target a target to watch + * @param {number=} startTime start time + */ + constructor( + directoryWatcher: DirectoryWatcher, + target: string, + startTime?: number | undefined, + ); + directoryWatcher: DirectoryWatcher; + path: string; + startTime: number | undefined; + /** + * @param {number} mtime mtime + * @param {boolean} initial true when initial, otherwise false + * @returns {boolean} true of start time less than mtile, otherwise false + */ + checkStartTime(mtime: number, initial: boolean): boolean; + close(): void; +} +import watchEventSource = require("./watchEventSource"); +/** @typedef {import("./index").NormalizedWatchOptions} NormalizedWatchOptions */ +/** @typedef {import("./index").EventType} EventType */ +/** @typedef {import("./index").TimeInfoEntries} TimeInfoEntries */ +/** @typedef {import("./index").Entry} Entry */ +/** @typedef {import("./index").ExistanceOnlyTimeEntry} ExistanceOnlyTimeEntry */ +/** @typedef {import("./index").OnlySafeTimeEntry} OnlySafeTimeEntry */ +/** @typedef {import("./index").EventMap} EventMap */ +/** @typedef {import("./getWatcherManager").WatcherManager} WatcherManager */ +/** @typedef {import("./watchEventSource").Watcher} EventSourceWatcher */ +/** @type {ExistanceOnlyTimeEntry} */ +declare const EXISTANCE_ONLY_TIME_ENTRY: ExistanceOnlyTimeEntry; +type NormalizedWatchOptions = import("./index").NormalizedWatchOptions; +type EventType = import("./index").EventType; +type TimeInfoEntries = import("./index").TimeInfoEntries; +type Entry = import("./index").Entry; +type ExistanceOnlyTimeEntry = import("./index").ExistanceOnlyTimeEntry; +type OnlySafeTimeEntry = import("./index").OnlySafeTimeEntry; +type EventMap = import("./index").EventMap; +type WatcherManager = import("./getWatcherManager").WatcherManager; +type EventSourceWatcher = import("./watchEventSource").Watcher; +type FileWatcherEvents = { + /** + * initial missing event + */ + "initial-missing": (type: EventType) => void; + /** + * change event + */ + change: (mtime: number, type: EventType, initial: boolean) => void; + /** + * remove event + */ + remove: (type: EventType) => void; + /** + * closed event + */ + closed: () => void; +}; +type DirectoryWatcherEvents = { + /** + * initial missing event + */ + "initial-missing": (type: EventType) => void; + /** + * change event + */ + change: ( + file: string, + mtime: number, + type: EventType, + initial: boolean, + ) => void; + /** + * remove event + */ + remove: (type: EventType) => void; + /** + * closed event + */ + closed: () => void; +}; +type InitialScanRemoved = Set; +type WatchpackEvents = { + /** + * change event + */ + change: ( + target: string, + mtime: string, + type: EventType, + initial: boolean, + ) => void; + /** + * closed event + */ + closed: () => void; +}; diff --git a/types/LinkResolver.d.ts b/types/LinkResolver.d.ts new file mode 100644 index 0000000..00fd534 --- /dev/null +++ b/types/LinkResolver.d.ts @@ -0,0 +1,10 @@ +export = LinkResolver; +declare class LinkResolver { + /** @type {Map} */ + cache: Map; + /** + * @param {string} file path to file or directory + * @returns {readonly string[]} array of file and all symlinks contributed in the resolving process (first item is the resolved file) + */ + resolve(file: string): readonly string[]; +} diff --git a/types/getWatcherManager.d.ts b/types/getWatcherManager.d.ts new file mode 100644 index 0000000..39d3a7c --- /dev/null +++ b/types/getWatcherManager.d.ts @@ -0,0 +1,61 @@ +declare namespace _exports { + export { + NormalizedWatchOptions, + EventMap, + DirectoryWatcherEvents, + FileWatcherEvents, + Watcher, + }; +} +declare function _exports(options: NormalizedWatchOptions): WatcherManager; +declare namespace _exports { + export { WatcherManager }; +} +export = _exports; +type NormalizedWatchOptions = import("./index").NormalizedWatchOptions; +type EventMap = import("./index").EventMap; +type DirectoryWatcherEvents = + import("./DirectoryWatcher").DirectoryWatcherEvents; +type FileWatcherEvents = import("./DirectoryWatcher").FileWatcherEvents; +type Watcher = import("./DirectoryWatcher").Watcher; +/** @typedef {import("./index").NormalizedWatchOptions} NormalizedWatchOptions */ +/** @typedef {import("./index").EventMap} EventMap */ +/** @typedef {import("./DirectoryWatcher").DirectoryWatcherEvents} DirectoryWatcherEvents */ +/** @typedef {import("./DirectoryWatcher").FileWatcherEvents} FileWatcherEvents */ +/** + * @template {EventMap} T + * @typedef {import("./DirectoryWatcher").Watcher} Watcher + */ +declare class WatcherManager { + /** + * @param {NormalizedWatchOptions} options options + */ + constructor(options: NormalizedWatchOptions); + options: import("./index").NormalizedWatchOptions; + /** @type {Map} */ + directoryWatchers: Map; + /** + * @param {string} directory a directory + * @returns {DirectoryWatcher} a directory watcher + */ + getDirectoryWatcher(directory: string): DirectoryWatcher; + /** + * @param {string} file file + * @param {number=} startTime start time + * @returns {Watcher | null} watcher or null if file has no directory + */ + watchFile( + file: string, + startTime?: number | undefined, + ): Watcher | null; + /** + * @param {string} directory directory + * @param {number=} startTime start time + * @returns {Watcher} watcher + */ + watchDirectory( + directory: string, + startTime?: number | undefined, + ): Watcher; +} +import DirectoryWatcher = require("./DirectoryWatcher"); diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..ef6a109 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,261 @@ +export = Watchpack; +/** + * @typedef {object} WatchpackEvents + * @property {(file: string, mtime: number, type: EventType) => void} change change event + * @property {(file: string, type: EventType) => void} remove remove event + * @property {(changes: Changes, removals: Removals) => void} aggregated aggregated event + */ +/** + * @extends {EventEmitter<{ [K in keyof WatchpackEvents]: Parameters }>} + */ +declare class Watchpack extends EventEmitter<{ + /** + * change event + */ + change: [file: string, mtime: number, type: EventType]; + /** + * remove event + */ + remove: [file: string, type: EventType]; + /** + * aggregated event + */ + aggregated: [changes: Changes, removals: Removals]; +}> { + /** + * @param {WatchOptions} options options + */ + constructor(options: WatchOptions); + /** @type {WatchOptions} */ + options: WatchOptions; + aggregateTimeout: number; + /** @type {NormalizedWatchOptions} */ + watcherOptions: NormalizedWatchOptions; + /** @type {WatcherManager} */ + watcherManager: WatcherManager; + /** @type {Map} */ + fileWatchers: Map; + /** @type {Map} */ + directoryWatchers: Map; + /** @type {Set} */ + _missing: Set; + startTime: number | undefined; + paused: boolean; + /** @type {Changes} */ + aggregatedChanges: Changes; + /** @type {Removals} */ + aggregatedRemovals: Removals; + /** @type {undefined | NodeJS.Timeout} */ + aggregateTimer: undefined | NodeJS.Timeout; + _onTimeout(): void; + /** + * @overload + * @param {Iterable} arg1 files + * @param {Iterable} arg2 directories + * @param {number=} arg3 startTime + * @returns {void} + */ + watch( + arg1: Iterable, + arg2: Iterable, + arg3?: number | undefined, + ): void; + /** + * @overload + * @param {WatchMethodOptions} arg1 watch options + * @returns {void} + */ + watch(arg1: WatchMethodOptions): void; + close(): void; + pause(): void; + /** + * @returns {Record} times + */ + getTimes(): Record; + /** + * @returns {TimeInfoEntries} time info entries + */ + getTimeInfoEntries(): TimeInfoEntries; + /** + * @param {TimeInfoEntries} fileTimestamps file timestamps + * @param {TimeInfoEntries} directoryTimestamps directory timestamps + */ + collectTimeInfoEntries( + fileTimestamps: TimeInfoEntries, + directoryTimestamps: TimeInfoEntries, + ): void; + /** + * @returns {Aggregated} aggregated info + */ + getAggregated(): Aggregated; + /** + * @param {string} item item + * @param {number} mtime mtime + * @param {string} file file + * @param {EventType} type type + */ + _onChange(item: string, mtime: number, file: string, type: EventType): void; + /** + * @param {string} item item + * @param {string} file file + * @param {EventType} type type + */ + _onRemove(item: string, file: string, type: EventType): void; +} +declare namespace Watchpack { + export { + WatcherManager, + DirectoryWatcher, + DirectoryWatcherEvents, + FileWatcherEvents, + EventMap, + Watcher, + IgnoredFunction, + Ignored, + WatcherOptions, + WatchOptions, + NormalizedWatchOptions, + EventType, + Entry, + OnlySafeTimeEntry, + ExistanceOnlyTimeEntry, + TimeInfoEntries, + Changes, + Removals, + Aggregated, + WatchMethodOptions, + Times, + WatchpackEvents, + }; +} +import { EventEmitter } from "events"; +declare class WatchpackFileWatcher { + /** + * @param {Watchpack} watchpack watchpack + * @param {Watcher} watcher watcher + * @param {string | string[]} files files + */ + constructor( + watchpack: Watchpack, + watcher: Watcher, + files: string | string[], + ); + /** @type {string[]} */ + files: string[]; + watcher: import("./DirectoryWatcher").Watcher< + import("./DirectoryWatcher").FileWatcherEvents + >; + /** + * @param {string | string[]} files files + */ + update(files: string | string[]): void; + close(): void; +} +declare class WatchpackDirectoryWatcher { + /** + * @param {Watchpack} watchpack watchpack + * @param {Watcher} watcher watcher + * @param {string} directories directories + */ + constructor( + watchpack: Watchpack, + watcher: Watcher, + directories: string, + ); + /** @type {string[]} */ + directories: string[]; + watcher: import("./DirectoryWatcher").Watcher< + import("./DirectoryWatcher").DirectoryWatcherEvents + >; + /** + * @param {string | string[]} directories directories + */ + update(directories: string | string[]): void; + close(): void; +} +type WatcherManager = import("./getWatcherManager").WatcherManager; +type DirectoryWatcher = import("./DirectoryWatcher"); +type DirectoryWatcherEvents = + import("./DirectoryWatcher").DirectoryWatcherEvents; +type FileWatcherEvents = import("./DirectoryWatcher").FileWatcherEvents; +type EventMap = Record any>; +type Watcher = import("./DirectoryWatcher").Watcher; +type IgnoredFunction = (item: string) => boolean; +type Ignored = string[] | RegExp | string | IgnoredFunction; +type WatcherOptions = { + /** + * true when need to resolve symlinks and watch symlink and real file, otherwise false + */ + followSymlinks?: boolean | undefined; + /** + * ignore some files from watching (glob pattern or regexp) + */ + ignored?: Ignored | undefined; + /** + * true when need to enable polling mode for watching, otherwise false + */ + poll?: (number | boolean) | undefined; +}; +type WatchOptions = WatcherOptions & { + aggregateTimeout?: number; +}; +type NormalizedWatchOptions = { + /** + * true when need to resolve symlinks and watch symlink and real file, otherwise false + */ + followSymlinks: boolean; + /** + * ignore some files from watching (glob pattern or regexp) + */ + ignored: IgnoredFunction; + /** + * true when need to enable polling mode for watching, otherwise false + */ + poll?: (number | boolean) | undefined; +}; +type EventType = + | `scan (${string})` + | "change" + | "rename" + | `watch ${string}` + | `directory-removed ${string}`; +type Entry = { + safeTime: number; + timestamp: number; + accuracy: number; +}; +type OnlySafeTimeEntry = { + safeTime: number; +}; +type ExistanceOnlyTimeEntry = {}; +type TimeInfoEntries = Map< + string, + Entry | OnlySafeTimeEntry | ExistanceOnlyTimeEntry | null +>; +type Changes = Set; +type Removals = Set; +type Aggregated = { + changes: Changes; + removals: Removals; +}; +type WatchMethodOptions = { + files?: Iterable; + directories?: Iterable; + missing?: Iterable; + startTime?: number; +}; +type Times = Record; +type WatchpackEvents = { + /** + * change event + */ + change: (file: string, mtime: number, type: EventType) => void; + /** + * remove event + */ + remove: (file: string, type: EventType) => void; + /** + * aggregated event + */ + aggregated: (changes: Changes, removals: Removals) => void; +}; diff --git a/types/reducePlan.d.ts b/types/reducePlan.d.ts new file mode 100644 index 0000000..8a5d69c --- /dev/null +++ b/types/reducePlan.d.ts @@ -0,0 +1,34 @@ +declare namespace _exports { + export { TreeNode }; +} +declare function _exports( + plan: Map, + limit: number, +): Map>; +export = _exports; +type TreeNode = { + /** + * target + */ + target: string; + /** + * parent + */ + parent: TreeNode; + /** + * children + */ + children: TreeNode[]; + /** + * number of entries + */ + entries: number; + /** + * true when active, otherwise false + */ + active: boolean; + /** + * value + */ + value: T[] | T | undefined; +}; diff --git a/types/watchEventSource.d.ts b/types/watchEventSource.d.ts new file mode 100644 index 0000000..6b16bb6 --- /dev/null +++ b/types/watchEventSource.d.ts @@ -0,0 +1,53 @@ +export function watch(filePath: string): Watcher; +export function batch(fn: () => void): void; +export function getNumberOfWatchers(): number; +export type FSWatcher = import("fs").FSWatcher; +export type EventType = import("./index").EventType; +export type WatcherSet = Set; +export type WatcherEvents = { + /** + * change event + */ + change: (eventType: EventType, filename?: string) => void; + /** + * error event + */ + error: (err: unknown) => void; +}; +/** + * @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 }>} + */ +export class Watcher extends EventEmitter<{ + /** + * change event + */ + change: [ + eventType: import("./index").EventType, + filename?: string | undefined, + ]; + /** + * error event + */ + error: [err: unknown]; +}> { + constructor(); + close(): void; +} +/** + * @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 + */ +export function createHandleChangeEvent( + watcher: FSWatcher, + filePath: string, + handleChangeEvent: (type: "rename" | "change", filename: string) => void, +): (type: "rename" | "change", filename: string) => void; +export const watcherLimit: number; +import { EventEmitter } from "events"; diff --git a/types/watchpack.d.ts b/types/watchpack.d.ts new file mode 100644 index 0000000..77bed9a --- /dev/null +++ b/types/watchpack.d.ts @@ -0,0 +1,2 @@ +declare const _exports: typeof import("./index"); +export = _exports;