Summary
Perry's native-module namespace objects (NATIVE_MODULE_CLASS_ID) do not enumerate their exports:
const crypto = require("crypto");
Object.keys(crypto).length; // node: 70 perry: 1
let n = 0; for (const k in crypto) n++; // node: 70 perry: 0
({ ...crypto }).createHmac; // node: [Function] perry: undefined
Every interop layer that builds a namespace by enumeration therefore produces an empty namespace: turbopack's ESM-import interop over an externalized builtin (e.i(...) on a.x("crypto", () => require("crypto"))), Babel's interopRequireWildcard, and plain object spread.
Production impact (how this was found): Auth.js credentials login on a Next.js 16 App Router standalone fails with CallbackRouteError: TypeError: value is not a function on the SUCCESS path only. jose's compiled HKDF does Z = e.i(<crypto>), probes typeof Z.hkdf (undefined → picks its pure-JS fallback), and the fallback calls (0, Z.createHmac)(...) — also undefined through the empty interop namespace → the session-token derivation throws, so every successful password check still 302s to error=Configuration. Failed logins work (never reach HKDF), which masked this until a real credentials round-trip.
Z.default.randomBytes style access (interop default member) works — only named enumeration/copy is missing.
Suggested direction
Give enumeration surfaces (Object.keys/getOwnPropertyNames, for-in, object spread/Object.assign copy) a native-module arm that synthesizes the export-name list per module and, for copies, materializes bound_native_callable_export_value(module, name) per key. A partial inventory already exists as the (module, method) → arity tables in object/native_module/callable_exports.rs; a fuller per-module export inventory (constants/objects included) would close the node gap (crypto = 70 keys in Node 22).
Repro: the snippet above; observed on main @ 5eb2bc4, macOS arm64.
Summary
Perry's native-module namespace objects (
NATIVE_MODULE_CLASS_ID) do not enumerate their exports:Every interop layer that builds a namespace by enumeration therefore produces an empty namespace: turbopack's ESM-import interop over an externalized builtin (
e.i(...)ona.x("crypto", () => require("crypto"))), Babel'sinteropRequireWildcard, and plain object spread.Production impact (how this was found): Auth.js credentials login on a Next.js 16 App Router standalone fails with
CallbackRouteError: TypeError: value is not a functionon the SUCCESS path only. jose's compiled HKDF doesZ = e.i(<crypto>), probestypeof Z.hkdf(undefined → picks its pure-JS fallback), and the fallback calls(0, Z.createHmac)(...)— also undefined through the empty interop namespace → the session-token derivation throws, so every successful password check still 302s toerror=Configuration. Failed logins work (never reach HKDF), which masked this until a real credentials round-trip.Z.default.randomBytesstyle access (interopdefaultmember) works — only named enumeration/copy is missing.Suggested direction
Give enumeration surfaces (
Object.keys/getOwnPropertyNames, for-in, object spread/Object.assigncopy) a native-module arm that synthesizes the export-name list per module and, for copies, materializesbound_native_callable_export_value(module, name)per key. A partial inventory already exists as the(module, method) → aritytables inobject/native_module/callable_exports.rs; a fuller per-module export inventory (constants/objects included) would close the node gap (crypto= 70 keys in Node 22).Repro: the snippet above; observed on main @ 5eb2bc4, macOS arm64.