diff --git a/CHANGELOG.md b/CHANGELOG.md index d91c79b022..ce0ecf097e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ Detailed changelog for Perry. See CLAUDE.md for concise summaries. +## v0.5.993 — fix(compile): recursively bundle transitive ESM imports for V8 fallback + +**Symptom.** A program that imports a pure-ESM npm package whose entry file re-exports siblings (`hono`'s `dist/index.js` → `./hono.js` → `./hono-base.js` → `./compose.js` → `./router/*` → `./utils/*` …) ended up with a `__perry_js_bundle.js` containing only the single entry file. Roughly 20 transitive `dist/**/*.js` files were silently dropped. Compiled binaries still worked when their `node_modules/` tree happened to sit alongside them (V8's `ModuleLoader::load` opens files off disk), but shipping the binary on its own — or running it in any sandbox where the resolved paths don't exist — left V8 throwing `Cannot resolve module` for every missing sibling, and in the realistic hono call path (`app.fetch(req)` running cross-thread) cascaded to an rc=139 segfault because the missing-module callback handed unboxed `undefined` back to compiled native code expecting a NaN-boxed pointer. + +**Root cause — JS branch of `collect_modules` was a leaf.** `crates/perry/src/commands/compile/collect_modules.rs` classifies every reachable file as either "native compile" (TypeScript / `compilePackages`-overridden source) or "JS runtime" (everything else under `node_modules/` when `--enable-js-runtime` is implicitly on). The native branch already recursed via `cached_resolve_import` for every `import.source` on the lowered HIR, and re-exports were chased separately a few lines later. The JS branch took the shortcut comment `// We don't parse JS/node_modules files for their imports (V8 will handle that at runtime)` and bailed after inserting one entry into `ctx.js_modules`. The same bailout fed `targets::generate_js_bundle`, which loops over `ctx.js_modules` to materialize `__COMPILETS_MODULES` — so the bundle is exactly as deep as the JS walk, i.e., one level. + +The codegen path was correct: SWC parses TypeScript fine, so the user-written `import { Hono } from 'hono'` got registered as an import edge and `hono`'s `package.json` `module: "dist/index.js"` resolved through `resolve_package_entry`. That gave us one JS module. From there the walk stopped. Everything `dist/index.js` re-exported existed on disk but was invisible to the bundler. + +**Fix — add a lightweight ESM-import scanner and recurse.** New helper `collect_js_module_imports` in `collect_modules.rs` regex-scans a JS source for the static import / re-export / string-literal-dynamic-import shapes and returns the resolved sibling paths. Only relative / absolute specifiers are followed — bare specifiers like `react` need full `node_modules` resolution that the entry walker already handled via `cached_resolve_import`, so the realistic hono / express / koa / fastify / ink shape (top-level package brings in its own relative submodules) is fully covered. The JS branch then loops over the returned paths and re-enters `collect_modules`, which re-runs the JS/native classification — covering the case where a JS file re-imports something that resolves to a TypeScript file under a `compilePackages` directory. + +The scanner is regex-based on purpose. Running SWC on every transitive JS file just to harvest specifiers would cost real time on `node_modules` walks (hono alone is 24 files; effect+drizzle compose into hundreds), and the bundle's only job here is "make sure the path is embedded". Runtime semantics — conditional execution, dynamic shape, namespace materialization — remain V8's job; the bundler just needs to know which file paths the V8 fallback will be asked for. + +**Validation.** Built a local `hono@latest` (4.12.19) repro under `/tmp/perry-hono/`: +- Pre-fix: bundle contained 1 `globalThis.__COMPILETS_MODULES[...]` entry (hono's `dist/index.js`). +- Post-fix: bundle contains 24 entries — every `dist/**/*.js` reachable from `index.js` through the entire re-export graph (`compose.js`, `context.js`, `hono.js`, `hono-base.js`, `http-exception.js`, `request.js`, `request/constants.js`, `router.js`, plus the full `router/{reg-exp,smart,trie}-router/*.js` and `utils/*.js` subtrees). +- `./out` still prints the expected `object` / `function` / `function`. +- New test fixture at `test-files/test_hono_bundle.ts` is a static type-check / compile-doesn't-choke probe; the bundle-walks-recursively assertion lives in the PR description because the parity suite doesn't install npm packages. + +**Known next blocker (not in scope).** The bundle is now correct content-wise, but V8's `ModuleLoader::load` in `crates/perry-jsruntime/src/modules.rs:464-538` still resolves via `std::fs::read_to_string` against the on-disk path — `__COMPILETS_MODULES` is generated but never consulted by the loader. Removing `node_modules/` from beside the binary still produces `Cannot resolve module`. Wiring the loader to prefer the in-binary embedded source is a separate fix (likely a small `if let Some(src) = unsafe { COMPILED_MODULES.get(&path_str) }` short-circuit before the disk read), tracked alongside #818's wider "compile to a single self-contained binary for V8-fallback packages" goal. + ## v0.5.992 — fix(jsruntime/events): lazy `_events` init so mixin paths (express) work **Symptom.** After v0.5.991's `util.inherits` fix landed (PR #984), the next express blocker surfaced: @@ -36,7 +56,6 @@ While there, also added the previously-missing prototype methods Node's EventEmi - `crates/perry-jsruntime/src/modules.rs` (events shim rewrite, ~20 lines) - `test-files/test_events_mixin_lazy.ts` (new regression test) - ## v0.5.991 — fix(codegen): V8 wildcard-namespace member calls — `R.sum([1,2,3])` returns 15 not 0 **Symptom.** `import * as R from 'ramda'; console.log(R.sum([1,2,3,4,5]))` printed `0` instead of `15`. Same shape for any `R.add(2,3)`, `R.identity(5)`, `R.head([1,2,3])` — every member call on a wildcard-namespace import of a V8-fallback module returned the literal `0.0`. The bug is reproducible without ramda using a tiny `.mjs` helper module (`export function sum(arr) { return arr.reduce(...) }`) imported as `import * as helper from './helper.mjs'`, then `helper.sum([1,2,3])`. Named imports from the same module (`import { sum }`) worked fine. diff --git a/CLAUDE.md b/CLAUDE.md index b6874aafcf..adef3a0c81 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.992 +**Current Version:** 0.5.993 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 0921edbf27..f12e8b3f96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4905,7 +4905,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.992" +version = "0.5.993" dependencies = [ "anyhow", "base64", @@ -4960,14 +4960,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.992" +version = "0.5.993" dependencies = [ "serde", ] [[package]] name = "perry-codegen" -version = "0.5.992" +version = "0.5.993" dependencies = [ "anyhow", "log", @@ -4980,7 +4980,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.992" +version = "0.5.993" dependencies = [ "anyhow", "perry-hir", @@ -4989,7 +4989,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.992" +version = "0.5.993" dependencies = [ "anyhow", "perry-hir", @@ -4997,7 +4997,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.992" +version = "0.5.993" dependencies = [ "anyhow", "perry-dispatch", @@ -5007,7 +5007,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.992" +version = "0.5.993" dependencies = [ "anyhow", "perry-hir", @@ -5016,7 +5016,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.992" +version = "0.5.993" dependencies = [ "anyhow", "base64", @@ -5029,7 +5029,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.992" +version = "0.5.993" dependencies = [ "anyhow", "perry-hir", @@ -5037,7 +5037,7 @@ dependencies = [ [[package]] name = "perry-diagnostics" -version = "0.5.992" +version = "0.5.993" dependencies = [ "serde", "serde_json", @@ -5045,7 +5045,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.992" +version = "0.5.993" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5056,7 +5056,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.992" +version = "0.5.993" dependencies = [ "anyhow", "clap", @@ -5071,7 +5071,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.992" +version = "0.5.993" dependencies = [ "argon2", "perry-ffi", @@ -5079,7 +5079,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.992" +version = "0.5.993" dependencies = [ "perry-ffi", "reqwest", @@ -5088,7 +5088,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.992" +version = "0.5.993" dependencies = [ "bcrypt", "perry-ffi", @@ -5096,7 +5096,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.992" +version = "0.5.993" dependencies = [ "perry-ffi", "rusqlite", @@ -5104,7 +5104,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.992" +version = "0.5.993" dependencies = [ "perry-ffi", "scraper", @@ -5112,14 +5112,14 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.992" +version = "0.5.993" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-cron" -version = "0.5.992" +version = "0.5.993" dependencies = [ "chrono", "cron", @@ -5128,7 +5128,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.992" +version = "0.5.993" dependencies = [ "chrono", "perry-ffi", @@ -5136,7 +5136,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.992" +version = "0.5.993" dependencies = [ "perry-ffi", "rust_decimal", @@ -5144,7 +5144,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.992" +version = "0.5.993" dependencies = [ "perry-ffi", "serde_json", @@ -5152,7 +5152,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.992" +version = "0.5.993" dependencies = [ "perry-ffi", "rand 0.8.6", @@ -5160,21 +5160,21 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.992" +version = "0.5.993" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.992" +version = "0.5.993" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.992" +version = "0.5.993" dependencies = [ "bytes", "http-body-util", @@ -5188,7 +5188,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.992" +version = "0.5.993" dependencies = [ "lazy_static", "perry-ffi", @@ -5199,7 +5199,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.992" +version = "0.5.993" dependencies = [ "lazy_static", "perry-ext-http-server", @@ -5211,7 +5211,7 @@ dependencies = [ [[package]] name = "perry-ext-http-server" -version = "0.5.992" +version = "0.5.993" dependencies = [ "bytes", "http-body-util", @@ -5230,7 +5230,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.992" +version = "0.5.993" dependencies = [ "lazy_static", "perry-ffi", @@ -5240,7 +5240,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.992" +version = "0.5.993" dependencies = [ "base64", "jsonwebtoken", @@ -5251,7 +5251,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.992" +version = "0.5.993" dependencies = [ "lru", "perry-ffi", @@ -5259,7 +5259,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.992" +version = "0.5.993" dependencies = [ "chrono", "perry-ffi", @@ -5267,7 +5267,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.992" +version = "0.5.993" dependencies = [ "bson", "futures-util", @@ -5279,7 +5279,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.992" +version = "0.5.993" dependencies = [ "chrono", "perry-ffi", @@ -5289,7 +5289,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.992" +version = "0.5.993" dependencies = [ "nanoid", "perry-ffi", @@ -5298,7 +5298,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.992" +version = "0.5.993" dependencies = [ "perry-ffi", "rustls", @@ -5309,7 +5309,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.992" +version = "0.5.993" dependencies = [ "lettre", "perry-ffi", @@ -5319,7 +5319,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.992" +version = "0.5.993" dependencies = [ "perry-ffi", "sqlx", @@ -5328,7 +5328,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.992" +version = "0.5.993" dependencies = [ "governor", "perry-ffi", @@ -5336,7 +5336,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.992" +version = "0.5.993" dependencies = [ "base64", "image", @@ -5345,14 +5345,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.992" +version = "0.5.993" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.992" +version = "0.5.993" dependencies = [ "lazy_static", "perry-ffi", @@ -5360,7 +5360,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.992" +version = "0.5.993" dependencies = [ "perry-ffi", "uuid", @@ -5368,7 +5368,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.992" +version = "0.5.993" dependencies = [ "perry-ffi", "regex", @@ -5378,7 +5378,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.992" +version = "0.5.993" dependencies = [ "futures-util", "lazy_static", @@ -5389,7 +5389,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.992" +version = "0.5.993" dependencies = [ "flate2", "perry-ffi", @@ -5397,7 +5397,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.992" +version = "0.5.993" dependencies = [ "dashmap 6.1.0", "once_cell", @@ -5406,7 +5406,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.992" +version = "0.5.993" dependencies = [ "anyhow", "perry-api-manifest", @@ -5420,7 +5420,7 @@ dependencies = [ [[package]] name = "perry-jsruntime" -version = "0.5.992" +version = "0.5.993" dependencies = [ "anyhow", "deno_core", @@ -5440,7 +5440,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.992" +version = "0.5.993" dependencies = [ "anyhow", "perry-diagnostics", @@ -5452,7 +5452,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.992" +version = "0.5.993" dependencies = [ "anyhow", "base64", @@ -5476,7 +5476,7 @@ dependencies = [ [[package]] name = "perry-stdlib" -version = "0.5.992" +version = "0.5.993" dependencies = [ "aes 0.8.4", "aes-gcm", @@ -5546,7 +5546,7 @@ dependencies = [ [[package]] name = "perry-transform" -version = "0.5.992" +version = "0.5.993" dependencies = [ "anyhow", "perry-hir", @@ -5556,7 +5556,7 @@ dependencies = [ [[package]] name = "perry-types" -version = "0.5.992" +version = "0.5.993" dependencies = [ "anyhow", "thiserror 1.0.69", @@ -5564,11 +5564,11 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.992" +version = "0.5.993" [[package]] name = "perry-ui-android" -version = "0.5.992" +version = "0.5.993" dependencies = [ "itoa", "jni", @@ -5583,7 +5583,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.992" +version = "0.5.993" dependencies = [ "rand 0.8.6", "serde", @@ -5593,7 +5593,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.992" +version = "0.5.993" dependencies = [ "cairo-rs", "dirs 5.0.1", @@ -5612,7 +5612,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.992" +version = "0.5.993" dependencies = [ "block2", "libc", @@ -5627,7 +5627,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.992" +version = "0.5.993" dependencies = [ "block2", "libc", @@ -5645,11 +5645,11 @@ version = "0.1.0" [[package]] name = "perry-ui-testkit" -version = "0.5.992" +version = "0.5.993" [[package]] name = "perry-ui-tvos" -version = "0.5.992" +version = "0.5.993" dependencies = [ "block2", "libc", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.992" +version = "0.5.993" dependencies = [ "block2", "libc", @@ -5679,7 +5679,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.992" +version = "0.5.993" dependencies = [ "block2", "libc", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.992" +version = "0.5.993" dependencies = [ "libc", "perry-runtime", @@ -5706,7 +5706,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.992" +version = "0.5.993" dependencies = [ "base64", "ed25519-dalek", @@ -5720,7 +5720,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.992" +version = "0.5.993" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 77a0e59a18..f5693aa6b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -190,7 +190,7 @@ opt-level = "s" # Optimize for size in stdlib opt-level = 3 [workspace.package] -version = "0.5.992" +version = "0.5.993" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/crates/perry/src/commands/compile/collect_modules.rs b/crates/perry/src/commands/compile/collect_modules.rs index d384de7715..ca2b7fc153 100644 --- a/crates/perry/src/commands/compile/collect_modules.rs +++ b/crates/perry/src/commands/compile/collect_modules.rs @@ -29,6 +29,96 @@ use super::{ JsModule, ParseCache, }; +/// Issue #818: scan a JS module's source for static ESM imports / +/// re-exports / string-literal dynamic imports, resolve each one +/// against the module's directory (with `resolve_with_extensions` so +/// extensionless and folder-index lookups work the same way they do at +/// import-time), and return the deduped list of file paths to add to +/// the bundle. +/// +/// Bare specifiers (`react`, `@foo/bar`) and unresolvable relative +/// paths are skipped: bare specifiers are the V8 fallback's job to +/// resolve via the node_modules tree (we don't have a `require.resolve` +/// equivalent here without a full parse), and unresolvable relatives +/// just leak the same runtime error the V8 loader would have produced +/// anyway. This keeps the scan cheap and side-effect free. +pub(super) fn collect_js_module_imports(file_path: &std::path::Path, source: &str) -> Vec { + use std::sync::OnceLock; + static IMPORT_RE: OnceLock = OnceLock::new(); + static EXPORT_FROM_RE: OnceLock = OnceLock::new(); + static DYNAMIC_IMPORT_RE: OnceLock = OnceLock::new(); + static BARE_IMPORT_RE: OnceLock = OnceLock::new(); + + // `import ... from "spec"` — matches default/named/namespace forms. + let import_re = IMPORT_RE.get_or_init(|| { + regex::Regex::new(r#"(?m)^\s*import\s+(?:[^'"]+?\s+from\s+)?['"]([^'"]+)['"]"#) + .expect("import regex") + }); + // Bare side-effect import: `import "./foo.js";` + let bare_re = BARE_IMPORT_RE.get_or_init(|| { + regex::Regex::new(r#"(?m)^\s*import\s+['"]([^'"]+)['"]"#).expect("bare import regex") + }); + // `export ... from "spec"` — covers `export *`, `export * as ns`, + // `export { a, b }`. Captures the specifier. + let export_re = EXPORT_FROM_RE.get_or_init(|| { + regex::Regex::new( + r#"(?m)^\s*export\s+(?:\*(?:\s+as\s+\w+)?|\{[^}]*\})\s+from\s+['"]([^'"]+)['"]"#, + ) + .expect("export from regex") + }); + // Dynamic `import("spec")` — string-literal only. + let dyn_re = DYNAMIC_IMPORT_RE.get_or_init(|| { + regex::Regex::new(r#"\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)"#).expect("dynamic import regex") + }); + + let mut specs: Vec = Vec::new(); + for cap in import_re.captures_iter(source) { + specs.push(cap[1].to_string()); + } + for cap in bare_re.captures_iter(source) { + specs.push(cap[1].to_string()); + } + for cap in export_re.captures_iter(source) { + specs.push(cap[1].to_string()); + } + for cap in dyn_re.captures_iter(source) { + specs.push(cap[1].to_string()); + } + + let parent = match file_path.parent() { + Some(p) => p, + None => return Vec::new(), + }; + + let mut out: Vec = Vec::new(); + let mut seen: HashSet = HashSet::new(); + for spec in specs { + // Only follow relative or absolute paths — bare specifiers like + // `react` need the node_modules resolver which is more invasive + // to call here. The original entry walker (TS path) already + // pulled bare-specifier dependencies in via `cached_resolve_import`, + // so the most common case (top-level package brings in submodules) + // is covered. Inside a package's `node_modules` tree, all + // sibling imports are relative-path anyway. + if !(spec.starts_with("./") || spec.starts_with("../") || spec.starts_with('/')) { + continue; + } + let candidate = if spec.starts_with('/') { + PathBuf::from(&spec) + } else { + parent.join(&spec) + }; + if let Some(resolved) = super::resolve::resolve_with_extensions(&candidate) { + if let Ok(canon) = resolved.canonicalize() { + if seen.insert(canon.clone()) { + out.push(canon); + } + } + } + } + out +} + /// Issue #841: Node.js submodules that Perry knows about at the /// resolver level (no perry-stdlib backing, no compiled-source backing) /// but for which we still want to provide a minimal import surface so @@ -133,6 +223,37 @@ pub(super) fn collect_modules( .map_err(|e| anyhow!("Failed to read {}: {}", canonical.display(), e))?; let specifier = canonical.to_string_lossy().to_string(); + // Issue #818: walk transitive ESM imports for JS modules so the + // bundle contains every file the V8 fallback will be asked to load + // at runtime. Without this, pure-ESM packages with relative + // sub-module imports (e.g. hono's `dist/index.js` re-exporting + // `./hono.js`, which re-exports `./hono-base.js`, …) would land + // in `ctx.js_modules` with only the entry file, leaving every + // transitive `./foo.js` to be resolved against disk at runtime — + // fine when node_modules/ is co-located with the binary, but + // produces a `Cannot resolve module` failure (and in some cases + // a downstream segfault when the missing-module callback returns + // an unboxed undefined to compiled native code) when the binary + // is shipped on its own. + // + // We deliberately collect imports via a lightweight regex scan + // rather than parsing every JS file through SWC. The bundler + // only needs to know what file paths to embed; runtime + // semantics (default vs named, conditional execution, dynamic + // import) are still V8's job. The regex catches all the static + // shapes we need to follow: + // import x from "./foo.js" + // import { a, b } from "./foo.js" + // import * as ns from "./foo.js" + // import "./side-effect.js" + // export { x } from "./foo.js" + // export * from "./foo.js" + // export * as ns from "./foo.js" + // Dynamic `import("./foo.js")` with a string-literal argument is + // also walked. Template-literal / variable specifiers can't be + // resolved statically and are skipped (V8 will surface the + // resolution failure at runtime, same as today). + let transitive_paths = collect_js_module_imports(&canonical, &source); ctx.js_modules.insert( specifier.clone(), JsModule { @@ -143,7 +264,23 @@ pub(super) fn collect_modules( ); ctx.needs_js_runtime = true; - // We don't parse JS/node_modules files for their imports (V8 will handle that at runtime) + // Recurse into each resolved sibling. We re-enter + // `collect_modules`, which re-runs the JS/native classification + // — covering the case where a JS file re-imports something that + // resolves to a TypeScript file under a `compilePackages` dir. + for next in transitive_paths { + collect_modules( + &next, + ctx, + visited, + enable_js_runtime, + format, + target, + next_class_id, + skip_transforms, + parse_cache.as_deref_mut(), + )?; + } return Ok(()); } diff --git a/test-files/test_hono_bundle.ts b/test-files/test_hono_bundle.ts new file mode 100644 index 0000000000..7167d1c40b --- /dev/null +++ b/test-files/test_hono_bundle.ts @@ -0,0 +1,14 @@ +// Issue #818: hono's `dist/index.js` re-exports from `./hono.js`, which +// re-exports from `./hono-base.js`, etc. — the V8-fallback bundle used +// to include only `index.js`, dropping ~20 transitive ESM submodules. +// This test exists to be run against a project that has `hono` in its +// node_modules; the parity / smoke suite doesn't install npm packages, +// so by itself the test only proves the compiler doesn't choke on the +// `import { Hono } from 'hono';` line. The real bundle-walks-recursively +// validation lives in /tmp/perry-hono in the PR notes. +import { Hono } from 'hono'; +const app = new Hono(); +app.get('/', (c) => c.text('Hi')); +console.log(typeof app); +console.log(typeof app.get); +console.log(typeof app.fetch);