Repository: webpack/enhanced-resolve
Pull request #409 (2024-03-07 13:27:35)

fix: support buffer/URL/number paths


Patch:
diff --git a/lib/CachedInputFileSystem.js b/lib/CachedInputFileSystem.js
index c8d304b..cddf00e 100644
--- a/lib/CachedInputFileSystem.js
+++ b/lib/CachedInputFileSystem.js
@@ -72,16 +72,28 @@ class OperationMergerBackend {
 
 		this.provide = this._provider
 			? /**
-			   * @param {string} path path
-			   * @param {any} options options
-			   * @param {function} callback callback
+			   * @param {PathLike | PathOrFileDescriptor} path path
+			   * @param {object | FileSystemCallback<any> | undefined} options options
+			   * @param {FileSystemCallback<any>=} callback callback
 			   * @returns {any} result
 			   */
 			  (path, options, callback) => {
 					if (typeof options === "function") {
-						callback = options;
+						callback = /** @type {FileSystemCallback<any>} */ (options);
 						options = undefined;
 					}
+					if (
+						typeof path !== "string" &&
+						!Buffer.isBuffer(path) &&
+						!(path instanceof URL) &&
+						typeof path !== "number"
+					) {
+						/** @type {Function} */
+						(callback)(
+							new TypeError("path must be a string, Buffer, URL or number")
+						);
+						return;
+					}
 					if (options) {
 						return /** @type {Function} */ (this._provider).call(
 							this._providerContext,
@@ -90,10 +102,6 @@ class OperationMergerBackend {
 							callback
 						);
 					}
-					if (typeof path !== "string") {
-						callback(new TypeError("path must be a string"));
-						return;
-					}
 					let callbacks = this._activeAsyncOperations.get(path);
 					if (callbacks) {
 						callbacks.push(callback);
@@ -116,8 +124,8 @@ class OperationMergerBackend {
 			: null;
 		this.provideSync = this._syncProvider
 			? /**
-			   * @param {string} path path
-			   * @param {any} options options
+			   * @param {PathLike | PathOrFileDescriptor} path path
+			   * @param {object=} options options
 			   * @returns {any} result
 			   */
 			  (path, options) => {
@@ -213,10 +221,16 @@ class CacheBackend {
 			callback = options;
 			options = undefined;
 		}
-		if (typeof path !== "string") {
-			callback(new TypeError("path must be a string"));
+		if (
+			typeof path !== "string" &&
+			!Buffer.isBuffer(path) &&
+			!(path instanceof URL) &&
+			typeof path !== "number"
+		) {
+			callback(new TypeError("path must be a string, Buffer, URL or number"));
 			return;
 		}
+		const strPath = typeof path !== "string" ? path.toString() : path;
 		if (options) {
 			return /** @type {Function} */ (this._provider).call(
 				this._providerContext,
@@ -232,19 +246,19 @@ class CacheBackend {
 		}
 
 		// Check in cache
-		let cacheEntry = this._data.get(path);
+		let cacheEntry = this._data.get(strPath);
 		if (cacheEntry !== undefined) {
 			if (cacheEntry.err) return nextTick(callback, cacheEntry.err);
 			return nextTick(callback, null, cacheEntry.result);
 		}
 
 		// Check if there is already the same operation running
-		let callbacks = this._activeAsyncOperations.get(path);
+		let callbacks = this._activeAsyncOperations.get(strPath);
 		if (callbacks !== undefined) {
 			callbacks.push(callback);
 			return;
 		}
-		this._activeAsyncOperations.set(path, (callbacks = [callback]));
+		this._activeAsyncOperations.set(strPath, (callbacks = [callback]));
 
 		// Run the operation
 		/** @type {Function} */
@@ -256,8 +270,8 @@ class CacheBackend {
 			 * @param {any} [result] result
 			 */
 			(err, result) => {
-				this._activeAsyncOperations.delete(path);
-				this._storeResult(path, err, result);
+				this._activeAsyncOperations.delete(strPath);
+				this._storeResult(strPath, err, result);
 
 				// Enter async mode if not yet done
 				this._enterAsyncMode();
@@ -277,9 +291,15 @@ class CacheBackend {
 	 * @returns {any} result
 	 */
 	provideSync(path, options) {
-		if (typeof path !== "string") {
+		if (
+			typeof path !== "string" &&
+			!Buffer.isBuffer(path) &&
+			!(path instanceof URL) &&
+			typeof path !== "number"
+		) {
 			throw new TypeError("path must be a string");
 		}
+		const strPath = typeof path !== "string" ? path.toString() : path;
 		if (options) {
 			return /** @type {Function} */ (this._syncProvider).call(
 				this._providerContext,
@@ -294,7 +314,7 @@ class CacheBackend {
 		}
 
 		// Check in cache
-		let cacheEntry = this._data.get(path);
+		let cacheEntry = this._data.get(strPath);
 		if (cacheEntry !== undefined) {
 			if (cacheEntry.err) throw cacheEntry.err;
 			return cacheEntry.result;
@@ -302,8 +322,8 @@ class CacheBackend {
 
 		// Get all active async operations
 		// This sync operation will also complete them
-		const callbacks = this._activeAsyncOperations.get(path);
-		this._activeAsyncOperations.delete(path);
+		const callbacks = this._activeAsyncOperations.get(strPath);
+		this._activeAsyncOperations.delete(strPath);
 
 		// Run the operation
 		// When in idle mode, we will enter sync mode
@@ -314,14 +334,14 @@ class CacheBackend {
 				path
 			);
 		} catch (err) {
-			this._storeResult(path, /** @type {Error} */ (err), undefined);
+			this._storeResult(strPath, /** @type {Error} */ (err), undefined);
 			this._enterSyncModeWhenIdle();
 			if (callbacks) {
 				runCallbacks(callbacks, /** @type {Error} */ (err), undefined);
 			}
 			throw err;
 		}
-		this._storeResult(path, null, result);
+		this._storeResult(strPath, null, result);
 		this._enterSyncModeWhenIdle();
 		if (callbacks) {
 			runCallbacks(callbacks, null, result);
@@ -330,7 +350,7 @@ class CacheBackend {
 	}
 
 	/**
-	 * @param {string | string[] | Set<string>} [what] what to purge
+	 * @param {string | Buffer | URL | number | (string | URL | Buffer | number)[] | Set<string | URL | Buffer | number>} [what] what to purge
 	 */
 	purge(what) {
 		if (!what) {
@@ -341,9 +361,15 @@ class CacheBackend {
 				}
 				this._enterIdleMode();
 			}
-		} else if (typeof what === "string") {
+		} else if (
+			typeof what === "string" ||
+			Buffer.isBuffer(what) ||
+			what instanceof URL ||
+			typeof what === "number"
+		) {
+			const strWhat = typeof what !== "string" ? what.toString() : what;
 			for (let [key, data] of this._data) {
-				if (key.startsWith(what)) {
+				if (key.startsWith(strWhat)) {
 					this._data.delete(key);
 					data.level.delete(key);
 				}
@@ -354,7 +380,8 @@ class CacheBackend {
 		} else {
 			for (let [key, data] of this._data) {
 				for (const item of what) {
-					if (key.startsWith(item)) {
+					const strItem = typeof item !== "string" ? item.toString() : item;
+					if (key.startsWith(strItem)) {
 						this._data.delete(key);
 						data.level.delete(key);
 						break;
@@ -368,17 +395,24 @@ class CacheBackend {
 	}
 
 	/**
-	 * @param {string|string[]|Set<string>} [what] what to purge
+	 * @param {string | Buffer | URL | number | (string | URL | Buffer | number)[] | Set<string | URL | Buffer | number>} [what] what to purge
 	 */
 	purgeParent(what) {
 		if (!what) {
 			this.purge();
-		} else if (typeof what === "string") {
-			this.purge(dirname(what));
+		} else if (
+			typeof what === "string" ||
+			Buffer.isBuffer(what) ||
+			what instanceof URL ||
+			typeof what === "number"
+		) {
+			const strWhat = typeof what !== "string" ? what.toString() : what;
+			this.purge(dirname(strWhat));
 		} else {
 			const set = new Set();
 			for (const item of what) {
-				set.add(dirname(item));
+				const strItem = typeof item !== "string" ? item.toString() : item;
+				set.add(dirname(strItem));
 			}
 			this.purge(set);
 		}
@@ -616,7 +650,7 @@ module.exports = class CachedInputFileSystem {
 	}
 
 	/**
-	 * @param {string|string[]|Set<string>} [what] what to purge
+	 * @param {string | Buffer | URL | number | (string | URL | Buffer | number)[] | Set<string | URL | Buffer | number>} [what] what to purge
 	 */
 	purge(what) {
 		this._statBackend.purge(what);
diff --git a/types.d.ts b/types.d.ts
index 6c69f4f..d3bf8bb 100644
--- a/types.d.ts
+++ b/types.d.ts
@@ -75,7 +75,15 @@ declare class CachedInputFileSystem {
 	readlinkSync: ReadlinkSync;
 	realpath?: RealPath;
 	realpathSync?: RealPathSync;
-	purge(what?: string | Set<string> | string[]): void;
+	purge(
+		what?:
+			| string
+			| number
+			| Buffer
+			| URL_url
+			| (string | number | Buffer | URL_url)[]
+			| Set<string | number | Buffer | URL_url>
+	): void;
 }
 declare class CloneBasenamePlugin {
 	constructor(


Test patch:
diff --git a/test/CachedInputFileSystem.test.js b/test/CachedInputFileSystem.test.js
index 2ee1d7c..c3a70e1 100644
--- a/test/CachedInputFileSystem.test.js
+++ b/test/CachedInputFileSystem.test.js
@@ -1,4 +1,6 @@
 const { CachedInputFileSystem } = require("../");
+const path = require("path");
+const url = require("url");
 
 describe("CachedInputFileSystem OperationMergerBackend ('stat' and 'statSync')", () => {
 	let fs;
@@ -432,7 +434,19 @@ describe("CachedInputFileSystem CacheBackend", () => {
 					fs.purge(["/test/path"]);
 					fs.readdir("/test/path", (err, r) => {
 						expect(r[0]).toEqual("2");
-						done();
+						fs.purge([url.pathToFileURL("/test/path")]);
+						fs.readdir("/test/path", (err, r) => {
+							expect(r[0]).toEqual("2");
+							fs.purge(Buffer.from("/test/path"));
+							fs.readdir("/test/path", (err, r) => {
+								expect(r[0]).toEqual("3");
+								fs.purge([Buffer.from("/test/path")]);
+								fs.readdir("/test/path", (err, r) => {
+									expect(r[0]).toEqual("4");
+									done();
+								});
+							});
+						});
 					});
 				});
 			});
@@ -453,3 +467,119 @@ describe("CachedInputFileSystem CacheBackend", () => {
 		next();
 	});
 });
+
+describe("CachedInputFileSystem CacheBackend and Node.JS filesystem", () => {
+	let fs;
+
+	beforeEach(() => {
+		fs = new CachedInputFileSystem(require("fs"), 1);
+	});
+
+	const file = path.resolve(__dirname, "./fixtures/abc.txt");
+
+	it("should work with string async", function (done) {
+		fs.readFile(file, (err, r) => {
+			if (err) {
+				done(err);
+				return;
+			}
+			expect(r.toString()).toEqual("abc");
+			done();
+		});
+	});
+
+	it("should work with string sync", function () {
+		const r = fs.readFileSync(file);
+		expect(r.toString()).toEqual("abc");
+	});
+
+	it("should work with Buffer async", function (done) {
+		fs.readFile(Buffer.from(file), (err, r) => {
+			if (err) {
+				done(err);
+				return;
+			}
+			expect(r.toString()).toEqual("abc");
+			done();
+		});
+	});
+
+	it("should work with Buffer sync", function () {
+		const r = fs.readFileSync(Buffer.from(file));
+		expect(r.toString()).toEqual("abc");
+	});
+
+	it("should work with URL async", function (done) {
+		fs.readFile(url.pathToFileURL(file), (err, r) => {
+			if (err) {
+				done(err);
+				return;
+			}
+			expect(r.toString()).toEqual("abc");
+			done();
+		});
+	});
+
+	it("should work with URL sync", function () {
+		const r = fs.readFileSync(url.pathToFileURL(file));
+		expect(r.toString()).toEqual("abc");
+	});
+});
+
+describe("CachedInputFileSystem OperationMergerBackend and Node.JS filesystem", () => {
+	let fs;
+
+	beforeEach(() => {
+		fs = new CachedInputFileSystem(require("fs"), 0);
+	});
+
+	const file = path.resolve(__dirname, "./fixtures/abc.txt");
+
+	it("should work with string async", function (done) {
+		fs.readFile(file, (err, r) => {
+			if (err) {
+				done(err);
+				return;
+			}
+			expect(r.toString()).toEqual("abc");
+			done();
+		});
+	});
+
+	it("should work with string sync", function () {
+		const r = fs.readFileSync(file);
+		expect(r.toString()).toEqual("abc");
+	});
+
+	it("should work with Buffer async", function (done) {
+		fs.readFile(Buffer.from(file), (err, r) => {
+			if (err) {
+				done(err);
+				return;
+			}
+			expect(r.toString()).toEqual("abc");
+			done();
+		});
+	});
+
+	it("should work with Buffer sync", function () {
+		const r = fs.readFileSync(Buffer.from(file));
+		expect(r.toString()).toEqual("abc");
+	});
+
+	it("should work with URL async", function (done) {
+		fs.readFile(url.pathToFileURL(file), (err, r) => {
+			if (err) {
+				done(err);
+				return;
+			}
+			expect(r.toString()).toEqual("abc");
+			done();
+		});
+	});
+
+	it("should work with URL sync", function () {
+		const r = fs.readFileSync(url.pathToFileURL(file));
+		expect(r.toString()).toEqual("abc");
+	});
+});