Skip to content

perf(ext/node): convert node:util, node:assert to lazy-loaded - #33876

Merged
bartlomieju merged 5 commits into
mainfrom
lazy-esm-node-assert
May 6, 2026
Merged

bartlomieju merged 5 commits into
mainfrom
lazy-esm-node-assert

Conversation

@bartlomieju

Copy link
Copy Markdown
Member

Summary

Convert node:util, node:util/types, node:assert, and node:assert/strict out of the snapshot using the lazy-loaded pattern:

node:util:

  • Implementation in util.ts (lazy_loaded_js IIFE)
  • Thin ESM wrapper util_esm.ts in lazy_loaded_esm
  • All 15 import ... from "node:util" in ext/node replaced with core.loadExtScript("ext:deno_node/util.ts")

node:util/types:

  • Already a thin wrapper over core.loadExtScript -- moved from esm to lazy_loaded_esm

node:assert:

  • Implementation in assert.ts (lazy_loaded_js IIFE)
  • Thin ESM wrapper assert_esm.ts in lazy_loaded_esm
  • All 16 import ... from "node:assert" in ext/node replaced with core.loadExtScript("ext:deno_node/assert.ts")

node:assert/strict:

  • Updated to load from core.loadExtScript instead of import from "node:assert"
  • Moved from esm to lazy_loaded_esm

Also converts internal/util/parse_args/parse_args.js from esm to lazy_loaded_js.

Test plan

  • ESM import, CJS require, dynamic import all work for all converted modules
  • node:sys === node:util identity preserved for CJS
  • node:assert/strict works
  • Format and lint clean (including Rust clippy)
  • CI

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 fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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") → loadNativeModule returns nodeMod.exports = nativeModuleExports.util = the IIFE's outer return = B ({ callbackify, ..., default: A }).
  • ESM (await import("node:util")).default → util_esm.ts's export 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

  1. Make the CJS path return mod.default — change 01_require.js's const util = ... to use mod.default instead of mod. Cleanest fix: keeps the require system aligned with ESM's default semantics.

  2. Make the IIFE's outer return the same object as defaultExport — restructure util.ts so defaultExport is the return, then B === A. Requires putting all named exports onto the same object that's referenced by default, which is roughly Node's actual shape (util and util.default both 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 fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@bartlomieju
bartlomieju enabled auto-merge (squash) May 6, 2026 16:11
@bartlomieju
bartlomieju merged commit 6100ec3 into main May 6, 2026
136 checks passed
@bartlomieju
bartlomieju deleted the lazy-esm-node-assert branch May 6, 2026 16:11
bartlomieju added a commit that referenced this pull request May 7, 2026
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)
bartlomieju added a commit that referenced this pull request May 7, 2026
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
bartlomieju added a commit that referenced this pull request May 7, 2026
Continues the lazy-loading conversion started in #33876, #33881, #33882,
and #33897.
bartlomieju added a commit that referenced this pull request May 7, 2026
Continues the lazy-loading conversion started in #33876, #33881, #33882,
#33897, and #33900.
littledivy pushed a commit to crowlKats/deno that referenced this pull request Jun 10, 2026
…nd#33876)

Convert `node:util`, `node:util/types`, `node:assert`, and
`node:assert/strict` out of the snapshot using the lazy-loaded pattern:
littledivy pushed a commit to crowlKats/deno that referenced this pull request Jun 10, 2026
)

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)
littledivy pushed a commit to crowlKats/deno that referenced this pull request Jun 10, 2026
)

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
littledivy pushed a commit to crowlKats/deno that referenced this pull request Jun 10, 2026
littledivy pushed a commit to crowlKats/deno that referenced this pull request Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants