Skip to content

fix(dynamic-import): resolve literal node: builtin import('node:crypto') to native namespace (#1673)#1742

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-1673-nodeimport
May 25, 2026
Merged

fix(dynamic-import): resolve literal node: builtin import('node:crypto') to native namespace (#1673)#1742
proggeramlug merged 1 commit into
mainfrom
worktree-fix-1673-nodeimport

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

Fixes #1673. A dynamic import() of a general node: builtin — node:crypto, node:util, node:os, … — rejected with undefined at runtime even for builtins Perry fully supports. Native modules have no compiled-source backing and no @__perry_ns_<prefix> global, and the dynamic-import dispatch only knew how to load a compiled-module namespace or a fixed node-submodule namespace.

Root cause

Known node-submodules (node:fs/promises, node:stream/promises, …) already resolved via the __node_submod__ sentinel → js_node_submodule_namespace (#1671). General native builtins are not in that submodule table, so a dynamic import('node:crypto') had no map entry and fell through to js_promise_rejected(undefined).

Fix

Reuse the same sentinel mechanism. When a dynamic-import target is a native module with no submodule-table entry, the driver records a __native_mod__<name> sentinel (keyed by the node:-stripped module name), and the dispatch builds the namespace via js_create_native_module_namespace — the same NATIVE_MODULE_CLASS_ID object that require('node:crypto') and import * as ns from 'node:crypto' produce, whose member access dispatches natively at runtime — then resolves the promise with it. Handled in both the single-path fast path and the multi-path js_string_equals chain.

An unsupported builtin keeps rejecting: is_native_module is false for it, so no sentinel is recorded and the dispatch falls through to js_promise_rejected, matching Node's failure mode (caught by try/catch).

Testing

  • New gap test test-files/test_gap_dynamic_import_node_builtin.ts — byte-equal to node --experimental-strip-types: node:crypto + node:util resolve, unsupported builtin rejects.
  • Verified manually: node:crypto (randomUUID), node:util (format), node:os (platform/EOL), node:fs/promises (already-working submodule — no regression), multi-path ternary between two node: builtins, and unsupported-builtin reject.
  • All 10 test_gap_dynamic_import_* gap tests pass; cargo fmt --all -- --check clean.

Note on test_parity_zlib.ts

The acceptance criteria mention test_parity_zlib.ts:77's await import("node:util"). That whole test currently fails to compile on an unrelated zlib.Deflate not-implemented gap (#463), so its node:util line can't be exercised through it. The dedicated gap test above provides the await import("node:util") coverage instead; the zlib-surface gaps are out of scope for this issue.

Sibling of #1672 (merged). No version bump / CHANGELOG entry — folded in at merge.

…o') to native namespace (#1673)

A dynamic `import()` of a general `node:` builtin (`node:crypto`, `node:util`,
`node:os`, ...) rejected with `undefined` at runtime even for builtins Perry
fully supports. Native modules have no compiled-source backing and no
`@__perry_ns_<prefix>` global, and the dynamic-import dispatch only knew how to
load a compiled-module namespace or a fixed node-submodule namespace.

Known node-submodules (`node:fs/promises`, `node:stream/promises`, ...) already
resolved via the `__node_submod__` sentinel → `js_node_submodule_namespace`
(#1671). General native builtins are NOT in that submodule table, so they fell
through to the reject path.

Reuse the same sentinel mechanism for them: when a dynamic-import target is a
native module with no submodule-table entry, the driver records a
`__native_mod__<name>` sentinel (keyed by the `node:`-stripped module name), and
the dispatch builds the namespace via `js_create_native_module_namespace` — the
same NATIVE_MODULE_CLASS_ID object `require('node:crypto')` and `import * as`
produce, whose member access dispatches natively at runtime — then resolves the
promise with it. Handled in both the single-path fast path and the multi-path
`js_string_equals` chain.

An unsupported builtin keeps rejecting: `is_native_module` is false for it, so no
sentinel is recorded and the dispatch falls through to `js_promise_rejected`,
matching Node's failure mode (caught by `try/catch`).

Adds test-files/test_gap_dynamic_import_node_builtin.ts (byte-equal to
`node --experimental-strip-types`): node:crypto + node:util resolve, unsupported
builtin rejects.
@proggeramlug proggeramlug merged commit 9c7039d into main May 25, 2026
10 checks passed
@proggeramlug proggeramlug deleted the worktree-fix-1673-nodeimport branch May 25, 2026 05:07
proggeramlug added a commit that referenced this pull request May 25, 2026
7 PRs landed on main after the v0.5.1027 bump (50c391f) without
per-PR tags. Neither v0.5.1026 nor v0.5.1027 were tagged on the
remote — v0.5.1028 is the first tag in this window.

- #1738 feat(compile): --trace/--focus debugging flags.
- #1723/#1741 fix(lockdown): #503 ns[dynamicKey].staticMember.
- #1673/#1742 fix(dynamic-import): literal node: builtin specifier.
- #1724/#1747 fix(node): Blob/URL globals trigger http-client feature.
- #1728/#1749 fix(node:path): win32 normalize/basename/toNamespacedPath.
- #1751 test(parity): stream consumers/promises/static batch.
- #1754 test(node-core): enrich common shim for #800.
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.

feat(dynamic-import): resolve literal node: builtin import('node:crypto') to native namespace

1 participant