From 6b951458405a806aa5a6bed4f585fc84e6d5feb5 Mon Sep 17 00:00:00 2001 From: David Michon 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} 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=} contextDependencies * @property {WriteOnlySet=} fileDependencies files that was found on file system * @property {WriteOnlySet=} missingDependencies dependencies that was not found on file system - * @property {Set=} 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 | 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; + 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 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; }