CJS hook re-evaluates ESM dependencies on require(), creating duplicate module instances (Node ≥22.12)
Acknowledgements
- I read the documentation and searched existing issues to avoid duplicates
- I understand this is a bug tracker and anything other than a proven bug will be closed
- I understand this is a free project and relies on community contributions
- I read and understood the Contribution guide
Minimal reproduction URL
https://github.com/DanTsk/tsx-require-esm-dual-instance
Problem & expected behavior (under 200 words)
What happens. On Node ≥ 22.12, when a CommonJS dependency does require("graphql") (graphql 17 ships .mjs via exports), tsx's CJS hook transpiles graphql/index.mjs to CJS with esbuild and module._compile()s it — recursively, for the whole package (168 files land in require.cache). The result is a second graphql instance, distinct from the one import "graphql" produced. graphql brands schemas with a per-instance Symbol('Schema'), so isSchema() from one copy rejects objects from the other.
Real-world form: mercurius (CJS) + graphql@17 + @graphql-tools/schema → startup fails with Expected {…} to be a GraphQL schema. on a perfectly valid schema.
What I expected. Plain node already handles this: native require(esm) (loadESMFromCJS) goes through the ESM module map and returns the same instance (import === require → true). node --import tsx/esm also behaves correctly — only the CJS hook diverges.
Cause. module-extensions.ts transforms any ESM-syntax file unconditionally. shouldApplyRequireEsmInterop and isRequireEsmCandidate exist but only pick an error code (L214–223) and emulate module.exports interop post-hoc (L245–258) — they never defer to Node.
Proposed fix (verified against the real app via pnpm patch): after isEsmSyntax,
if (shouldApplyRequireEsmInterop && isEsmSyntax && isRequireEsmCandidate(cleanFilePath)) {
return defaultLoader(module, cleanFilePath);
}TS files are unaffected (isEsmSyntax is .js/.mjs-only); Node < 22.12 unchanged. Full analysis and CI matrix (Node 22 & 24) in the repo README.
Bugs are expected to be fixed by those affected by it
- I'm interested in working on this issue
Compensating engineering work will speed up resolution and support the project
- I'm willing to offer $10 for financial support
Source: privatenumber/tsx