fix(dynamic-import): resolve literal node: builtin import('node:crypto') to native namespace (#1673)#1742
Merged
Merged
Conversation
…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.
4 tasks
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1673. A dynamic
import()of a generalnode:builtin —node:crypto,node:util,node:os, … — rejected withundefinedat 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 dynamicimport('node:crypto')had no map entry and fell through tojs_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 thenode:-stripped module name), and the dispatch builds the namespace viajs_create_native_module_namespace— the sameNATIVE_MODULE_CLASS_IDobject thatrequire('node:crypto')andimport * 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-pathjs_string_equalschain.An unsupported builtin keeps rejecting:
is_native_moduleis false for it, so no sentinel is recorded and the dispatch falls through tojs_promise_rejected, matching Node's failure mode (caught bytry/catch).Testing
test-files/test_gap_dynamic_import_node_builtin.ts— byte-equal tonode --experimental-strip-types:node:crypto+node:utilresolve, unsupported builtin rejects.node:crypto(randomUUID),node:util(format),node:os(platform/EOL),node:fs/promises(already-working submodule — no regression), multi-path ternary between twonode:builtins, and unsupported-builtin reject.test_gap_dynamic_import_*gap tests pass;cargo fmt --all -- --checkclean.Note on
test_parity_zlib.tsThe acceptance criteria mention
test_parity_zlib.ts:77'sawait import("node:util"). That whole test currently fails to compile on an unrelatedzlib.Deflatenot-implemented gap (#463), so itsnode:utilline can't be exercised through it. The dedicated gap test above provides theawait import("node:util")coverage instead; the zlib-surface gaps are out of scope for this issue.