Back to Tsx

Node integration

notes/tsx/node-integration.md

4.23.126.6 KB
Original Source

Node integration

How tsx integrates with Node. The underlying Node evidence is owned by notes/node.

Feature gates

The current gates are defined in src/utils/node-features.ts.

tsx featureNode reference
moduleRegisterAsync module hooks
moduleRegisterHooksCjsReloadCJS reload support
esmLoadReadFileCJS source from load hooks
importAttributesImport attributes
importMetaPathPropertiesimport.meta path properties
requireEsmCJS requiring ESM
requireEsmNoWarningCJS requiring ESM
cjsNamespaceModuleExportsCJS synthetic namespaces
cjsNamespaceFromLoadHookCJS source from load hooks
requireEsmExtensionlessMjsCJS requiring ESM
wasmModulesWASM modules
modulePackageMainResolutionPackage main assertion fix
cliTestFlag and testRunnerGlobTest runner
nativeTypeScriptNative type stripping

Module hooks

tsx uses the async module.register() path below the CJS-reload-safe boundary and sync module.registerHooks() at or above it (selection, registration). The sync path requires the CJS reload handshake, not only the existence of registerHooks().

Node v22.22.3 is included in this gate through the reload-handshake backport (gate). Gate changes must run the CJS interop and watch suites at each newly supported boundary.

The async worker path adds measurable fixed startup cost. In the hooks-passthrough benchmark with 1000 modules on an M5 Pro, Node 24.10.0 async hooks took 200ms and Node 24.15.0 sync hooks took 145ms. The benchmark documentation is in scripts/benchmark/README.md.

CommonJS loader integration

tsx patches Module._resolveFilename, registers TypeScript extension handlers, and preserves cache identity across transformed data URLs and internal queries (registration). The integration points are:

Candidate hygiene avoids resolution candidates that cannot win before calling Node (ESM resolver). This reduces the cost of Node's eager CJS hint decoration while retaining Node as the final authority for package exports, symlinks, and format detection. The resolver-policy owner is module-resolution.md.

CJS/ESM interop

tsx transforms TypeScript before Node's CJS preparse stage so Node constructs the namespace from JavaScript source (load hook). parentImportsCommonJsExports coordinates resolve and load hooks when a parent needs CJS named or namespace exports.

For native require(esm) interop, tsx distinguishes accessor descriptors emitted for ESM exports from ordinary CJS data descriptors before honoring a module.exports escape hatch (extension handler). This preserves ordinary CJS objects that contain a literal module.exports key.

When esbuild rejects top-level await for CommonJS output, the extension handler recognizes the diagnostic only for module-system candidates that native require(esm) could load. It preserves esbuild's TransformError name, message, and source location, and adds Node's loader-facing ERR_REQUIRE_ESM or ERR_REQUIRE_ASYNC_MODULE code so callers can fall back to import() (Node contract). The shared transformer remains context-neutral, so this mapping does not change ESM loader diagnostics.

Native TypeScript integration

When Node provides module-typescript or commonjs-typescript, tsx preserves Node's module classification (load hook). When Node provides no format, tsx uses its compatibility resolver and legacy CommonJS default.

tsx remains responsible for transformation syntax Node does not support, extension and path behavior Node leaves to tools, and transformed CJS/ESM interop. Node's type-stripping limits are documented in notes/node/type-stripping.md.

Re-verification map

When touching an integration boundary, confirm the matching Node mechanism and run a behavior-level test:

Integration pointtsx siteNode mechanism
CJS preparse call stackis-from-cjs-lexer.tsCJS preparse
CJS lexer grammarsrc/esm/hook/load.tsCJS preparse
CJS cache identityinterop-cjs-exports.ts, preserve-query.tsCJS cache
Extension dispatchmodule-extensions.tsExtension handlers
Sync resolve/load hookssrc/esm/api/register.ts, src/esm/hook/Module hooks
CJS reload handshakesrc/esm/hook/load.tsCJS reload support