Skip to content

feat(jsruntime): V8 ModuleLoader reads from embedded module map (#818)#989

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-agent-a4dff99270152311c
May 18, 2026
Merged

feat(jsruntime): V8 ModuleLoader reads from embedded module map (#818)#989
proggeramlug merged 1 commit into
mainfrom
worktree-agent-a4dff99270152311c

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

  • Closes the second half of fix: #811 — net.isIP/isIPv4/isIPv6 + auto-select-family defaults #818. v0.5.993 made the bundle's content correct (transitive ESM siblings now walk fully); this PR makes the bundle's consumer correct (V8 ModuleLoader reads from an in-memory map, not from disk).
  • Compile-time: emit a generated .c with one static const char[] per JS module source + length, plus matching pairs for every TS import edge whose resolved path lives in ctx.js_modules. A __attribute__((constructor(101))) calls the registration FFIs before main invokes js_runtime_init().
  • Runtime: NodeModuleLoader::resolve_module_path consults a bare-specifier alias map and a path map (with extension/index fallback) when on-disk probes fail. NodeModuleLoader::load checks the source map before std::fs::read_to_string.
  • Result: a Perry binary that uses a V8-fallback npm package (hono, express, etc.) boots in a directory with no node_modules/ and no source files.

Test plan

  • Build hono test, move binary to isolated dir without node_modules/, run successfully:
    cd /tmp/perry-selfcontained
    cat > test.ts <<'EOF2'
    import { Hono } from 'hono';
    const app = new Hono();
    app.get('/', (c) => c.text('Hi'));
    console.log(typeof app, typeof app.get);
    EOF2
    npm install hono@4.6.5 --silent
    perry test.ts -o out
    mkdir -p /tmp/perry-iso && cp out /tmp/perry-iso/ && cd /tmp/perry-iso && ./out
    # → object function
  • Same binary still works in the original node_modules/-co-located directory.
  • Same binary works in a directory that has an unrelated node_modules/ (e.g. node_modules/junk/).
  • New fixture test-files/test_v8_self_contained.ts documents the expectation.
  • CI: lint, cargo-test, parity, compile-smoke, api-docs-drift, security-audit.

Files touched

  • crates/perry-jsruntime/src/modules.rsEMBEDDED_MODULES / EMBEDDED_ALIASES maps + FFIs + load/resolve consults + extension-fallback helper.
  • crates/perry/src/commands/compile/targets.rsgenerate_embedded_js_object + ASCII-clean c_string_literal helper (octal-escapes non-printable bytes; defangs trigraphs).
  • crates/perry/src/commands/compile.rs — append generated .o to obj_paths whenever needs_js_runtime && !js_modules.is_empty().
  • test-files/test_v8_self_contained.ts — fixture.
  • CHANGELOG.md, CLAUDE.md, Cargo.toml, Cargo.lock — v0.5.994.

v0.5.993 closed half of #818: the bundle now contains every transitive
ESM sibling. The other half — `NodeModuleLoader::load` still reading
sources via `std::fs::read_to_string` and ignoring the bundle — meant
binaries still required `node_modules/` to live next to them at
runtime. This commit makes V8-fallback binaries truly self-contained.

Three pieces:

1. `perry-jsruntime/src/modules.rs` grows two `RwLock<HashMap>`s —
   `EMBEDDED_MODULES` (build-time canonical path -> source) and
   `EMBEDDED_ALIASES` (bare specifier -> path) — plus `#[no_mangle]`
   FFIs `js_register_embedded_module` / `js_register_embedded_alias`
   and Rust-facing wrappers.

2. `NodeModuleLoader::resolve_module_path` consults the alias map for
   bare specs and the path map (with extension/index fallback) when
   on-disk probes fail. `NodeModuleLoader::load` checks the map before
   `std::fs::read_to_string`. Keys are build-time canonical paths used
   as opaque identifiers — `canonicalize()` in `resolve()` already
   gracefully falls back when the path doesn't exist on the runtime
   filesystem, and the `file://` URL handed to V8 works either way.

3. New `targets::generate_embedded_js_object` writes a generated `.c`
   file with one `static const char[]` per JS module source + length
   pair (and matching pairs for every TS import edge whose
   `resolved_path` is in `ctx.js_modules`), wrapped in a
   `__attribute__((constructor(101)))` that calls the registration
   FFIs at process start. `cc -c` produces an `.o`; compile.rs appends
   it to `obj_paths` whenever `needs_js_runtime && !js_modules.is_empty()`.
   Octal escapes for every non-printable byte keep the generated C
   ASCII-clean regardless of the JS file's encoding; `?` is escaped to
   defang trigraph hazards.

Validation: `import { Hono } from 'hono'` compiles, the resulting
binary moved to an isolated directory with no `node_modules/` and no
source still prints `object function`. The on-disk
`__perry_js_bundle.js` is retained as a build-time debugging artifact
but is no longer needed at runtime.

New fixture: `test-files/test_v8_self_contained.ts`.
@proggeramlug proggeramlug merged commit f47f931 into main May 18, 2026
5 of 9 checks passed
@proggeramlug proggeramlug deleted the worktree-agent-a4dff99270152311c branch May 18, 2026 04:33
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.

1 participant