From 6b951458405a806aa5a6bed4f585fc84e6d5feb5 Mon Sep 17 00:00:00 2001
From: David Michon <dmichon@microsoft.com>
Date: Wed, 8 Jan 2025 23:17:28 +0000
Subject: [PATCH 1/2] feat: Use single-linked list for resolver stack

---
 lib/Resolver.js | 117 +++++++++++++++++++++++++++++++++++-------------
 types.d.ts      |  12 ++++-
 2 files changed, 97 insertions(+), 32 deletions(-)

diff --git a/lib/Resolver.js b/lib/Resolver.js
index fdb73dc..6315663 100644
--- a/lib/Resolver.js
+++ b/lib/Resolver.js
@@ -306,8 +306,16 @@ const {
 /** @typedef {BaseResolveRequest & Partial<ParsedIdentifier>} ResolveRequest */
 
 /**
- * String with special formatting
- * @typedef {string} StackEntry
+ * Single-linked-list entry for the stack
+ * @typedef {Object} StackEntry
+ * @property {string | undefined} name
+ * @property {string | false} path
+ * @property {string} request
+ * @property {string} query
+ * @property {string} fragment
+ * @property {boolean} directory
+ * @property {boolean} module
+ * @property {StackEntry | undefined} parent
  */
 
 /**
@@ -323,7 +331,7 @@ const {
  * @property {WriteOnlySet<string>=} contextDependencies
  * @property {WriteOnlySet<string>=} fileDependencies files that was found on file system
  * @property {WriteOnlySet<string>=} missingDependencies dependencies that was not found on file system
- * @property {Set<StackEntry>=} stack set of hooks' calls. For instance, `resolve → parsedResolve → describedResolve`,
+ * @property {StackEntry=} stack set of hooks' calls. For instance, `resolve → parsedResolve → describedResolve`,
  * @property {(function(string): void)=} log log function
  * @property {ResolveContextYield=} yield yield result, if provided plugins can return several results
  */
@@ -350,24 +358,80 @@ function toCamelCase(str) {
 	return str.replace(/-([a-z])/g, str => str.slice(1).toUpperCase());
 }
 
+/**
+ * @param {StackEntry | undefined} stack The tip of the existing stack
+ * @param {StackEntry} query The entry to look for
+ * @returns {boolean} If the stack contains the specified entry already
+ */
+function hasStackEntry(stack, query) {
+	while (stack) {
+		if (
+			stack.name === query.name &&
+			stack.path === query.path &&
+			stack.request === query.request &&
+			stack.query === query.query &&
+			stack.fragment === query.fragment &&
+			stack.directory === query.directory &&
+			stack.module === query.module
+		) {
+			return true;
+		}
+		stack = stack.parent;
+	}
+	return false;
+}
+
+/**
+ * @param {StackEntry} entry The stack entry to format
+ * @returns {string} A formatted string representing the entry
+ */
+function formatStackEntry(entry) {
+	return (
+		entry.name +
+		": (" +
+		entry.path +
+		") " +
+		(entry.request || "") +
+		(entry.query || "") +
+		(entry.fragment || "") +
+		(entry.directory ? " directory" : "") +
+		(entry.module ? " module" : "")
+	);
+}
+
+/**
+ * @param {StackEntry} stack The tip of the stack to format
+ * @returns {string} The formatted stack
+ */
+function formatStack(stack) {
+	/** @type {StackEntry | undefined} */
+	let entry = stack;
+	let formatted = "";
+	while (entry) {
+		formatted = "\n  " + formatStackEntry(entry) + formatted;
+		entry = stack.parent;
+	}
+	return formatted;
+}
+
 class Resolver {
 	/**
 	 * @param {ResolveStepHook} hook hook
 	 * @param {ResolveRequest} request request
+	 * @param {StackEntry} [parent] parent stack entry
 	 * @returns {StackEntry} stack entry
 	 */
-	static createStackEntry(hook, request) {
-		return (
-			hook.name +
-			": (" +
-			request.path +
-			") " +
-			(request.request || "") +
-			(request.query || "") +
-			(request.fragment || "") +
-			(request.directory ? " directory" : "") +
-			(request.module ? " module" : "")
-		);
+	static createStackEntry(hook, request, parent) {
+		return {
+			name: hook.name,
+			path: request.path,
+			request: request.request || "",
+			query: request.query || "",
+			fragment: request.fragment || "",
+			directory: !!request.directory,
+			module: !!request.module,
+			parent
+		};
 	}
 
 	/**
@@ -670,33 +734,24 @@ class Resolver {
 	 * @returns {void}
 	 */
 	doResolve(hook, request, message, resolveContext, callback) {
-		const stackEntry = Resolver.createStackEntry(hook, request);
+		const parent = resolveContext.stack;
+		// Add a singly-linked list node to the stack
+		const stackEntry = Resolver.createStackEntry(hook, request, parent);
 
-		/** @type {Set<string> | undefined} */
-		let newStack;
-		if (resolveContext.stack) {
-			newStack = new Set(resolveContext.stack);
-			if (resolveContext.stack.has(stackEntry)) {
+		if (parent) {
+			if (hasStackEntry(parent, stackEntry)) {
 				/**
 				 * Prevent recursion
 				 * @type {Error & {recursion?: boolean}}
 				 */
 				const recursionError = new Error(
-					"Recursion in resolving\nStack:\n  " +
-						Array.from(newStack).join("\n  ")
+					"Recursion in resolving\nStack:" + formatStack(stackEntry)
 				);
 				recursionError.recursion = true;
 				if (resolveContext.log)
 					resolveContext.log("abort resolving because of recursion");
 				return callback(recursionError);
 			}
-			newStack.add(stackEntry);
-		} else {
-			// creating a set with new Set([item])
-			// allocates a new array that has to be garbage collected
-			// this is an EXTREMELY hot path, so let's avoid it
-			newStack = new Set();
-			newStack.add(stackEntry);
 		}
 		this.hooks.resolveStep.call(hook, request);
 
@@ -708,7 +763,7 @@ class Resolver {
 					fileDependencies: resolveContext.fileDependencies,
 					contextDependencies: resolveContext.contextDependencies,
 					missingDependencies: resolveContext.missingDependencies,
-					stack: newStack
+					stack: stackEntry
 				},
 				message
 			);
diff --git a/types.d.ts b/types.d.ts
index fc09289..8b77196 100644
--- a/types.d.ts
+++ b/types.d.ts
@@ -685,7 +685,7 @@ declare interface ResolveContext {
 	/**
 	 * set of hooks' calls. For instance, `resolve → parsedResolve → describedResolve`,
 	 */
-	stack?: Set<string>;
+	stack?: StackEntry;
 
 	/**
 	 * log function
@@ -982,6 +982,16 @@ declare abstract class Resolver {
 	join(path: string, request: string): string;
 	normalize(path: string): string;
 }
+declare interface StackEntry {
+	name?: string;
+	path: string | false;
+	request: string;
+	query: string;
+	fragment: string;
+	directory: boolean;
+	module: boolean;
+	parent?: StackEntry;
+}
 declare interface Stat {
 	(
 		path: PathLike,

From 8bd21dd7b0f961ede4c8972f15a26830621dfd00 Mon Sep 17 00:00:00 2001
From: David Michon <dmichon@microsoft.com>
Date: Mon, 13 Jan 2025 21:46:21 +0000
Subject: [PATCH 2/2] fix: infinite loop

---
 lib/Resolver.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/Resolver.js b/lib/Resolver.js
index 6315663..b78cbd6 100644
--- a/lib/Resolver.js
+++ b/lib/Resolver.js
@@ -409,7 +409,7 @@ function formatStack(stack) {
 	let formatted = "";
 	while (entry) {
 		formatted = "\n  " + formatStackEntry(entry) + formatted;
-		entry = stack.parent;
+		entry = entry.parent;
 	}
 	return formatted;
 }