Skip to content

fix(core): hash Map, Set and RegExp options in deep-hash tokens - #17945

Merged
kamilmysliwiec merged 3 commits into
nestjs:masterfrom
Chaoqi31:fix/deep-hash-module-tokens
Sep 30, 2026
Merged

kamilmysliwiec merged 3 commits into
nestjs:masterfrom
Chaoqi31:fix/deep-hash-module-tokens

Conversation

@Chaoqi31

Copy link
Copy Markdown
Contributor

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Other... Please describe:

What is the current behavior?

With moduleIdGeneratorAlgorithm: 'deep-hash', DeepHashedModuleOpaqueKeyFactory.createForDynamic() hashes the serialized dynamic metadata. The serializer, fast-safe-stringify, follows JSON.stringify semantics, so a Map, Set or RegExp anywhere in the metadata becomes {}. Two imports of the same dynamic module whose options differ only there get the same token, for example options holding new Map([['a', 1]]) and new Map([['b', 2]]), new Set(['x']) and new Set(['y']), or /^\/api/ and /^\/admin/.

NestContainer.addModule() then finds the token, returns the first module with inserted: false, and the second import's providers are dropped, so its consumers get the first import's options. The option's JSDoc says the id "is generated based on the serialized module definition", and nothing in the specs pins the collisions.

The new spec against the current factory:

 × should serialize maps, sets and regular expressions in a dynamic metadata object
Expected: "{"providers":[{"provide":"Options","useValue":{"map":{"Map":[["a",1]]},"set":{"Set":["b"]},"pattern":{"RegExp":"/c/i"}}}]}"
Received: "{"providers":[{"provide":"Options","useValue":{"map":{},"set":{},"pattern":{}}}]}"

Issue Number: N/A

What is the new behavior?

replacer serializes a Map as { Map: [...entries] }, a Set as { Set: [...values] } and a RegExp as { RegExp: value.toString() }. The tag keeps them apart from a plain array or string with the same content; a plain object literally shaped { Map: [...] }, { Set: [...] } or { RegExp: '...' } still gets the same token as the real value. Every other value serializes as before, so tokens of modules without these values do not change, and identical options still share one module. The default reference algorithm is not affected.

One limit: fast-safe-stringify does not look inside a Map or Set when it removes circular references, so metadata with a cycle through one of them now serializes to its fallback string ([unable to serialize, circular reference is too complex to analyze]) instead of {}. That string carries no module id or name, so every dynamic module with such metadata gets the same token. BigInt values already reach the same fallback.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

npx vitest run packages/core: 1395 passed.

Chaoqi31 and others added 3 commits September 30, 2026 18:50
With `moduleIdGeneratorAlgorithm: 'deep-hash'`, a dynamic module's
token is the hash of its serialized metadata. `fast-safe-stringify`
follows JSON semantics, so a `Map`, `Set` or `RegExp` in the options
became `{}`, and two imports of the same dynamic module that differed
only there got one token. The container kept the first module and
silently dropped the second import's providers.

Serialize them by their entries, values and source, tagged so they do
not collide with plain values that hold the same data.
@kamilmysliwiec
kamilmysliwiec merged commit 9e49fe4 into nestjs:master Sep 30, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants