Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: single-linked list for resolver stack #443

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 86 additions & 31 deletions lib/Resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/

/**
Expand All @@ -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
*/
Expand All @@ -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 = entry.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
};
}

/**
Expand Down Expand Up @@ -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);

Expand All @@ -708,7 +763,7 @@ class Resolver {
fileDependencies: resolveContext.fileDependencies,
contextDependencies: resolveContext.contextDependencies,
missingDependencies: resolveContext.missingDependencies,
stack: newStack
stack: stackEntry
},
message
);
Expand Down
12 changes: 11 additions & 1 deletion types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ declare interface ResolveContext {
/**
* set of hooks' calls. For instance, `resolve → parsedResolve → describedResolve`,
*/
stack?: Set<string>;
stack?: StackEntry;

/**
* log function
Expand Down Expand Up @@ -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,
Expand Down