Skip to content

fix(compile): #818 — recursively bundle transitive ESM imports for V8 fallback#987

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

fix(compile): #818 — recursively bundle transitive ESM imports for V8 fallback#987
proggeramlug merged 1 commit into
mainfrom
worktree-agent-ad3fa19152c54f56b

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

  • __perry_js_bundle.js only contained the first JS module Perry landed on. Pure-ESM packages whose entry re-exports siblings (hono's dist/index.js./hono.js./hono-base.js → … ~20 submodules) shipped with one entry, dropping the rest. Compiled binaries still worked when their node_modules/ tree happened to sit next to them (V8 reads files off disk), but the realistic hono call path (app.fetch(req) running cross-thread) cascaded into rc=139 segfaults when those paths weren't co-located.
  • The JS branch of collect_modules was a leaf: it pushed one entry to ctx.js_modules and bailed with the comment "V8 will handle that at runtime". The native TypeScript branch already recursed through cached_resolve_import; JS did not.
  • Added collect_js_module_imports — a lightweight regex-based scanner that extracts static import / export ... from / string-literal import(...) specifiers, resolves each relative / absolute path via the existing resolve_with_extensions, and re-enters collect_modules for every result. Bare specifiers (react, @foo/bar) stay the entry walker's job since the top-level TS path already pulls package deps in via cached_resolve_import.

Validation

  • hono@4.12.19 repro under /tmp/perry-hono/ (import { Hono } from 'hono'; const app = new Hono(); …):
    • Pre-fix: __perry_js_bundle.js contained 1 globalThis.__COMPILETS_MODULES[...] entry (dist/index.js).
    • Post-fix: bundle contains 24 entries — every dist/**/*.js reachable from index.js through the full re-export graph (compose.js, context.js, hono.js, hono-base.js, http-exception.js, request.js, request/constants.js, router.js, plus the router/{reg-exp,smart,trie}-router/*.js and utils/*.js subtrees).
    • Runtime output unchanged: object / function / function.
  • Compile smoke (console.log("hello")) still works.
  • cargo build --release -p perry-runtime -p perry-stdlib -p perry-jsruntime -p perry green.

Known next blocker (out of scope)

V8's ModuleLoader::load in crates/perry-jsruntime/src/modules.rs:464-538 still reads sources via std::fs::read_to_string(&path) and never consults the embedded __COMPILETS_MODULES map. So the binary still requires the node_modules/ tree at runtime — removing it after compile produces Cannot resolve module. Wiring the loader to prefer the embedded source (a small short-circuit at the top of load()) is a separate fix toward the wider "single self-contained binary for V8-fallback packages" goal also captured under #818.

Test plan

  • CI green (lint, cargo-test, parity, compile-smoke, api-docs-drift, security-audit)
  • Manual hono smoke: cd /tmp/perry-hono && perry test.ts -o out && ./out prints object / function / function
  • Bundle contains > 1 module entry for hono (grep -c '^globalThis.__COMPILETS_MODULES\[' __perry_js_bundle.js == 24)

… fallback

The V8-fallback bundler (`__perry_js_bundle.js`) only included the first
JS module it landed on — pure-ESM packages whose entry re-exports
siblings (hono's `dist/index.js` → `./hono.js` → `./hono-base.js` → …
twenty-some submodules) ended up with one entry, dropping the rest.
Compiled binaries still worked when their `node_modules/` tree happened
to sit next to them (V8 reads files off disk), but the realistic hono
call path (`app.fetch(req)` running cross-thread) cascaded into rc=139
segfaults when those resolved paths weren't co-located, and shipping the
binary on its own surfaced `Cannot resolve module` for every missing
sibling.

Root cause was the JS branch of `collect_modules` bailing after one
insert with the comment "V8 will handle that at runtime". Native
TypeScript imports already recursed via `cached_resolve_import`; JS did
not. Added a lightweight regex-based scanner
(`collect_js_module_imports`) that pulls out static `import` /
`export ... from` / string-literal `import("...")` specifiers, resolves
each relative / absolute path via the existing `resolve_with_extensions`
helper, and re-enters `collect_modules` for each — re-running the
JS/native classification so cross-package landings into a
`compilePackages`-overridden TS file are still handled correctly. Bare
specifiers (`react`, `@foo/bar`) stay the entry walker's job since the
top-level TS path already pulls package deps; the relative walk only
needs to cover the inside-a-package case, which is always relative-path.

Validation: hono@4.12.19 repro under `/tmp/perry-hono/` — pre-fix
bundle had 1 module entry, post-fix has 24 (every `dist/**/*.js`
reachable from `index.js` through the full re-export graph). New
fixture `test-files/test_hono_bundle.ts` documents the smoke shape;
the bundle-walks-recursively assertion lives in the PR description
because the parity suite doesn't install npm packages.

Next blocker (out of scope): V8's `ModuleLoader::load` in
`crates/perry-jsruntime/src/modules.rs` still reads from disk via
`std::fs::read_to_string` and never consults `__COMPILETS_MODULES`, so
the binary isn't yet self-contained even with the correct bundle.
@proggeramlug proggeramlug force-pushed the worktree-agent-ad3fa19152c54f56b branch from 36bb20f to d0e210d Compare May 18, 2026 04:13
@proggeramlug proggeramlug merged commit b8fe013 into main May 18, 2026
5 of 9 checks passed
@proggeramlug proggeramlug deleted the worktree-agent-ad3fa19152c54f56b branch May 18, 2026 04:13
proggeramlug added a commit that referenced this pull request May 18, 2026
…object_to_v8 (#1015)

PR #987 unblocked hono compilation by bundling transitive ESM imports, but
the binary then SIGSEGV'd at `app.fetch(req)` because `native_object_to_v8`
treated bridge "small handles" (low-value integers stored in JS_HANDLE_TAG)
as raw pointers to ObjectHeader, dereferencing invalid memory.

Added a small-handle guard in bridge.rs that returns the handle as a V8
number instead of attempting pointer indirection.

Hono no longer crashes; next blocker is V8 fallback's missing `Response`
global (separate concern).
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