Repository navigation
perf(ext/node): convert node:util, node:assert to lazy-loaded - #33876
Conversation
Move internal/util/parse_args/parse_args.js from esm to lazy_loaded_js. Replace `import process` with createLazyLoader. Update util.ts to use core.loadExtScript instead of import.
Move the node:util implementation into util.ts (lazy_loaded_js IIFE).
Add util_esm.ts as the thin ESM wrapper in lazy_loaded_esm.
Move node:util/types to lazy_loaded_esm (already a thin wrapper).
Replace all `import ... from "node:util"` in ext/node with
core.loadExtScript("ext:deno_node/util.ts") so node:util is fully
out of the snapshot.
Move the node:assert implementation into assert.ts (lazy_loaded_js
IIFE). Add assert_esm.ts as the thin ESM wrapper in lazy_loaded_esm.
Move node:assert/strict to lazy_loaded_esm as well.
Replace all `import ... from "node:assert"` in ext/node with
core.loadExtScript("ext:deno_node/assert.ts").
Remove lazyTypes/createLazyLoader("node:util/types") from util.ts and
use core.loadExtScript("ext:deno_node/internal/util/types.ts") directly.
This ensures util.types, require("util/types"), and import from
"node:util/types" all return the same object (reference equality).
Also fix util/types.ts default export to not spread-copy.
fibibot
left a comment
There was a problem hiding this comment.
Mechanical scope is well-handled — 35 files updated, the 15 import ... from "node:util" and 16 import ... from "node:assert" callsites in ext/node/ consistently swapped to core.loadExtScript(), and the lazy_loaded_esm wrapper structure mirrors the established pattern.
Heads-up on the process.getBuiltinModule identity contract — this is the same surface that broke #33875 (which I REQUEST_CHANGES'd minutes ago). The test plan correctly verifies node:sys === node:util for CJS, but that's the CJS-CJS identity. The contract that bit #33875 was CJS-vs-ESM:
// parallel/test-process-get-builtin.mjs:57
assert.strictEqual(process.getBuiltinModule(id), imported.default);Walking through the new util.ts IIFE shape:
const defaultExport = { format, formatWithOptions, ... }; // object literal A
return { callbackify, debuglog, ..., default: defaultExport }; // object literal B (B.default = A)And the consumer paths:
- CJS
process.getBuiltinModule("util")→loadNativeModulereturnsnodeMod.exports = nativeModuleExports.util= the IIFE's outer return = B ({ callbackify, ..., default: A }). - ESM
(await import("node:util")).default→util_esm.ts'sexport default mod.default= A ({ format, ... }).
B and A are different objects. CI is still pending (12 builds + others), but if test-process-get-builtin.mjs runs at HEAD I'd expect the same node_compat (3/3) failure as #33875.
Two ways to make the test pass
-
Make the CJS path return
mod.default— change01_require.js'sconst util = ...to usemod.defaultinstead ofmod. Cleanest fix: keeps the require system aligned with ESM'sdefaultsemantics. -
Make the IIFE's outer return the same object as
defaultExport— restructureutil.tssodefaultExportis the return, thenB === A. Requires putting all named exports onto the same object that's referenced bydefault, which is roughly Node's actual shape (utilandutil.defaultboth point at the same module-exports object).
Option 2 is the more principled fix and matches Node's behavior better. It's also what the original (pre-conversion) util.ts likely did.
CI
54 success, 12 pending, 0 fail at this HEAD (CI just started). Will re-confirm — flagging now since the bug pattern matches #33875 and you can probably catch it before the test-process-get-builtin shard reports.
- Remove the `default: defaultExport` property from util.ts CJS exports so `loadExtScript` doesn't leak a `default` key into the CJS module - Update sys_esm.js to re-export from node:util instead of using the ext: specifier which can't resolve between lazy-loaded ESM modules - Update util_esm.ts default export to use `mod` directly since `default` no longer exists on the loaded script
fibibot
left a comment
There was a problem hiding this comment.
LGTM — f532e702 (fix: remove defaultExport from util.ts and fix sys_esm.js for lazy ESM) takes option (2) from my prior comment exactly: restructured so the IIFE's outer return is the default object instead of building a separate defaultExport literal that was a different identity.
What I verified
util.ts: removed the 26-line defaultExport = { ... } literal entirely, and removed default: defaultExport from the IIFE's return. The IIFE now returns one single object containing all the named exports. ✓
util_esm.ts: export default mod.default → export default mod. Now the ESM-side default export is the same object the CJS side sees via core.loadExtScript("ext:deno_node/util.ts"). ✓
sys_esm.js: re-exports from node:util directly (was util_esm.ts). Cleaner — node:sys is a thin alias and Node's identity contract require("sys") === require("util") flows through the public alias rather than going through both lazy_loaded_esm wrappers. ✓
This means process.getBuiltinModule("util") === (await import("node:util")).default should now hold — the test that broke #33875 with the same pattern. CI confirms: 129 success, 4 pending, 0 fail. The previously-failing test-process-get-builtin.mjs is in the green set.
The 4 pending are slow test specs / wpt release linux-x86_64 shards — they don't exercise the identity contract. Substance is unambiguously correct.
Good follow-through on the design pattern after the #33875 / #33873 iterations clarified what default semantics need to look like for the lazy_loaded_esm <-> CJS bridge.
Continues the lazy-loading conversion started in #33876 and #33881. - Convert 6 node:* modules from eagerly-loaded ESM to lazy-loaded IIFE scripts with thin ESM wrappers: `node:wasi`, `node:trace_events`, `node:punycode`, `node:domain`, `node:perf_hooks`, `node:vm` - Update import sites in `01_require.js`, `http2.ts`, and `repl.ts` to use `core.loadExtScript()` instead of ESM imports - Add `trace_events` to `DENO_SUPPORTED_BUILTIN_NODE_MODULES` (was previously missing from the builtin list)
Continues the lazy-loading conversion started in #33876, #33881, and #33882. - Convert 3 `node:*` modules from eagerly-loaded ESM to lazy-loaded IIFE scripts with thin ESM wrappers: `node:querystring`, `node:async_hooks`, `node:diagnostics_channel` - Update import sites in `01_require.js`, `url.ts`, `net.ts`, `http2.ts`, `dgram.ts`, `_http_agent.js`, `_http_client.js`, `_http_server.js`, and `internal_binding/http_parser.ts` to use `core.loadExtScript()` instead of ESM imports - Also fixes stale `node:perf_hooks` ESM imports in `_http_client.js` and `_http_server.js` that were missed in #33882
…nd#33876) Convert `node:util`, `node:util/types`, `node:assert`, and `node:assert/strict` out of the snapshot using the lazy-loaded pattern:
) Continues the lazy-loading conversion started in denoland#33876 and denoland#33881. - Convert 6 node:* modules from eagerly-loaded ESM to lazy-loaded IIFE scripts with thin ESM wrappers: `node:wasi`, `node:trace_events`, `node:punycode`, `node:domain`, `node:perf_hooks`, `node:vm` - Update import sites in `01_require.js`, `http2.ts`, and `repl.ts` to use `core.loadExtScript()` instead of ESM imports - Add `trace_events` to `DENO_SUPPORTED_BUILTIN_NODE_MODULES` (was previously missing from the builtin list)
) Continues the lazy-loading conversion started in denoland#33876, denoland#33881, and denoland#33882. - Convert 3 `node:*` modules from eagerly-loaded ESM to lazy-loaded IIFE scripts with thin ESM wrappers: `node:querystring`, `node:async_hooks`, `node:diagnostics_channel` - Update import sites in `01_require.js`, `url.ts`, `net.ts`, `http2.ts`, `dgram.ts`, `_http_agent.js`, `_http_client.js`, `_http_server.js`, and `internal_binding/http_parser.ts` to use `core.loadExtScript()` instead of ESM imports - Also fixes stale `node:perf_hooks` ESM imports in `_http_client.js` and `_http_server.js` that were missed in denoland#33882
) Continues the lazy-loading conversion started in denoland#33876, denoland#33881, denoland#33882, and denoland#33897.
) Continues the lazy-loading conversion started in denoland#33876, denoland#33881, denoland#33882, denoland#33897, and denoland#33900.
Summary
Convert
node:util,node:util/types,node:assert, andnode:assert/strictout of the snapshot using the lazy-loaded pattern:node:util:
util.ts(lazy_loaded_js IIFE)util_esm.tsin lazy_loaded_esmimport ... from "node:util"in ext/node replaced withcore.loadExtScript("ext:deno_node/util.ts")node:util/types:
core.loadExtScript-- moved from esm to lazy_loaded_esmnode:assert:
assert.ts(lazy_loaded_js IIFE)assert_esm.tsin lazy_loaded_esmimport ... from "node:assert"in ext/node replaced withcore.loadExtScript("ext:deno_node/assert.ts")node:assert/strict:
core.loadExtScriptinstead ofimport from "node:assert"Also converts
internal/util/parse_args/parse_args.jsfrom esm to lazy_loaded_js.Test plan
node:sys === node:utilidentity preserved for CJSnode:assert/strictworks