@@ -370,6 +370,19 @@ const _pathCacheByFs = new WeakMap();
370370 * @typedef {string } StackEntry
371371 */
372372
373+ /**
374+ * Structured representation of a stack entry used internally for
375+ * structural equality checks without allocating a formatted string.
376+ * @typedef {object } RawStackEntry
377+ * @property {string | undefined } name hook name
378+ * @property {string | false } path request path
379+ * @property {string } request request string
380+ * @property {string } query query string
381+ * @property {string } fragment fragment string
382+ * @property {boolean } directory directory flag
383+ * @property {boolean } module module flag
384+ */
385+
373386/**
374387 * @template T
375388 * @typedef {{ add: (item: T) => void } } WriteOnlySet
@@ -410,6 +423,166 @@ function toCamelCase(str) {
410423 return str . replace ( / - ( [ a - z ] ) / g, ( str ) => str . slice ( 1 ) . toUpperCase ( ) ) ;
411424}
412425
426+ /**
427+ * Formats a raw stack entry to its string representation.
428+ * @param {RawStackEntry } entry raw entry
429+ * @returns {string } formatted stack entry string
430+ */
431+ function formatStackEntry ( entry ) {
432+ return `${ entry . name } : (${ entry . path } ) ${ entry . request } ${ entry . query } ${
433+ entry . fragment
434+ } ${ entry . directory ? " directory" : "" } ${ entry . module ? " module" : "" } `;
435+ }
436+
437+ /**
438+ * Structural equality check between two raw stack entries.
439+ * Avoids formatting to a string on the hot recursion-check path.
440+ * @param {RawStackEntry } a first entry
441+ * @param {RawStackEntry } b second entry
442+ * @returns {boolean } whether the entries are structurally equal
443+ */
444+ function rawStackEntriesEqual ( a , b ) {
445+ return (
446+ a . name === b . name &&
447+ a . path === b . path &&
448+ a . request === b . request &&
449+ a . query === b . query &&
450+ a . fragment === b . fragment &&
451+ a . directory === b . directory &&
452+ a . module === b . module
453+ ) ;
454+ }
455+
456+ /**
457+ * A Set<string> that is backed by a singly-linked list of structured
458+ * stack entries. Extending the stack with a new entry is O(1) in time
459+ * and memory (instead of O(n) for cloning a `Set`), and formatted
460+ * entry strings are computed lazily — only when the set is iterated,
461+ * queried with `has`, or its `size` is read.
462+ *
463+ * The public `Set<string>` contract is preserved for backwards
464+ * compatibility so that consumers like webpack's `ResolverCachePlugin`
465+ * can continue to read `resolveContext.stack` as a `Set<string>`.
466+ * @extends {Set<string> }
467+ */
468+ class StackSet extends Set {
469+ /**
470+ * @param {Set<string> | undefined } parent previous stack (or undefined)
471+ * @param {RawStackEntry } entry raw entry at the tip of this stack
472+ */
473+ constructor ( parent , entry ) {
474+ super ( ) ;
475+ /** @type {Set<string> | undefined } */
476+ this . _parent = parent ;
477+ /** @type {RawStackEntry } */
478+ this . _entry = entry ;
479+ /** @type {string | undefined } */
480+ this . _string = undefined ;
481+ }
482+
483+ /**
484+ * @returns {string } the formatted string for this tip entry (memoized)
485+ */
486+ _getString ( ) {
487+ if ( this . _string === undefined ) {
488+ this . _string = formatStackEntry ( this . _entry ) ;
489+ }
490+ return this . _string ;
491+ }
492+
493+ /**
494+ * Fast structural recursion check without allocating any strings.
495+ * @param {RawStackEntry } query raw entry to look for
496+ * @returns {boolean } whether the entry exists in this stack
497+ */
498+ _hasRaw ( query ) {
499+ if ( rawStackEntriesEqual ( this . _entry , query ) ) return true ;
500+ const parent = this . _parent ;
501+ if ( ! parent ) return false ;
502+ if ( parent instanceof StackSet ) return parent . _hasRaw ( query ) ;
503+ // Fallback for user-provided plain `Set<string>`: pay the string
504+ // formatting cost once to query it.
505+ return parent . has ( formatStackEntry ( query ) ) ;
506+ }
507+
508+ /**
509+ * @param {string } value value to check
510+ * @returns {boolean } whether the stack contains the value
511+ */
512+ has ( value ) {
513+ if ( this . _getString ( ) === value ) return true ;
514+ return this . _parent ? this . _parent . has ( value ) : false ;
515+ }
516+
517+ /**
518+ * @returns {number } number of entries in the stack
519+ */
520+ get size ( ) {
521+ return 1 + ( this . _parent ? this . _parent . size : 0 ) ;
522+ }
523+
524+ /**
525+ * Walks from the oldest (bottom of stack) to the newest (tip).
526+ * @returns {SetIterator<string> } iterator of formatted stack entries
527+ */
528+ [ Symbol . iterator ] ( ) {
529+ return /** @type {SetIterator<string> } */ (
530+ // eslint-disable-next-line no-use-before-define
531+ /** @type {unknown } */ ( iterateStackSet ( this ) )
532+ ) ;
533+ }
534+
535+ /**
536+ * @returns {SetIterator<string> } same as [Symbol.iterator]
537+ */
538+ keys ( ) {
539+ return this [ Symbol . iterator ] ( ) ;
540+ }
541+
542+ /**
543+ * @returns {SetIterator<string> } same as [Symbol.iterator]
544+ */
545+ values ( ) {
546+ return this [ Symbol . iterator ] ( ) ;
547+ }
548+
549+ /**
550+ * @returns {SetIterator<[string, string]> } iterator yielding [v, v]
551+ */
552+ entries ( ) {
553+ return /** @type {SetIterator<[string, string]> } */ (
554+ // eslint-disable-next-line no-use-before-define
555+ /** @type {unknown } */ ( iterateStackSetEntries ( this ) )
556+ ) ;
557+ }
558+
559+ /**
560+ * @param {(value: string, value2: string, set: Set<string>) => void } callback callback
561+ * @param {unknown= } thisArg this argument
562+ * @returns {void }
563+ */
564+ forEach ( callback , thisArg ) {
565+ for ( const v of this ) callback . call ( thisArg , v , v , this ) ;
566+ }
567+ }
568+
569+ /**
570+ * @param {StackSet } set the set to iterate
571+ * @returns {Generator<string> } generator yielding formatted entries
572+ */
573+ function * iterateStackSet ( set ) {
574+ if ( set . _parent ) yield * set . _parent ;
575+ yield set . _getString ( ) ;
576+ }
577+
578+ /**
579+ * @param {StackSet } set the set to iterate
580+ * @returns {Generator<[string, string]> } generator yielding [v, v] tuples
581+ */
582+ function * iterateStackSetEntries ( set ) {
583+ for ( const v of set ) yield [ v , v ] ;
584+ }
585+
413586class Resolver {
414587 /**
415588 * @param {ResolveStepHook } hook hook
@@ -739,33 +912,48 @@ class Resolver {
739912 * @returns {void }
740913 */
741914 doResolve ( hook , request , message , resolveContext , callback ) {
742- const stackEntry = Resolver . createStackEntry ( hook , request ) ;
915+ const parent = resolveContext . stack ;
916+
917+ /** @type {RawStackEntry } */
918+ const rawEntry = {
919+ name : hook . name ,
920+ path : request . path ,
921+ request : request . request || "" ,
922+ query : request . query || "" ,
923+ fragment : request . fragment || "" ,
924+ directory : Boolean ( request . directory ) ,
925+ module : Boolean ( request . module ) ,
926+ } ;
743927
744- /** @type {Set<string> | undefined } */
745- let newStack ;
746- if ( resolveContext . stack ) {
747- newStack = new Set ( resolveContext . stack ) ;
748- if ( resolveContext . stack . has ( stackEntry ) ) {
749- /**
750- * Prevent recursion
751- * @type {Error & { recursion?: boolean } }
752- */
753- const recursionError = new Error (
754- `Recursion in resolving\nStack:\n ${ [ ...newStack ] . join ( "\n " ) } ` ,
755- ) ;
756- recursionError . recursion = true ;
757- if ( resolveContext . log ) {
758- resolveContext . log ( "abort resolving because of recursion" ) ;
759- }
760- return callback ( recursionError ) ;
928+ // Fast O(n) structural recursion check without allocating strings.
929+ // If the caller supplied a plain `Set<string>` rather than a
930+ // `StackSet`, fall back to the string-based `has` check.
931+ let recursion = false ;
932+ if ( parent !== undefined ) {
933+ recursion =
934+ parent instanceof StackSet
935+ ? parent . _hasRaw ( rawEntry )
936+ : parent . has ( formatStackEntry ( rawEntry ) ) ;
937+ }
938+
939+ // O(1) extension of the stack via a linked-list node that lazily
940+ // formats its string representation. Preserves the `Set<string>`
941+ // public contract.
942+ const newStack = new StackSet ( parent , rawEntry ) ;
943+
944+ if ( recursion ) {
945+ /**
946+ * Prevent recursion
947+ * @type {Error & { recursion?: boolean } }
948+ */
949+ const recursionError = new Error (
950+ `Recursion in resolving\nStack:\n ${ [ ...newStack ] . join ( "\n " ) } ` ,
951+ ) ;
952+ recursionError . recursion = true ;
953+ if ( resolveContext . log ) {
954+ resolveContext . log ( "abort resolving because of recursion" ) ;
761955 }
762- newStack . add ( stackEntry ) ;
763- } else {
764- // creating a set with new Set([item])
765- // allocates a new array that has to be garbage collected
766- // this is an EXTREMELY hot path, so let's avoid it
767- newStack = new Set ( ) ;
768- newStack . add ( stackEntry ) ;
956+ return callback ( recursionError ) ;
769957 }
770958 this . hooks . resolveStep . call ( hook , request ) ;
771959
0 commit comments