diff --git a/CLAUDE.md b/CLAUDE.md
index 39dbd67118..e3e66f7353 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.1604
+**Current Version:** 0.5.1605
## TypeScript Parity Status
diff --git a/Cargo.lock b/Cargo.lock
index ebd1fecdee..1fbb5e01ea 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5623,7 +5623,7 @@ checksum = "1473d470930ed48574515a25df34900f3af89c6fa422d903e019121312a9f13e"
[[package]]
name = "perry"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -5687,7 +5687,7 @@ dependencies = [
[[package]]
name = "perry-api-manifest"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-dispatch",
"serde",
@@ -5695,7 +5695,7 @@ dependencies = [
[[package]]
name = "perry-audio-miniaudio"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"cc",
"libc",
@@ -5704,7 +5704,7 @@ dependencies = [
[[package]]
name = "perry-codegen"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"aho-corasick",
"anyhow",
@@ -5721,7 +5721,7 @@ dependencies = [
[[package]]
name = "perry-codegen-arkts"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"anyhow",
"perry-hir",
@@ -5729,7 +5729,7 @@ dependencies = [
[[package]]
name = "perry-codegen-glance"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"anyhow",
"perry-hir",
@@ -5737,7 +5737,7 @@ dependencies = [
[[package]]
name = "perry-codegen-js"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"anyhow",
"perry-dispatch",
@@ -5746,7 +5746,7 @@ dependencies = [
[[package]]
name = "perry-codegen-swiftui"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"anyhow",
"perry-hir",
@@ -5754,7 +5754,7 @@ dependencies = [
[[package]]
name = "perry-codegen-wasm"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -5766,7 +5766,7 @@ dependencies = [
[[package]]
name = "perry-codegen-wear-tiles"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"anyhow",
"perry-hir",
@@ -5774,7 +5774,7 @@ dependencies = [
[[package]]
name = "perry-container-compose"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"async-trait",
"clap",
@@ -5798,14 +5798,14 @@ dependencies = [
[[package]]
name = "perry-container-e2e"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"anyhow",
]
[[package]]
name = "perry-diagnostics"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"serde",
"serde_json",
@@ -5813,7 +5813,7 @@ dependencies = [
[[package]]
name = "perry-dispatch"
-version = "0.5.1604"
+version = "0.5.1605"
[[package]]
name = "perry-doc-fixture-my-bindings"
@@ -5824,7 +5824,7 @@ dependencies = [
[[package]]
name = "perry-doc-tests"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"anyhow",
"clap",
@@ -5839,7 +5839,7 @@ dependencies = [
[[package]]
name = "perry-ext-ads"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"block2",
"objc2",
@@ -5849,25 +5849,16 @@ dependencies = [
[[package]]
name = "perry-ext-argon2"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"argon2",
"perry-ffi",
"rand_core 0.6.4",
]
-[[package]]
-name = "perry-ext-axios"
-version = "0.5.1604"
-dependencies = [
- "perry-ffi",
- "reqwest",
- "tokio",
-]
-
[[package]]
name = "perry-ext-bcrypt"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"bcrypt",
"perry-ffi",
@@ -5875,7 +5866,7 @@ dependencies = [
[[package]]
name = "perry-ext-better-sqlite3"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-ffi",
"rusqlite",
@@ -5883,7 +5874,7 @@ dependencies = [
[[package]]
name = "perry-ext-cheerio"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-ffi",
"scraper",
@@ -5891,7 +5882,7 @@ dependencies = [
[[package]]
name = "perry-ext-commander"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-ffi",
"perry-runtime",
@@ -5899,7 +5890,7 @@ dependencies = [
[[package]]
name = "perry-ext-cron"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"chrono",
"cron",
@@ -5909,7 +5900,7 @@ dependencies = [
[[package]]
name = "perry-ext-dayjs"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"chrono",
"perry-ffi",
@@ -5917,7 +5908,7 @@ dependencies = [
[[package]]
name = "perry-ext-decimal"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-ffi",
"rust_decimal",
@@ -5925,7 +5916,7 @@ dependencies = [
[[package]]
name = "perry-ext-dotenv"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-ffi",
"serde_json",
@@ -5933,7 +5924,7 @@ dependencies = [
[[package]]
name = "perry-ext-ethers"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-ffi",
"rand 0.10.2",
@@ -5941,7 +5932,7 @@ dependencies = [
[[package]]
name = "perry-ext-events"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-ffi",
"perry-runtime",
@@ -5949,14 +5940,14 @@ dependencies = [
[[package]]
name = "perry-ext-exponential-backoff"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-ffi",
]
[[package]]
name = "perry-ext-fastify"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"bytes",
"http-body-util",
@@ -5973,7 +5964,7 @@ dependencies = [
[[package]]
name = "perry-ext-fetch"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"bytes",
"lazy_static",
@@ -5986,7 +5977,7 @@ dependencies = [
[[package]]
name = "perry-ext-http"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -6018,7 +6009,7 @@ dependencies = [
[[package]]
name = "perry-ext-ioredis"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"lazy_static",
"perry-ffi",
@@ -6028,7 +6019,7 @@ dependencies = [
[[package]]
name = "perry-ext-lru-cache"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"lru",
"perry-ffi",
@@ -6037,7 +6028,7 @@ dependencies = [
[[package]]
name = "perry-ext-moment"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"chrono",
"perry-ffi",
@@ -6045,7 +6036,7 @@ dependencies = [
[[package]]
name = "perry-ext-mongodb"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"bson",
"futures-util",
@@ -6057,7 +6048,7 @@ dependencies = [
[[package]]
name = "perry-ext-mysql2"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"chrono",
"perry-ffi",
@@ -6069,7 +6060,7 @@ dependencies = [
[[package]]
name = "perry-ext-nanoid"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"nanoid",
"perry-ffi",
@@ -6078,7 +6069,7 @@ dependencies = [
[[package]]
name = "perry-ext-net"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"bytes",
"perry-ffi",
@@ -6093,7 +6084,7 @@ dependencies = [
[[package]]
name = "perry-ext-node-forge"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"const-oid 0.10.2",
"der 0.8.2",
@@ -6112,7 +6103,7 @@ dependencies = [
[[package]]
name = "perry-ext-nodemailer"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"lettre",
"perry-ffi",
@@ -6122,7 +6113,7 @@ dependencies = [
[[package]]
name = "perry-ext-parcel-watcher"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"notify",
"perry-ffi",
@@ -6134,7 +6125,7 @@ dependencies = [
[[package]]
name = "perry-ext-pdf"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-ffi",
"printpdf",
@@ -6142,7 +6133,7 @@ dependencies = [
[[package]]
name = "perry-ext-pg"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-ffi",
"sqlx",
@@ -6151,7 +6142,7 @@ dependencies = [
[[package]]
name = "perry-ext-qs"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-ffi",
"perry-runtime",
@@ -6160,7 +6151,7 @@ dependencies = [
[[package]]
name = "perry-ext-ratelimit"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"governor",
"perry-ffi",
@@ -6168,7 +6159,7 @@ dependencies = [
[[package]]
name = "perry-ext-sharp"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"fast_image_resize",
"image",
@@ -6179,7 +6170,7 @@ dependencies = [
[[package]]
name = "perry-ext-streams"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"lazy_static",
"perry-ffi",
@@ -6188,7 +6179,7 @@ dependencies = [
[[package]]
name = "perry-ext-typescript"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"anyhow",
"perry-ffi",
@@ -6208,7 +6199,7 @@ dependencies = [
[[package]]
name = "perry-ext-undici"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-ffi",
"perry-runtime",
@@ -6217,7 +6208,7 @@ dependencies = [
[[package]]
name = "perry-ext-ws"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"futures-util",
"lazy_static",
@@ -6230,7 +6221,7 @@ dependencies = [
[[package]]
name = "perry-ext-zlib"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"brotli",
"flate2",
@@ -6240,7 +6231,7 @@ dependencies = [
[[package]]
name = "perry-ffi"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"dashmap 6.2.1",
"once_cell",
@@ -6250,7 +6241,7 @@ dependencies = [
[[package]]
name = "perry-hir"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"anyhow",
"perry-api-manifest",
@@ -6270,11 +6261,11 @@ dependencies = [
[[package]]
name = "perry-native-registration"
-version = "0.5.1604"
+version = "0.5.1605"
[[package]]
name = "perry-parser"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"anyhow",
"perry-diagnostics",
@@ -6287,7 +6278,7 @@ dependencies = [
[[package]]
name = "perry-perex"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perex",
"regex",
@@ -6295,7 +6286,7 @@ dependencies = [
[[package]]
name = "perry-runtime"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"ahash",
"base64 0.22.1",
@@ -6353,14 +6344,14 @@ dependencies = [
[[package]]
name = "perry-runtime-static"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-runtime",
]
[[package]]
name = "perry-stdlib"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"aes 0.8.4",
"aes 0.9.1",
@@ -6447,21 +6438,21 @@ dependencies = [
[[package]]
name = "perry-stdlib-static"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-stdlib",
]
[[package]]
name = "perry-transform"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"perry-hir",
]
[[package]]
name = "perry-ui"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"dirs",
"perry-ffi",
@@ -6471,7 +6462,7 @@ dependencies = [
[[package]]
name = "perry-ui-android"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"base64 0.22.1",
"jni",
@@ -6486,7 +6477,7 @@ dependencies = [
[[package]]
name = "perry-ui-geisterhand"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"rand 0.10.2",
"serde",
@@ -6496,7 +6487,7 @@ dependencies = [
[[package]]
name = "perry-ui-gtk4"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"base64 0.22.1",
"cairo-rs 0.22.9",
@@ -6519,7 +6510,7 @@ dependencies = [
[[package]]
name = "perry-ui-ios"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"base64 0.22.1",
"block2",
@@ -6536,7 +6527,7 @@ dependencies = [
[[package]]
name = "perry-ui-macos"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"base64 0.22.1",
"block2",
@@ -6553,7 +6544,7 @@ dependencies = [
[[package]]
name = "perry-ui-model"
-version = "0.5.1604"
+version = "0.5.1605"
[[package]]
name = "perry-ui-test"
@@ -6564,11 +6555,11 @@ dependencies = [
[[package]]
name = "perry-ui-testkit"
-version = "0.5.1604"
+version = "0.5.1605"
[[package]]
name = "perry-ui-tvos"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"base64 0.22.1",
"block2",
@@ -6585,7 +6576,7 @@ dependencies = [
[[package]]
name = "perry-ui-visionos"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"base64 0.22.1",
"block2",
@@ -6602,7 +6593,7 @@ dependencies = [
[[package]]
name = "perry-ui-watchos"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"block2",
"libc",
@@ -6616,7 +6607,7 @@ dependencies = [
[[package]]
name = "perry-ui-windows"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"base64 0.22.1",
"libc",
@@ -6635,7 +6626,7 @@ dependencies = [
[[package]]
name = "perry-ui-windows-winui"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"base64 0.22.1",
"libc",
@@ -6648,7 +6639,7 @@ dependencies = [
[[package]]
name = "perry-updater"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -6663,7 +6654,7 @@ dependencies = [
[[package]]
name = "perry-wasm-host"
-version = "0.5.1604"
+version = "0.5.1605"
dependencies = [
"wasmi",
]
diff --git a/Cargo.toml b/Cargo.toml
index b8cf098885..f5a536bf9d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,7 +20,6 @@ members = [
"crates/perry-ext-better-sqlite3",
"crates/perry-ext-zlib",
"crates/perry-ext-exponential-backoff",
- "crates/perry-ext-axios",
"crates/perry-ext-events",
"crates/perry-ext-decimal",
"crates/perry-ext-dayjs",
@@ -334,7 +333,7 @@ codegen-units = 1
codegen-units = 1
[workspace.package]
-version = "0.5.1604"
+version = "0.5.1605"
edition = "2021"
license = "MIT"
repository = "https://github.com/PerryTS/perry"
@@ -482,7 +481,6 @@ perry-ext-lru-cache = { path = "crates/perry-ext-lru-cache" }
perry-ext-better-sqlite3 = { path = "crates/perry-ext-better-sqlite3" }
perry-ext-zlib = { path = "crates/perry-ext-zlib" }
perry-ext-exponential-backoff = { path = "crates/perry-ext-exponential-backoff" }
-perry-ext-axios = { path = "crates/perry-ext-axios" }
perry-ext-events = { path = "crates/perry-ext-events" }
perry-ext-decimal = { path = "crates/perry-ext-decimal" }
perry-ext-dayjs = { path = "crates/perry-ext-dayjs" }
diff --git a/changelog.d/10649-stream-subclass-heritage.md b/changelog.d/10649-stream-subclass-heritage.md
new file mode 100644
index 0000000000..5e9bb57991
--- /dev/null
+++ b/changelog.d/10649-stream-subclass-heritage.md
@@ -0,0 +1,10 @@
+### Fixed
+
+- **`node:stream` subclass overrides (`_transform`/`_write`/`_read`) are no
+ longer ignored when the heritage reaching `class X extends ` is a
+ local alias, an indirect subclass, a class expression, or a CJS
+ destructured `require('stream')` — the shape nodemailer uses in every
+ stream class it defines. `write()`/`push()` used to throw
+ `ERR_METHOD_NOT_IMPLEMENTED` because the override was never installed on
+ `this`; the dynamic `super()` dispatch now recognizes the resolved
+ bound-export value regardless of how the heritage expression reached it.
diff --git a/changelog.d/10679-axios-native-binding-removal.md b/changelog.d/10679-axios-native-binding-removal.md
new file mode 100644
index 0000000000..0d4237bf90
--- /dev/null
+++ b/changelog.d/10679-axios-native-binding-removal.md
@@ -0,0 +1,63 @@
+Removed the axios native binding (`crates/perry-ext-axios`, `perry-stdlib/src/axios.rs`,
+and their `js_axios_*` FFI surface) so `import axios from "axios"` resolves to the real
+npm package instead of Perry's hand-written reimplementation. First package removed under
+the owner's decision to delete native bindings rather than let them drift from upstream
+behavior — the motivating case was `jsonwebtoken.verify` silently accepting forged tokens.
+
+**Base branch note**: this PR must not merge before #10673 (the `agent-base`
+namespace/`export =` fallback fix) — axios's `https-proxy-agent` dependency needs that
+fix to compile at all.
+
+**What was removed**: the `NATIVE_MODULES` entry and manifest method rows (`entries.rs`,
+`entries/part_4.rs`), the `[bindings.axios]` block in `well_known_bindings.toml`, the
+`perry-ext-axios` crate and its `Cargo.toml`/workspace-member registration, the
+`crates/perry-stdlib/src/axios.rs` implementation and its `common/dispatch/property_dispatch.rs`
+response-property dispatch arm, the codegen dispatch for axios's static HTTP methods and
+response-property access (`lower_call/options/fetch.rs`), the `js_axios_*` FFI declarations
+(`runtime_decls/stdlib_ffi/third_party.rs`) and their runtime/no-op-stub implementations
+(`perry-runtime/src/closure/v8_stubs.rs`, `perry-ui-android/src/stdlib_stubs.rs`), the
+HIR "axios.get/post/… returns a Response" local-instance tagging used only to route
+`.status`/`.data`/`.statusText` through the now-deleted native dispatch (`local_natives.rs`,
+`destructuring/var_decl/native_new.rs`, `lower/module_decl.rs`, `lower/stmt.rs`), the
+`emit.rs` "callable default export" special-case that existed only for axios's `axios(config)`
+shape, and the `workspace-architecture.json` entry for the deleted crate.
+
+Also removed: the axios-only HIR test (`axios_response_property_lowering.rs`) and the
+native-dispatch NaN-boxing regression test (`test_issue_340_axios_response_props.ts`,
+plus its stale `known_failures.json` entry) — both tested internals of the deleted native
+shim and have no analog against real-source axios. Swapped `unimplemented_api_check.rs`'s
+`supported_module_with_unknown_member_is_rejected` regression witness from axios to
+node-fetch (still a `NATIVE_MODULES` member) since it needs a live native module to
+demonstrate the #513 invariant. Regenerated `docs/api/perry.d.ts`, `docs/src/api/reference.md`,
+and `docs/src/native-libraries/governance.md`'s table; dropped axios's row/section from
+`docs/native-libraries.md` and its line from the `perry native list` example in
+`docs/src/cli/commands.md`. Updated `workspace-architecture.json`'s baseline counts
+(`workspace_members` 83→82, `decision_counts.externalize` 33→32) for the removed crate.
+
+**Acceptance test — what a plain axios import needs**: a plain `import axios from "axios"`
+with **no `perry.compilePackages` entry for axios at all** compiles and runs correctly —
+verified against a live `node:http` GET/POST round-trip (`tests/release/packages/axios-get`,
+whose `package.json` already had no `compilePackages` block and needed no new one) and a
+second from-scratch repro. Perry's default "compile npm package source when no native
+binding claims the specifier" path picks up axios and its full transitive dependency graph
+(agent-base, https-proxy-agent, follow-redirects, form-data, combined-stream, mime-types,
+debug, and the rest — the same ~26 packages the earlier `perry.compilePackages`-forced
+probe used) automatically; `perry compile` reports "130 module(s): 130 native, 0 JavaScript"
+for them. **No `package.json` configuration beyond a plain `"axios"` dependency + `npm
+install` is required** — the `perry.compilePackages` list from the original probe is not
+needed post-removal.
+
+Validated: `cargo test` green for `perry-api-manifest` (41), `perry-hir` (all suites, 0
+failed), `perry-stdlib` (139, `RUST_TEST_THREADS=1`), `perry` (1136 lib tests + the 5
+integration tests whose comments mention axios: `incoming_message_pipe`,
+`issue_10662_namespace_export_equals_fallback`, `issue_5174_headers_http_pump_hang`,
+`response_stream_body_pull` — none depend on axios functionally, all green); the full
+`crates/perry/tests/*.rs` integration sweep was not run (unrelated to this change, and
+each fixture takes ~3 min on the shared build host — CI's `e2e-scoped` only runs
+integration tests named by the diff, which is none here). `run_lint_gates.sh
+SKIP_COMPILE_GATES=1`: 76 of 77 script gates pass; the one red
+("Public benchmark evidence freshness") is pre-existing on every PR. `cargo fmt --all --
+--check` clean. Two pre-existing, unrelated breakages on the base branch (confirmed via
+`git stash` A/B, not touched here): `perry-codegen`'s test target fails to compile
+(`ImportedClass` missing a field in two unrelated test files) and `perry-runtime`'s
+`--tests` build carries one pre-existing dead-code warning in `box.rs`.
diff --git a/changelog.d/10747-box-captured-local-types.md b/changelog.d/10747-box-captured-local-types.md
new file mode 100644
index 0000000000..9f909c4fc1
--- /dev/null
+++ b/changelog.d/10747-box-captured-local-types.md
@@ -0,0 +1,57 @@
+### Fixed
+
+An array local captured by a nested closure read back `undefined` in its own
+declaring scope, once `Array.prototype` had ever carried an indexed property.
+The closure kept seeing the correct array, so the two storages for one binding
+disagreed and `peek() === dest` was false. The trigger is rare — something must
+put an indexed property on `Array.prototype`, which arms a monotone deopt latch
+that deleting the property does not clear — but the affected shape, an array
+local captured by a closure, is ordinary code, and the symptom is a silently
+empty-looking variable rather than a crash.
+
+`0ed806587c` (#10488) added a `ctx.local_types` refresh to the redeclaration
+branch of `lower_let`, so `is_numeric_expr` and `static_type_of` would stop
+disagreeing about a hoisted `var`. **It fixed that desync for hoisted `var` and
+introduced a new one for box-captured locals.** A captured local reaches that
+same branch without any redeclaration in the source: `Stmt::PreallocateBoxes`
+registers the id up front, so its one real `Stmt::Let` finds `ctx.locals`
+already populated and lands there. For such an id the refined type describes the
+VALUE while the slot holds a box pointer, and the `local_types` readers then
+lower reads as raw local loads instead of going through `js_box_get_bits`.
+
+The fix skips the refresh when `ctx.boxed_vars` holds the id — an existing
+`FnCtx` field already in scope, documented as the set whose `LocalGet` unboxes
+through `js_box_get_bits`. A hoisted `var` is unboxed unless separately
+captured, so #10488 keeps its fix; `test_gap_10488_var_array_void_compare` and
+both `let_stmt_var_redeclare_tests` stay green.
+
+A box-captured local now keeps whatever type the predefine recorded, normally
+`Any`, so it can lose a fast path. That is the conservative direction — the cost
+is a fast path, never correctness — and it is not a regression against any
+working behaviour: before `0ed806587c` these locals got no refinement at all,
+and after it they got one that described the value while the slot held a box
+pointer. If refinement for captured locals is wanted later, the right shape is a
+box-aware type, not this line.
+
+Found via `test_gap_array_side_mask_covers_a_pointer_stored_at_a_late_index`,
+which regressed in merge train 219. Despite its name and its companions living
+in `gc/tests/`, it is not a GC bug: reading the slot *before* the `gc()` showed
+it already `undefined`, which removed the collector in one probe. `slice`
+passing while `splice` failed was an ordering artefact — the first invocation in
+a process passes because the latch is not yet armed. Attributed by bisect over
+six builds, each with its runtime stamp checked against the commit under test,
+narrowing train 219 to `0ed806587c`. A plain revert was measured and rejected:
+it fixes this bug but reds `test_gap_10488`, moving the failure rather than
+clearing it.
+
+Verified on one 871-fixture gap-suite invocation: 864 pass, 7 fail, 0 compile
+fail, 0 crash. All 7 are pre-existing — the six standing `gap_snapshot.json`
+entries in exactly their recorded state, plus `test_gap_9592_child_timeout_threads`
+(#10730, a macOS fixture-portability bug). `test_gap_10488_var_array_void_compare`
+(position 46), the new `test_gap_10727_captured_array_local_proto_index` (59) and
+`test_gap_array_side_mask_...` (353) all passed in that same pass.
+
+Note for the next change to `crates/perry-codegen/src/stmt/let_stmt.rs`: it is
+now at exactly 2000 lines, the `check_file_size.sh` cap. It was at 1999 before
+this change and the combined #10488/#10727 comment was condensed to fit the
+guard in at net +1 line. Anything further needs the file split first.
diff --git a/changelog.d/10749-require-main-entry-only.md b/changelog.d/10749-require-main-entry-only.md
new file mode 100644
index 0000000000..7c7cb43655
--- /dev/null
+++ b/changelog.d/10749-require-main-entry-only.md
@@ -0,0 +1,32 @@
+Fixed `require.main === module` being trivially `true` in **every** compiled
+CommonJS module, not just the process entry point (#10735).
+`cjs_wrap`'s preamble unconditionally emitted `require.main = module;`, so
+the standard "am I the entry, or merely imported?" idiom took its CLI
+branch in every dependency that used it — including bundled packages
+(dotenv 18.0.1's `dist/index.cjs` is a confirmed real-world example).
+
+The fix threads the compiler's existing entry-module knowledge (the same
+comparison `import.meta.main` uses) through `cjs_wrap`, but that alone is
+insufficient: `cjs_wrap` transpiles a statically-known
+`require('./relative')` into a hoisted ESM import, and ESM import
+evaluation runs a module's static-import dependencies *before* the
+importing module's own top-level code. So a CJS entry's own dependencies
+initialize before the entry's own preamble runs, which means "the entry
+publishes `require.main` in its own preamble" is too late for any
+dependency reached via a hoisted static import. Fixed by publishing a
+placeholder object as the shared "main module" from the program's `main()`
+itself, before any module's `__init` runs at all (gated on the entry being
+CJS-wrapped, so an ESM entry correctly leaves `require.main` `undefined`
+for CJS modules it imports); the entry later reclaims that exact object
+and fills in its real fields, preserving identity for dependencies that
+captured `require.main` before the entry's own code ran.
+
+The new runtime-side cache backing this (`CJS_MAIN_MODULE`) is registered
+with the GC's mutable-root-scanner machinery and verified under forced
+evacuation (`PERRY_GC_SCHEDULE_SEED`/`PERRY_GC_FORCE_EVACUATE`/
+`PERRY_GC_PROTECT_FROMSPACE`): the placeholder moved, the cache followed
+it, and every identity assertion held across 8,006 forced collections. A
+new test (`gc::tests::cjs_main_module`) asserts the rewrite counter is
+non-zero under a real evacuating minor, so a future regression that stops
+rewriting the cache fails a test instead of silently reintroducing this
+bug's failure mode.
diff --git a/changelog.d/PENDINGCS-compile-smoke-known.md b/changelog.d/PENDINGCS-compile-smoke-known.md
index 0bbb9266d0..3f9ccf1f0e 100644
--- a/changelog.d/PENDINGCS-compile-smoke-known.md
+++ b/changelog.d/PENDINGCS-compile-smoke-known.md
@@ -1,15 +1,18 @@
-**ci: tolerate two known compile-smoke failures (#9470)**
+**ci: tolerate one known compile-smoke failure (#9470)**
`compile-smoke` is in `full-suite-gate`'s `needs` and exits on `FAIL -gt 0` with
-no allowlist, so two long-standing failures were blocking **every** release cut:
+no allowlist, so a long-standing failure was blocking **every** release cut:
```
-Compile smoke: 1359 passed, 2 failed, 67 skipped
-- test_issue_340_axios_response_props
+Compile smoke: 1360 passed, 1 failed, 67 skipped
- test_issue_414_mysql_query_params
```
-Both are the tokio-coherence refusal. Auto-optimize rebuilds the stdlib static
+(`test_issue_340_axios_response_props` — the other case this note originally
+tracked — was removed along with the native axios binding; see
+`changelog.d/10679-axios-native-binding-removal.md`, PR #10679.)
+
+It is the tokio-coherence refusal. Auto-optimize rebuilds the stdlib static
into `target/perry-auto-/` **without** the ext wrappers in the same cargo
invocation (`optimized_libs/driver.rs:846-857` passes only
`-p perry-runtime-static -p perry-stdlib-static --no-default-features`), so
@@ -32,7 +35,7 @@ The list is self-policing, verified in all four directions:
| case | result |
|---|---|
-| exactly the 2 known | passes |
+| exactly the 1 known | passes |
| known + a NEW failure | **fails**, naming the new one |
| a known one now passes | **fails** as a stale entry |
| all pass | **fails** as stale entries |
diff --git a/crates/perry-api-manifest/src/emit.rs b/crates/perry-api-manifest/src/emit.rs
index 5a3945493b..d42c99dc54 100644
--- a/crates/perry-api-manifest/src/emit.rs
+++ b/crates/perry-api-manifest/src/emit.rs
@@ -203,8 +203,6 @@ pub fn emit_dts(_perry_version: &str) -> String {
// Followup under #466 will tighten this when signature data lands.
let mut emitted_fn_names: std::collections::HashSet<&str> =
std::collections::HashSet::new();
- let mut emitted_static_methods = Vec::new();
- let mut callable_default = None;
for e in entries.iter().filter(|e| {
matches!(
e.kind,
@@ -221,24 +219,19 @@ pub fn emit_dts(_perry_version: &str) -> String {
continue;
}
if e.name == "default" {
- if *module == "axios" {
- callable_default = Some(*e);
- } else {
- let _ = writeln!(out, " /** {}{} */", source_dts_tag(e), stub_dts_suffix(e));
- let _ = writeln!(out, " export default function {};", render_signature(e));
- }
+ let _ = writeln!(out, " /** {}{} */", source_dts_tag(e), stub_dts_suffix(e));
+ let _ = writeln!(out, " export default function {};", render_signature(e));
continue;
}
- emitted_static_methods.push(*e);
let _ = writeln!(out, " /** {}{} */", source_dts_tag(e), stub_dts_suffix(e));
let signature = render_signature(e);
if is_ts_reserved_word(e.name) {
- // Reserved words (e.g. `axios.delete`) can't appear as
- // a function declaration's name — `tsc` rejects
- // `export function delete(...)` with TS1359 (#526).
- // Declare under an underscored alias and re-export with
- // the original name; the `as ` rename slot
- // accepts arbitrary identifiers.
+ // Reserved words (e.g. a method literally named `delete`)
+ // can't appear as a function declaration's name — `tsc`
+ // rejects `export function delete(...)` with TS1359
+ // (#526). Declare under an underscored alias and
+ // re-export with the original name; the `as `
+ // rename slot accepts arbitrary identifiers.
let alias = format!("_{}", e.name);
let _ = writeln!(out, " function {}{};", alias, signature);
let _ = writeln!(out, " export {{ {} as {} }};", alias, e.name);
@@ -247,35 +240,6 @@ pub fn emit_dts(_perry_version: &str) -> String {
}
}
- if let Some(default) = callable_default {
- let _ = writeln!(
- out,
- " /** {}{} */",
- source_dts_tag(default),
- stub_dts_suffix(default)
- );
- if emitted_static_methods.is_empty() {
- let _ = writeln!(
- out,
- " export default function {};",
- render_signature(default)
- );
- } else {
- let signature = render_signature(default).replacen("): ", ") => ", 1);
- let _ = writeln!(out, " const _default: ({}) & {{", signature);
- for method in emitted_static_methods {
- let target = if is_ts_reserved_word(method.name) {
- format!("_{}", method.name)
- } else {
- ts_ident(method.name)
- };
- let _ = writeln!(out, " {}: typeof {};", ts_ident(method.name), target);
- }
- let _ = writeln!(out, " }};");
- let _ = writeln!(out, " export default _default;");
- }
- }
-
let _ = writeln!(out, "}}");
let _ = writeln!(out);
}
@@ -806,47 +770,6 @@ mod tests {
);
}
- /// #526 acceptance: a method named after a TS reserved word must
- /// not surface as `export function (...)` — `tsc` errors
- /// out with TS1359. The emitter routes through the
- /// `function _delete; export { _delete as delete }` alias pattern
- /// so a fresh `perry init` project's `tsc -p .` succeeds.
- #[test]
- fn dts_axios_delete_does_not_use_reserved_word_as_fn_name() {
- let dts = emit_dts("test");
- let block_start = dts.find("declare module \"axios\"").expect("axios block");
- let after = &dts[block_start..];
- let block_end = after.find("\n}\n").expect("block end");
- let block = &after[..block_end];
- assert!(
- !block.contains("export function delete("),
- "axios.delete must not be emitted as `export function delete(` (TS1359)\nblock: {}",
- block
- );
- assert!(
- block.contains("function _delete(") && block.contains("_delete as delete"),
- "axios.delete should use the `function _delete; export {{ _delete as delete }}` \
- alias pattern\nblock: {}",
- block
- );
- }
-
- #[test]
- fn dts_axios_default_export_exposes_static_methods() {
- let dts = emit_dts("test");
- let block_start = dts.find("declare module \"axios\"").expect("axios block");
- let after = &dts[block_start..];
- let block_end = after.find("\n}\n").expect("block end");
- let block = &after[..block_end];
- assert!(
- block.contains("get: typeof get;")
- && block.contains("delete: typeof _delete;")
- && block.contains("export default _default;"),
- "the callable axios default must expose its static methods\nblock: {}",
- block
- );
- }
-
/// Defense-in-depth for #526: every reserved word the emitter
/// recognizes should round-trip through the alias pattern, so
/// future manifest additions (e.g. `axios.try`, `axios.new`) don't
diff --git a/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs
index 09604c36e7..7ea8ac5320 100644
--- a/crates/perry-api-manifest/src/entries.rs
+++ b/crates/perry-api-manifest/src/entries.rs
@@ -39,7 +39,6 @@ pub const NATIVE_MODULES: &[&str] = &[
// iovalkey: the Valkey fork of ioredis (valkey-io/iovalkey), served by the
// same perry-ext-ioredis surface — see well_known_bindings.toml.
"iovalkey",
- "axios", // HTTP client (routes onto the native fetch/http stack)
"node-fetch", // WHATWG fetch client
"ws", // WebSocket client/server
"zlib", // (Node builtin) gzip/deflate/brotli/zstd compression
diff --git a/crates/perry-api-manifest/src/entries/part_4.rs b/crates/perry-api-manifest/src/entries/part_4.rs
index e16285f1d1..ea6315342a 100644
--- a/crates/perry-api-manifest/src/entries/part_4.rs
+++ b/crates/perry-api-manifest/src/entries/part_4.rs
@@ -277,20 +277,6 @@ pub(crate) const API_MANIFEST_PART_4: &[ApiEntry] = &[
// method surface; only the constructor's default protocol differs.
class("https", "Agent"),
method("https", "Agent", false, None),
- // --- axios (perry-ext-axios) — the npm `axios` HTTP client surface.
- // The default export is callable (`axios(config)`); both flow
- // through perry-ext-axios's `js_axios_*` symbols. ---
- method("axios", "default", false, None),
- method("axios", "get", false, None),
- method("axios", "post", false, None),
- method("axios", "put", false, None),
- method("axios", "delete", false, None),
- method("axios", "patch", false, None),
- method("axios", "head", false, None),
- method("axios", "options", false, None),
- method("axios", "request", false, None),
- method("axios", "create", false, None),
- method("axios", "all", false, None),
// --- node-fetch (perry-ext-fetch) — also exposes the Web Fetch
// API classes (Headers, Request, Response, Blob, FormData). ---
method("node-fetch", "default", false, None),
diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs
index 484c6d0176..17e6f1d24b 100644
--- a/crates/perry-codegen/src/codegen/entry.rs
+++ b/crates/perry-codegen/src/codegen/entry.rs
@@ -749,6 +749,14 @@ pub(super) fn compile_module_entry(
],
);
}
+ // #10735: publish the shared `require.main` placeholder before
+ // ANY module's `__init` below runs (those are this CJS entry's
+ // OWN static imports, which ESM eval order runs before the
+ // entry's own preamble). See the callee's doc comment. Skipped
+ // for an ESM entry, which must leave `require.main` `undefined`.
+ if crate::collectors::is_cjs_wrapped_module(hir) {
+ blk.call_void("js_bootstrap_cjs_main_module_placeholder", &[]);
+ }
for (index, prefix) in non_entry_module_prefixes.iter().enumerate() {
if cross_module.deferred_module_prefixes.contains(prefix) {
continue;
diff --git a/crates/perry-codegen/src/lower_call/native/native_runtime_branch.rs b/crates/perry-codegen/src/lower_call/native/native_runtime_branch.rs
index 5fa5d09881..4c2b89861c 100644
--- a/crates/perry-codegen/src/lower_call/native/native_runtime_branch.rs
+++ b/crates/perry-codegen/src/lower_call/native/native_runtime_branch.rs
@@ -173,6 +173,23 @@
.block()
.call(DOUBLE, "js_has_path_module", &[(DOUBLE, &path)]));
}
+ // #10735: publish/read the shared CJS "main module" —
+ // `require.main` for the process entry module vs. for every
+ // other CJS-wrapped module. See `cjs_wrap/wrap.rs` and
+ // `perry-runtime::module_require::{js_set_cjs_main_module,
+ // js_get_cjs_main_module}`.
+ "setCjsMainModule" => {
+ let module = args.first().map_or_else(
+ || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))),
+ |arg| lower_expr(ctx, arg),
+ )?;
+ ctx.block()
+ .call_void("js_set_cjs_main_module", &[(DOUBLE, &module)]);
+ return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)));
+ }
+ "getCjsMainModule" => {
+ return Ok(ctx.block().call(DOUBLE, "js_get_cjs_main_module", &[]));
+ }
// #10360: seeded into every module init under `--platform bun`
// so the runtime can follow Bun where its Web APIs differ from
// Node's (e.g. the Response null-body-status check).
diff --git a/crates/perry-codegen/src/lower_call/options/fetch.rs b/crates/perry-codegen/src/lower_call/options/fetch.rs
index 63e1f6dd84..103c502d49 100644
--- a/crates/perry-codegen/src/lower_call/options/fetch.rs
+++ b/crates/perry-codegen/src/lower_call/options/fetch.rs
@@ -10,7 +10,7 @@ use anyhow::Result;
use perry_hir::Expr;
use super::get_raw_string_ptr;
-use crate::expr::{lower_expr, nanbox_pointer_inline, nanbox_string_inline, unbox_to_i64, FnCtx};
+use crate::expr::{lower_expr, nanbox_pointer_inline, nanbox_string_inline, FnCtx};
use crate::nanbox::double_literal;
use crate::types::{DOUBLE, I1, I64};
@@ -157,61 +157,6 @@ pub(in crate::lower_call) fn lower_fetch_native_method(
}
}
- // ── axios: static HTTP method calls ──
- // Must be before the receiver guard — these are receiver-less calls.
- if module == "axios" && object.is_none() {
- let url_box = if !args.is_empty() {
- lower_expr(ctx, &args[0])?
- } else {
- double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
- };
- let blk = ctx.block();
- let url_handle = unbox_to_i64(blk, &url_box);
- match method {
- "get" | "head" | "options" => {
- let rt_fn = match method {
- "get" => "js_axios_get",
- "head" => "js_axios_head",
- _ => "js_axios_options",
- };
- let promise = blk.call(I64, rt_fn, &[(I64, &url_handle)]);
- return Ok(Some(nanbox_pointer_inline(blk, &promise)));
- }
- "delete" => {
- let promise = blk.call(I64, "js_axios_delete", &[(I64, &url_handle)]);
- return Ok(Some(nanbox_pointer_inline(blk, &promise)));
- }
- "post" | "put" | "patch" => {
- // #598: pass the body as a NaN-boxed f64 instead of
- // unboxing to i64. Pre-fix the unbox produced a raw
- // pointer the runtime read as `*const StringHeader`
- // — for an object literal the pointer was a real
- // ObjectHeader, the runtime read its bytes as a
- // StringHeader (length / refcount / data prefix),
- // and the request body became `^@^B^@^@H...` (the
- // ObjectHeader struct followed by the first character
- // of the stringified field). The runtime side now
- // detects strings vs everything-else via the NaN-box
- // tag and routes through `js_json_stringify`.
- let body_box = if args.len() > 1 {
- lower_expr(ctx, &args[1])?
- } else {
- double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
- };
- let rt_fn = match method {
- "post" => "js_axios_post",
- "put" => "js_axios_put",
- _ => "js_axios_patch",
- };
- let promise =
- ctx.block()
- .call(I64, rt_fn, &[(I64, &url_handle), (DOUBLE, &body_box)]);
- return Ok(Some(nanbox_pointer_inline(ctx.block(), &promise)));
- }
- _ => {}
- }
- }
-
// Web Streams static factories.
if module == "readable_stream" && object.is_none() && method == "from" {
let iterable = if !args.is_empty() {
@@ -1259,44 +1204,5 @@ pub(in crate::lower_call) fn lower_fetch_native_method(
}
}
- // ── axios: response property access (response.status, .data, .statusText, .headers) ──
- if module == "axios" {
- if let Some(recv) = object {
- let recv_handle = lower_expr(ctx, recv)?;
- let blk = ctx.block();
- // The awaited axios response is a Handle (i64) NaN-boxed via
- // `JsValue::from_object_ptr(handle as *mut ())` (POINTER_TAG |
- // (handle & POINTER_MASK)). Use `unbox_to_i64` to strip the
- // tag and recover the bare handle id; calling
- // `bitcast_double_to_i64` alone leaves the upper-16 tag bits
- // and the runtime's `get_handle::` lookup
- // misses, returning 0 / undefined for every property. (#604
- // followup — only surfaced once the listen() hang was fixed.)
- let h_i64 = unbox_to_i64(blk, &recv_handle);
- match method {
- "status" => {
- let status = blk.call(DOUBLE, "js_axios_response_status", &[(I64, &h_i64)]);
- return Ok(Some(status));
- }
- "statusText" => {
- let str_ptr = blk.call(I64, "js_axios_response_status_text", &[(I64, &h_i64)]);
- return Ok(Some(nanbox_string_inline(blk, &str_ptr)));
- }
- "data" => {
- // Use the auto-parsed variant (JSON when the body
- // looks like JSON, raw string otherwise) so
- // `r.data.ok` / `r.data[0]` work the same way as
- // in npm `axios`. The function returns a NaN-boxed
- // f64 directly; no need to nanbox here. (#604
- // followup — only surfaced once listen() hang fix
- // unblocked the axios chain.)
- let v = blk.call(DOUBLE, "js_axios_response_data_parsed", &[(I64, &h_i64)]);
- return Ok(Some(v));
- }
- _ => {}
- }
- }
- }
-
Ok(None)
}
diff --git a/crates/perry-codegen/src/runtime_decls/objects.rs b/crates/perry-codegen/src/runtime_decls/objects.rs
index e8f1bfd637..d1b723a377 100644
--- a/crates/perry-codegen/src/runtime_decls/objects.rs
+++ b/crates/perry-codegen/src/runtime_decls/objects.rs
@@ -409,6 +409,15 @@ pub fn declare_phase_b_objects(module: &mut LlModule) {
module.declare_function("js_has_path_module", DOUBLE, &[DOUBLE]);
// #10360: `--platform bun` marker (see `__perry_runtime.setBunPlatform`).
module.declare_function("js_set_bun_platform", VOID, &[]);
+ // #10735: shared CJS "main module" (`require.main`) — the entry module
+ // publishes, every other CJS module reads back. See
+ // `__perry_runtime.setCjsMainModule` / `.getCjsMainModule`.
+ module.declare_function("js_set_cjs_main_module", VOID, &[DOUBLE]);
+ module.declare_function("js_get_cjs_main_module", DOUBLE, &[]);
+ // #10735: allocates and publishes the placeholder `require.main` object
+ // BEFORE any module's `__init` runs (called directly from `main()`, not
+ // from generated module JS — see `codegen::entry::compile_module_entry`).
+ module.declare_function("js_bootstrap_cjs_main_module_placeholder", VOID, &[]);
// Next.js wall 54 (part 2): register a Deferred module's `__init` address by
// path so a runtime `require(absolutePath)` can trigger its lazy init.
module.declare_function("js_register_path_init", VOID, &[PTR, I64, I64]);
diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs
index c600e17d87..05bf3a210f 100644
--- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs
+++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs
@@ -25,7 +25,7 @@ use web::declare_web;
/// Stdlib / FFI runtime functions. Without these declarations, user code
/// that touches any of the third-party stdlib modules (http, mysql2, pg,
-/// redis, mongodb, bcrypt, jsonwebtoken, axios, sharp, cron, WebSocket,
+/// redis, mongodb, bcrypt, jsonwebtoken, sharp, cron, WebSocket,
/// zlib, etc.) emits `use of undefined value '@js_*'` at clang -c time
/// because the IR references the name without a preceding `declare`.
///
@@ -36,7 +36,7 @@ pub fn declare_stdlib_ffi(module: &mut LlModule) {
declare_net_http(module);
// PostgreSQL, Redis/ioredis, MongoDB, SQLite, OS, Crypto, Nanoid.
declare_data_stores(module);
- // bcrypt/argon2, perry/ads, perry/thread, JWT, axios, sharp, cron,
+ // bcrypt/argon2, perry/ads, perry/thread, JWT, sharp, cron,
// async_hooks/AsyncLocalStorage, DisposableStack, zlib, Buffer,
// child_process, cheerio.
declare_third_party(module);
diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/third_party.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/third_party.rs
index ff029c6f90..22d20a3b74 100644
--- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/third_party.rs
+++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/third_party.rs
@@ -1,5 +1,5 @@
//! Third-party package stdlib FFI declarations (extracted from stdlib_ffi.rs):
-//! bcrypt/argon2, perry/ads, perry/thread, jsonwebtoken, axios, sharp, cron,
+//! bcrypt/argon2, perry/ads, perry/thread, jsonwebtoken, sharp, cron,
//! async_hooks/AsyncLocalStorage, DisposableStack, zlib, Buffer, child_process,
//! cheerio.
@@ -63,30 +63,13 @@ pub(crate) fn declare_third_party(module: &mut LlModule) {
module.declare_function("js_perry_native_f32", DOUBLE, &[DOUBLE]);
module.declare_function("js_perry_native_f64", DOUBLE, &[DOUBLE]);
- // ========== axios / node-fetch ==========
- module.declare_function("js_axios_create", DOUBLE, &[I64]);
- module.declare_function("js_axios_delete", I64, &[I64]);
- module.declare_function("js_axios_get", I64, &[I64]);
- module.declare_function("js_axios_head", I64, &[I64]);
- module.declare_function("js_axios_options", I64, &[I64]);
+ // ========== node-fetch ==========
// #598: body arg is a NaN-boxed f64 (DOUBLE) so the runtime can
// distinguish strings from objects via the tag and JSON.stringify
// non-string bodies. Pre-fix this was I64 (raw unboxed pointer)
- // which had no way to tell `axios.post(url, "raw json")` from
- // `axios.post(url, {a: 1})`.
- module.declare_function("js_axios_post", I64, &[I64, DOUBLE]);
- module.declare_function("js_axios_put", I64, &[I64, DOUBLE]);
- module.declare_function("js_axios_patch", I64, &[I64, DOUBLE]);
- module.declare_function("js_axios_request", I64, &[I64]);
- module.declare_function("js_axios_response_status", DOUBLE, &[I64]);
- module.declare_function("js_axios_response_status_text", I64, &[I64]);
- module.declare_function("js_axios_response_data", I64, &[I64]);
// Issue #604 followup — JSON-auto-parsing variant of `.data`. Returns
// a NaN-boxed JSValue (parsed object/array/number/bool/null when the
// response body is JSON, raw string otherwise) so `r.data.ok` works
- // the same way as npm `axios` does for `application/json` responses.
- module.declare_function("js_axios_response_data_parsed", DOUBLE, &[I64]);
-
// ========== sharp / image ==========
module.declare_function("js_sharp_auto_orient", I64, &[I64]);
module.declare_function("js_sharp_avif", I64, &[I64, DOUBLE]);
diff --git a/crates/perry-codegen/src/stmt/let_stmt.rs b/crates/perry-codegen/src/stmt/let_stmt.rs
index c2d71f73ac..e99810badc 100644
--- a/crates/perry-codegen/src/stmt/let_stmt.rs
+++ b/crates/perry-codegen/src/stmt/let_stmt.rs
@@ -383,22 +383,22 @@ pub(crate) fn lower_let(
ctx,
&perry_hir::Expr::LocalSet(id, Box::new(init_expr.clone())),
)?;
- // #10488: a hoisted `var`'s real declaration reaches this
- // redeclaration branch (#1803 predefine-then-declare shape) and
- // returns below before the fresh-declaration path's
- // `ctx.local_types.insert` ever runs. `proven_local_types` a few
- // lines up IS refreshed per-site, so `is_numeric_expr` (which
- // consults it via `stable_local_type_proof`) sees this
- // declaration's more specific type — but `local_types` keeps
- // whatever the FIRST (predefine) site declared, normally `Any`.
- // That desyncs `is_numeric_expr` from `static_type_of` /
- // `expr_may_return_boxed_value_from_raw_f64_fallback`, which both
- // read `local_types`: a strict-equality compare against an
- // out-of-bounds/hole array read was treated as definitely-numeric
- // (a bare `fcmp`, which cannot represent `undefined`) instead of
- // falling back to a boxed compare. Refresh `local_types` here too
- // so both predicates agree on this local's current type.
- ctx.local_types.insert(id, refined_ty.clone());
+ // #10488: a hoisted `var`'s real declaration reaches this branch
+ // (#1803 predefine-then-declare) and returns before the
+ // fresh-declaration path's `insert` runs, desyncing
+ // `is_numeric_expr` from the `local_types` readers. Refresh here.
+ //
+ // #10727: but NOT when the storage is a box. A captured local
+ // reaches this branch with no source redeclaration --
+ // `PreallocateBoxes` registers the id up front -- and the refined
+ // type then describes the VALUE while the slot holds a box
+ // pointer, so reads lower as raw local loads instead of
+ // `js_box_get_bits`: the declaring scope read `undefined` while a
+ // closure over the same binding saw the real value. A hoisted
+ // `var` is unboxed unless captured, so #10488 keeps its fix.
+ if !ctx.boxed_vars.contains(&id) {
+ ctx.local_types.insert(id, refined_ty.clone());
+ }
} else if ctx.tdz_boxes.remove(&id) {
// No-init reuse (`let x;`) of a TDZ-seeded box must still end the
// dead zone by clearing the sentinel to `undefined`; otherwise a
diff --git a/crates/perry-ext-axios/Cargo.toml b/crates/perry-ext-axios/Cargo.toml
deleted file mode 100644
index 889c76e9da..0000000000
--- a/crates/perry-ext-axios/Cargo.toml
+++ /dev/null
@@ -1,20 +0,0 @@
-[package]
-name = "perry-ext-axios"
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-description = "Native bindings for the npm `axios` HTTP client — uses only `perry-ffi`. First HTTP-client port (Phase 5 step 13)."
-
-[lints]
-workspace = true
-
-[lib]
-crate-type = ["staticlib", "rlib"]
-
-[dependencies]
-perry-ffi.workspace = true
-reqwest = { workspace = true }
-tokio = { workspace = true }
-
-[dev-dependencies]
-perry-ffi = { workspace = true, features = ["runtime-link"] }
diff --git a/crates/perry-ext-axios/src/lib.rs b/crates/perry-ext-axios/src/lib.rs
deleted file mode 100644
index 001bfb65e0..0000000000
--- a/crates/perry-ext-axios/src/lib.rs
+++ /dev/null
@@ -1,328 +0,0 @@
-//! Native bindings for the npm `axios` HTTP client.
-//!
-//! Phase 5 step 13 — first HTTP-client wrapper port. Uses
-//! perry-ffi v0.5.x's full surface: handle registry +
-//! spawn_blocking + JsPromise + JsValue. Reqwest under the hood
-//! (same as perry-stdlib's existing axios copy).
-//!
-//! Functionally identical to `crates/perry-stdlib/src/axios.rs`.
-
-use perry_ffi::{
- alloc_string, get_handle, json_stringify, read_string, register_handle, spawn_blocking,
- with_handle, Handle, JsPromise, JsString, JsValue, Promise, StringHeader,
-};
-
-/// #598: read the body argument as a JSON string. axios in npm-land
-/// accepts the body as either a string (sent as-is) or any JS value
-/// (JSON.stringify'd before send). Pre-fix Perry's FFI took a raw
-/// `*const StringHeader`, which the codegen produced by unboxing the
-/// caller's NaN-boxed value — for an object literal the unboxed
-/// pointer was a real `*mut ObjectHeader`, the runtime read it as a
-/// `*mut StringHeader`, and the request body became the byte pattern
-/// of the ObjectHeader struct followed by the first character of the
-/// stringified field. Same shape under bun: `axios.post(url, {a:1})`
-/// sends `{"a":1}`. With the new f64 signature, the codegen passes
-/// the NaN-boxed value through; here we route strings unchanged and
-/// JSON.stringify everything else.
-unsafe fn read_body_as_string(value_bits: f64) -> String {
- const STRING_TAG: u64 = 0x7FFF_0000_0000_0000;
- const SHORT_STRING_TAG: u64 = 0x7FFB_0000_0000_0000;
- const TAG_MASK: u64 = 0xFFFF_0000_0000_0000;
- const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001;
- const TAG_NULL: u64 = 0x7FFC_0000_0000_0002;
- let bits = value_bits.to_bits();
- if bits == TAG_UNDEFINED || bits == TAG_NULL {
- return String::new();
- }
- let tag = bits & TAG_MASK;
- if tag == STRING_TAG || tag == SHORT_STRING_TAG {
- // String: read as-is, no JSON quoting.
- let ptr = (bits & 0x0000_FFFF_FFFF_FFFF) as *const StringHeader;
- let handle = JsString::from_raw(ptr as *mut StringHeader);
- return read_string(handle).map(String::from).unwrap_or_default();
- }
- // Object / number / array / etc. — JSON.stringify.
- let v = JsValue::from_bits(bits);
- json_stringify(v).unwrap_or_default()
-}
-
-/// Response handle wrapper.
-pub struct AxiosResponseHandle {
- pub status: u16,
- pub status_text: String,
- pub data: String,
- /// Issue #627: lower-cased Content-Type header value (without
- /// charset suffix), or empty string if absent. `js_axios_response_data_parsed`
- /// consults this to decide whether to JSON-parse the body — matches
- /// npm axios's content-type-based behavior, replacing v0.5.714's
- /// body-shape heuristic which would incorrectly parse a JSON-shaped
- /// string body served with `text/plain`.
- pub content_type: String,
-}
-
-unsafe fn read_str(ptr: *const StringHeader) -> Option {
- let handle = JsString::from_raw(ptr as *mut StringHeader);
- read_string(handle).map(String::from)
-}
-
-/// Common request driver — runs the reqwest call inside
-/// spawn_blocking, packages the response into an
-/// `AxiosResponseHandle`, registers it, and resolves the promise
-/// with a POINTER_TAG-tagged handle value (issue #340 trick from
-/// the original perry-stdlib axios — without the explicit
-/// NaN-boxing, the awaiter sees a subnormal float that decays
-/// to `undefined` on `r.status` accesses).
-fn run_request(
- method: &'static str,
- url_or_err: Result,
- build: F,
-) -> *mut Promise
-where
- F: FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder + Send + 'static,
-{
- let promise = JsPromise::new();
- let raw = promise.as_raw();
- let url = match url_or_err {
- Ok(u) => u,
- Err(msg) => {
- promise.reject_string(msg);
- return raw;
- }
- };
-
- spawn_blocking(move || {
- let result: Result = tokio::runtime::Handle::current()
- .block_on(async move {
- let client = reqwest::Client::new();
- let request = build(client, url);
- let response = request
- .send()
- .await
- .map_err(|e| format!("{} request failed: {}", method, e))?;
- let status = response.status().as_u16();
- let status_text = response
- .status()
- .canonical_reason()
- .unwrap_or("")
- .to_string();
- // Issue #627: capture Content-Type before consuming the
- // body. Lower-case + take the part before `;` so
- // `application/json; charset=utf-8` reduces to
- // `application/json` for the JSON-parse decision.
- let content_type = response
- .headers()
- .get(reqwest::header::CONTENT_TYPE)
- .and_then(|v| v.to_str().ok())
- .map(|s| s.split(';').next().unwrap_or(s).trim().to_ascii_lowercase())
- .unwrap_or_default();
- let data = response
- .text()
- .await
- .map_err(|e| format!("Failed to read response body: {}", e))?;
- Ok(AxiosResponseHandle {
- status,
- status_text,
- data,
- content_type,
- })
- });
- match result {
- Ok(resp) => {
- let handle = register_handle(resp);
- // POINTER_TAG-tagged handle value — see #340.
- promise.resolve(JsValue::from_object_ptr(handle as *mut ()));
- }
- Err(msg) => promise.reject_string(&msg),
- }
- });
- raw
-}
-
-/// `axios.get(url) -> Promise`.
-///
-/// # Safety
-///
-/// `url_ptr` must be null or a Perry-runtime `StringHeader`.
-#[no_mangle]
-pub unsafe extern "C" fn js_axios_get(url_ptr: *const StringHeader) -> *mut Promise {
- let url = read_str(url_ptr).ok_or("Invalid URL");
- run_request("GET", url, |client, url| client.get(&url))
-}
-
-/// `axios.head(url) -> Promise`.
-///
-/// # Safety
-///
-/// `url_ptr` must be null or a Perry-runtime `StringHeader`.
-#[no_mangle]
-pub unsafe extern "C" fn js_axios_head(url_ptr: *const StringHeader) -> *mut Promise {
- let url = read_str(url_ptr).ok_or("Invalid URL");
- run_request("HEAD", url, |client, url| client.head(&url))
-}
-
-/// `axios.options(url) -> Promise`.
-///
-/// # Safety
-///
-/// `url_ptr` must be null or a Perry-runtime `StringHeader`.
-#[no_mangle]
-pub unsafe extern "C" fn js_axios_options(url_ptr: *const StringHeader) -> *mut Promise {
- let url = read_str(url_ptr).ok_or("Invalid URL");
- run_request("OPTIONS", url, |client, url| {
- client.request(reqwest::Method::OPTIONS, &url)
- })
-}
-
-/// `axios.post(url, data) -> Promise`.
-///
-/// # Safety
-///
-/// `url_ptr` must be null or a Perry-runtime `StringHeader`. `data` is
-/// a NaN-boxed JSValue — strings are sent as-is, all other shapes are
-/// JSON.stringify'd. See `read_body_as_string` for the routing rule
-/// (#598). The signature uses `f64` to match the codegen dispatch's
-/// pass-as-double path; Rust's calling convention puts it in d0 / a
-/// vector register on AArch64, matching what the codegen emits.
-#[no_mangle]
-pub unsafe extern "C" fn js_axios_post(url_ptr: *const StringHeader, data: f64) -> *mut Promise {
- let url = read_str(url_ptr).ok_or("Invalid URL");
- let body = read_body_as_string(data);
- run_request("POST", url, move |client, url| {
- client
- .post(&url)
- .header("Content-Type", "application/json")
- .body(body)
- })
-}
-
-/// `axios.put(url, data) -> Promise`. Same body-encoding
-/// rule as `axios.post` (#598).
-///
-/// # Safety
-///
-/// `url_ptr` must be null or a Perry-runtime `StringHeader`. `data` is
-/// a NaN-boxed JSValue.
-#[no_mangle]
-pub unsafe extern "C" fn js_axios_put(url_ptr: *const StringHeader, data: f64) -> *mut Promise {
- let url = read_str(url_ptr).ok_or("Invalid URL");
- let body = read_body_as_string(data);
- run_request("PUT", url, move |client, url| {
- client
- .put(&url)
- .header("Content-Type", "application/json")
- .body(body)
- })
-}
-
-/// `axios.delete(url) -> Promise`.
-///
-/// # Safety
-///
-/// `url_ptr` must be null or a Perry-runtime `StringHeader`.
-#[no_mangle]
-pub unsafe extern "C" fn js_axios_delete(url_ptr: *const StringHeader) -> *mut Promise {
- let url = read_str(url_ptr).ok_or("Invalid URL");
- run_request("DELETE", url, |client, url| client.delete(&url))
-}
-
-/// `axios.patch(url, data) -> Promise`. Same body-encoding
-/// rule as `axios.post` (#598).
-///
-/// # Safety
-///
-/// `url_ptr` must be null or a Perry-runtime `StringHeader`. `data` is
-/// a NaN-boxed JSValue.
-#[no_mangle]
-pub unsafe extern "C" fn js_axios_patch(url_ptr: *const StringHeader, data: f64) -> *mut Promise {
- let url = read_str(url_ptr).ok_or("Invalid URL");
- let body = read_body_as_string(data);
- run_request("PATCH", url, move |client, url| {
- client
- .patch(&url)
- .header("Content-Type", "application/json")
- .body(body)
- })
-}
-
-/// `response.status -> number`.
-#[no_mangle]
-pub extern "C" fn js_axios_response_status(handle: Handle) -> f64 {
- if let Some(r) = get_handle::(handle) {
- r.status as f64
- } else {
- 0.0
- }
-}
-
-/// `response.statusText -> string`.
-#[no_mangle]
-pub extern "C" fn js_axios_response_status_text(handle: Handle) -> *mut StringHeader {
- with_handle::(handle, |r| alloc_string(&r.status_text).as_raw())
- .unwrap_or(std::ptr::null_mut())
-}
-
-/// `response.data -> string`. Legacy/backwards-compat — returns the
-/// raw response body bytes as a perry string. For the JSON-auto-parse
-/// path that npm `axios` provides (where `r.data.ok` works directly
-/// when the server returns `application/json`), see
-/// `js_axios_response_data_parsed` below.
-#[no_mangle]
-pub extern "C" fn js_axios_response_data(handle: Handle) -> *mut StringHeader {
- with_handle::(handle, |r| alloc_string(&r.data).as_raw())
- .unwrap_or(std::ptr::null_mut())
-}
-
-/// `response.data -> any` — auto-parsed variant. npm `axios` parses
-/// the response body as JSON when the response's content-type starts
-/// with `application/json`; otherwise it hands back the raw string.
-/// Returns an f64 NaN-boxed JSValue: a string for non-JSON, a parsed
-/// object/array/number/bool/null for JSON. Returns the string fallback
-/// on any parse error so callers don't have to special-case malformed
-/// JSON. The TS-side `r.data` getter routes here so `r.data.ok` /
-/// `r.data[0]` / etc. work the same way as in node `axios`. Issue
-/// #604 followup — only surfaced once the listen() hang was fixed.
-#[no_mangle]
-pub extern "C" fn js_axios_response_data_parsed(handle: Handle) -> f64 {
- // Issue #627: snapshot body + content-type in one with_handle pass to
- // avoid two registry lookups + leaking the lock across the FFI call to
- // js_json_parse below.
- let snapshot = with_handle::(handle, |r| {
- (r.data.clone(), r.content_type.clone())
- });
- let (body, content_type) = match snapshot {
- Some(s) => s,
- None => return f64::from_bits(0x7FFC_0000_0000_0001), // TAG_UNDEFINED
- };
- // Issue #627: npm axios parses JSON only when content-type starts with
- // `application/json` (with optional `; charset=...`). Pre-fix, perry
- // used a body-shape heuristic which would incorrectly parse a JSON-
- // looking string body served with `text/plain`. The `+json` suffix
- // form (e.g. `application/vnd.api+json`) also gets parsed by npm
- // axios per the standard, so accept either shape.
- let is_json_ct = content_type == "application/json" || content_type.ends_with("+json");
- if is_json_ct {
- // Cross the FFI boundary into the runtime's JSON parser. The
- // runtime returns `undefined` (TAG_UNDEFINED) on parse error,
- // which we detect and fall through to the raw-string path so
- // the user always gets *something* on `r.data`. Note: the
- // runtime's `js_json_parse` declares its return type as
- // `JSValue` (repr(transparent) over u64), so we declare it
- // here as `u64` rather than `f64` to keep the AArch64 ABI on
- // the integer register (x0) instead of the float register (d0).
- extern "C" {
- fn js_json_parse(ptr: *const StringHeader) -> u64;
- }
- let s = alloc_string(&body);
- let parsed_bits = unsafe { js_json_parse(s.as_raw()) };
- const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001;
- if parsed_bits != TAG_UNDEFINED {
- return f64::from_bits(parsed_bits);
- }
- }
- // Non-JSON or parse failure — return the raw body as a perry
- // string. NaN-boxed via STRING_TAG so the receiver sees it as a
- // proper JS string.
- let s = alloc_string(&body);
- let bits = 0x7FFF_0000_0000_0000_u64 | (s.as_raw() as u64 & 0x0000_FFFF_FFFF_FFFF);
- f64::from_bits(bits)
-}
diff --git a/crates/perry-hir/src/destructuring/var_decl/native_new.rs b/crates/perry-hir/src/destructuring/var_decl/native_new.rs
index 0944d8cd10..145a34a2f6 100644
--- a/crates/perry-hir/src/destructuring/var_decl/native_new.rs
+++ b/crates/perry-hir/src/destructuring/var_decl/native_new.rs
@@ -460,19 +460,6 @@ pub(crate) fn register_native_from_new_and_calls(
Some("Connection")
}
("pg", "connect") => Some("Client"),
- // axios.get/post/put/delete/patch/request — mirror
- // the top-level decl arm in lower.rs:4011 so
- // `await axios.get(...)` registers the result as
- // an axios.Response inside async function bodies.
- // Without this, `r.status` / `r.data` fall through
- // to generic property dispatch and read the
- // raw handle pointer as an ObjectHeader. Issue
- // #604 followup — same pattern as the createServer
- // registration above.
- (
- "axios",
- "get" | "post" | "put" | "delete" | "patch" | "request",
- ) => Some("Response"),
_ => None,
};
if let Some(class_name) = class_name {
diff --git a/crates/perry-hir/src/js_transform/local_natives.rs b/crates/perry-hir/src/js_transform/local_natives.rs
index df2868c134..0c2e42be83 100644
--- a/crates/perry-hir/src/js_transform/local_natives.rs
+++ b/crates/perry-hir/src/js_transform/local_natives.rs
@@ -1245,24 +1245,6 @@ pub fn fix_native_instance_expr_with_locals(
Expr::PropertyGet {
object, property, ..
} => {
- if let Expr::LocalGet(local_id) = object.as_ref() {
- if matches!(property.as_str(), "status" | "statusText" | "data")
- && matches!(
- local_id_instances.get(local_id),
- Some((module, class)) if module == "axios" && class == "Response"
- )
- {
- let object_expr = std::mem::replace(object.as_mut(), Expr::Undefined);
- *expr = Expr::NativeMethodCall {
- module: "axios".to_string(),
- class_name: Some("Response".to_string()),
- object: Some(Box::new(object_expr)),
- method: property.clone(),
- args: Vec::new(),
- };
- return;
- }
- }
// Recurse into the object first so any nested `$(sel)` Call has
// been rewritten to a cheerio NativeMethodCall.
fix_native_instance_expr_with_locals(object, native_instances, local_id_instances);
@@ -1372,10 +1354,6 @@ pub fn detect_native_instance_creation_with_context(
("node-cron", "schedule") => "CronJob",
("readline", "createInterface") => "Interface",
("bun", "Transpiler") => "Transpiler",
- (
- "axios",
- "get" | "post" | "put" | "delete" | "patch" | "head" | "options" | "request",
- ) => "Response",
// Issue #1193: `const $ = load(html)` / `loadFragment(html)`
// returns the jQuery-like callable used as `$(selector)`.
// Tagging the local as CheerioAPI lets the rewriter below
diff --git a/crates/perry-hir/src/lower/expr_call/globals.rs b/crates/perry-hir/src/lower/expr_call/globals.rs
index 44f4596f55..2551653daa 100644
--- a/crates/perry-hir/src/lower/expr_call/globals.rs
+++ b/crates/perry-hir/src/lower/expr_call/globals.rs
@@ -351,6 +351,38 @@ pub(super) fn try_global_builtins(
args: vec![path],
}));
}
+ // #10735: `require.main` must be the process ENTRY module only —
+ // equal to `module` there, unequal (or `undefined`, when the
+ // process entry is ESM) everywhere else. The CJS preamble
+ // (`cjs_wrap/wrap.rs`) emits exactly one of these two calls per
+ // module depending on whether IT is the compile-time entry:
+ // the entry publishes its own `module` record as the shared
+ // "main module"; every other CJS module reads it back instead of
+ // assigning its OWN `module` (which is the bug — it made
+ // `require.main === module` trivially true everywhere).
+ "__perry_set_cjs_main_module" => {
+ let module = if !args.is_empty() {
+ args.remove(0)
+ } else {
+ Expr::Undefined
+ };
+ return Ok(Ok(Expr::NativeMethodCall {
+ module: "__perry_runtime".to_string(),
+ class_name: None,
+ object: None,
+ method: "setCjsMainModule".to_string(),
+ args: vec![module],
+ }));
+ }
+ "__perry_get_cjs_main_module" => {
+ return Ok(Ok(Expr::NativeMethodCall {
+ module: "__perry_runtime".to_string(),
+ class_name: None,
+ object: None,
+ method: "getCjsMainModule".to_string(),
+ args: vec![],
+ }));
+ }
"Symbol" => {
// Symbol() / Symbol(description)
if args.is_empty() {
diff --git a/crates/perry-hir/src/lower/module_decl.rs b/crates/perry-hir/src/lower/module_decl.rs
index 142efb4eb5..0a5f50f6b6 100644
--- a/crates/perry-hir/src/lower/module_decl.rs
+++ b/crates/perry-hir/src/lower/module_decl.rs
@@ -1007,11 +1007,6 @@ pub(crate) fn lower_module_decl(
"http" | "https",
"request" | "get",
) => Some("ClientRequest"),
- (
- "axios",
- "get" | "post" | "put" | "delete"
- | "patch" | "request",
- ) => Some("Response"),
_ => None,
};
if let Some(class_name) = class_name {
diff --git a/crates/perry-hir/src/lower/stmt.rs b/crates/perry-hir/src/lower/stmt.rs
index bc94fe146c..94ec086fd3 100644
--- a/crates/perry-hir/src/lower/stmt.rs
+++ b/crates/perry-hir/src/lower/stmt.rs
@@ -1027,10 +1027,6 @@ pub(crate) fn lower_stmt(
} = inner.as_ref()
{
let class_name = match (mod_name.as_str(), method.as_str()) {
- (
- "axios",
- "get" | "post" | "put" | "delete" | "patch" | "request",
- ) => Some("Response"),
("mongodb", "connect") => Some("MongoClient"),
("pg", "connect") => Some("Client"),
_ => None,
diff --git a/crates/perry-hir/tests/axios_response_property_lowering.rs b/crates/perry-hir/tests/axios_response_property_lowering.rs
deleted file mode 100644
index e153eb9513..0000000000
--- a/crates/perry-hir/tests/axios_response_property_lowering.rs
+++ /dev/null
@@ -1,48 +0,0 @@
-use perry_diagnostics::SourceCache;
-use perry_hir::{clear_current_module_source, fix_local_native_instances, lower_module};
-use perry_parser::parse_typescript_with_cache;
-
-#[test]
-fn awaited_axios_response_properties_keep_native_dispatch() {
- let mut cache = SourceCache::new();
- let parsed = parse_typescript_with_cache(
- r#"
- import axios from "axios";
-
- async function main() {
- const response = await axios.get("https://example.com/data");
- const head = await axios.head("https://example.com/data");
- const options = await axios.options("https://example.com/data");
- console.log(`status=${response.status}`);
- console.log(`ok=${response.data.ok}`);
- console.log(`head=${head.status}:${head.data}`);
- console.log(`options=${options.status}:${options.data}`);
- }
- "#,
- "/tmp/axios_response_property_lowering.ts",
- &mut cache,
- )
- .expect("parse");
- let mut module = lower_module(
- &parsed.module,
- "test",
- "/tmp/axios_response_property_lowering.ts",
- )
- .expect("lower");
- clear_current_module_source();
- fix_local_native_instances(&mut module);
-
- let main = module
- .functions
- .iter()
- .find(|function| function.name == "main")
- .expect("main function");
- let hir = format!("{:#?}", main.body);
- assert!(
- hir.matches("class_name: Some(").count() == 6
- && hir.matches("\"Response\"").count() == 6
- && hir.contains("method: \"status\"")
- && hir.contains("method: \"data\""),
- "awaited Axios response properties lost native dispatch:\n{hir}"
- );
-}
diff --git a/crates/perry-hir/tests/unimplemented_api_check.rs b/crates/perry-hir/tests/unimplemented_api_check.rs
index 14fa823fbc..62419f4129 100644
--- a/crates/perry-hir/tests/unimplemented_api_check.rs
+++ b/crates/perry-hir/tests/unimplemented_api_check.rs
@@ -319,8 +319,9 @@ fn os_eol_and_path_sep_compile() {
/// As of #513, every module in `NATIVE_MODULES` has at least one
/// manifest entry, so the permissive fall-through is unreachable for
-/// supported modules. `axios.foo` (which used to silently compile under
-/// the pre-#513 zero-entries-permissive shape) now errors.
+/// supported modules. `node-fetch`'s bogus member (which used to
+/// silently compile under the pre-#513 zero-entries-permissive shape)
+/// now errors.
///
/// The drift test `every_native_module_has_at_least_one_manifest_entry`
/// in `crates/perry-codegen/tests/manifest_consistency.rs` makes this
@@ -330,14 +331,14 @@ fn os_eol_and_path_sep_compile() {
fn supported_module_with_unknown_member_is_rejected() {
let result = lower_result_strict(
r#"
- import axios from "axios";
- const x = axios.foo;
+ import fetch from "node-fetch";
+ const x = fetch.foo;
"#,
);
- let err = result.expect_err("axios.foo should error post-#513");
+ let err = result.expect_err("node-fetch.foo should error post-#513");
assert!(
- err.contains("axios.foo") && err.contains("not implemented"),
- "expected error naming `axios.foo` and `not implemented`, got: {err}"
+ err.contains("fetch.foo") && err.contains("not implemented"),
+ "expected error naming `fetch.foo` and `not implemented`, got: {err}"
);
}
diff --git a/crates/perry-runtime/src/closure/mod.rs b/crates/perry-runtime/src/closure/mod.rs
index 9930d058b8..93e20e8bb3 100644
--- a/crates/perry-runtime/src/closure/mod.rs
+++ b/crates/perry-runtime/src/closure/mod.rs
@@ -103,11 +103,10 @@ pub use v8_stubs::{
};
pub use v8_stubs::{
- js_argon2_hash_options, js_axios_create, js_axios_request, js_lodash_ends_with,
- js_lodash_escape, js_lodash_includes, js_lodash_lower_first, js_lodash_replace,
- js_lodash_split, js_lodash_start_case, js_lodash_starts_with, js_lodash_unescape,
- js_lodash_upper_first, js_ratelimit_create, js_sharp_negate, js_sharp_quality,
- js_sharp_to_format,
+ js_argon2_hash_options, js_lodash_ends_with, js_lodash_escape, js_lodash_includes,
+ js_lodash_lower_first, js_lodash_replace, js_lodash_split, js_lodash_start_case,
+ js_lodash_starts_with, js_lodash_unescape, js_lodash_upper_first, js_ratelimit_create,
+ js_sharp_negate, js_sharp_quality, js_sharp_to_format,
};
#[cfg(test)]
diff --git a/crates/perry-runtime/src/closure/v8_stubs.rs b/crates/perry-runtime/src/closure/v8_stubs.rs
index 1dbca0403d..b5d1fb27a8 100644
--- a/crates/perry-runtime/src/closure/v8_stubs.rs
+++ b/crates/perry-runtime/src/closure/v8_stubs.rs
@@ -1,5 +1,5 @@
//! V8-interop no-op stubs and AOT no-op stubs for unconditionally-declared
-//! FFI symbols (lodash, axios, argon2, sharp, ratelimit).
+//! FFI symbols (lodash, argon2, sharp, ratelimit).
// V8 interop no-op stubs. Perry no longer ships a runtime JS engine: the
// `perry-jsruntime` crate (V8 via `deno_core`) that used to provide the real
@@ -288,14 +288,6 @@ pub extern "C" fn js_lodash_upper_first() -> f64 {
0.0
}
#[no_mangle]
-pub extern "C" fn js_axios_create() -> i64 {
- 0
-}
-#[no_mangle]
-pub extern "C" fn js_axios_request() -> i64 {
- 0
-}
-#[no_mangle]
pub extern "C" fn js_argon2_hash_options() -> i64 {
0
}
diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs
index c062e634f4..ae21b86be8 100644
--- a/crates/perry-runtime/src/gc/mod.rs
+++ b/crates/perry-runtime/src/gc/mod.rs
@@ -999,6 +999,11 @@ pub fn gc_init() {
// Runtime path-module exports and cached initialization errors live in a
// per-heap Rust registry, so moving GC must mark and rewrite them.
reg_scanner!(crate::module_require::scan_module_path_roots_mut);
+ // #10735: the shared CJS "main module" (`require.main` / Node's
+ // `process.mainModule`) is a raw heap pointer cached in a thread-local
+ // outside any shadow frame — a moving collection must mark and rewrite
+ // it like any other mutable root.
+ reg_scanner!(crate::module_require::scan_cjs_main_module_root_mut);
reg_budgeted_scanner!(
promise_mutable_root_scanner,
crate::promise::scan_promise_roots_mut_step,
diff --git a/crates/perry-runtime/src/gc/tests/cjs_main_module.rs b/crates/perry-runtime/src/gc/tests/cjs_main_module.rs
new file mode 100644
index 0000000000..b6fd7269d2
--- /dev/null
+++ b/crates/perry-runtime/src/gc/tests/cjs_main_module.rs
@@ -0,0 +1,58 @@
+//! #10735 witness: `scan_cjs_main_module_root_mut`'s rewrite counter is a
+//! diagnostic nothing else reads, which is exactly the kind of thing that
+//! rots silently -- a future change that stops rewriting the cache would
+//! report `total_rewrites=0` forever while looking perfectly healthy. This
+//! test forces a REAL evacuating collection (not a passive scan that never
+//! sees a move) and asserts both that the placeholder's address actually
+//! changed and that the counter tracked it, per CLAUDE.md's "a gate must
+//! assert its subject was live" rule.
+//!
+//! Lives here rather than in `module_require.rs` because a real evacuating
+//! minor needs `CopyingNurseryTestGuard`'s preflight setup (generated write
+//! barriers reporting "active", the conservative-full-scan test default
+//! turned off, a clean shadow stack / remembered set) -- machinery private
+//! to this `gc::tests` tree. That guard also clears the thread's mutable
+//! scanner registry so unrelated GC tests see only the roots they install,
+//! which would remove the very scanner under test, so this file
+//! re-registers it explicitly after constructing the guard.
+
+use super::super::*;
+use super::support::CopyingNurseryTestGuard;
+
+#[test]
+fn placeholder_move_under_forced_evacuation_increments_the_rewrite_counter() {
+ let _nursery = CopyingNurseryTestGuard::new(0);
+ let _evac = knob_overrides::ForcedEvacuationTestGuard::on();
+ let _diag = GcDiagTestGuard::force_on();
+ // `CopyingNurseryTestGuard::new` clears the thread's scanner registry so
+ // this collection sees exactly the roots the test installs -- put the
+ // one under test back.
+ gc_register_mutable_root_scanner(crate::module_require::scan_cjs_main_module_root_mut);
+
+ crate::module_require::js_bootstrap_cjs_main_module_placeholder();
+ let before_bits =
+ crate::module_require::test_cjs_main_module_bits().expect("placeholder must be published");
+ let rewrites_before = crate::module_require::test_cjs_main_module_rewrite_count();
+
+ js_gc_collect();
+
+ let after_bits = crate::module_require::test_cjs_main_module_bits()
+ .expect("placeholder must survive the collection");
+ let rewrites_after = crate::module_require::test_cjs_main_module_rewrite_count();
+
+ assert_ne!(
+ before_bits, after_bits,
+ "forced evacuation must have moved the placeholder -- if the \
+ address is unchanged, this test's premise (a real move happened) \
+ is false, and the counter assertion below would be checking \
+ nothing (before={before_bits:#x} after={after_bits:#x})"
+ );
+ assert!(
+ rewrites_after > rewrites_before,
+ "scan_cjs_main_module_root_mut must have counted the rewrite \
+ (before={rewrites_before} after={rewrites_after}); a zero delta \
+ here means the cache stopped following the object it caches -- \
+ the exact regression #10735's identity guarantee depends on never \
+ happening"
+ );
+}
diff --git a/crates/perry-runtime/src/gc/tests/mod.rs b/crates/perry-runtime/src/gc/tests/mod.rs
index 63d50b4fa6..12c8c0fa0b 100644
--- a/crates/perry-runtime/src/gc/tests/mod.rs
+++ b/crates/perry-runtime/src/gc/tests/mod.rs
@@ -15,6 +15,7 @@ mod buffer_side_tables;
mod census;
mod census_block_windows;
mod census_whole_block;
+mod cjs_main_module;
mod concat_site;
mod contract;
mod copy_slot_decode;
diff --git a/crates/perry-runtime/src/module_require.rs b/crates/perry-runtime/src/module_require.rs
index 17d92d2018..01c35ad3d6 100644
--- a/crates/perry-runtime/src/module_require.rs
+++ b/crates/perry-runtime/src/module_require.rs
@@ -23,6 +23,158 @@ fn undefined() -> f64 {
f64::from_bits(TAG_UNDEFINED)
}
+crate::perry_thread_local! {
+ /// The process ENTRY module's own CJS `module` record — Node's
+ /// `require.main` / `process.mainModule`. Set exactly once, by the entry
+ /// module's own preamble (see `cjs_wrap::wrap_commonjs_with_body_offset`
+ /// in the compiler), before that module's body runs any `require()` of
+ /// its own — so every dependency it (transitively) requires observes
+ /// this already published. Every OTHER CJS-wrapped module reads it back
+ /// via [`js_get_cjs_main_module`] instead of assigning its own local
+ /// `module`, which is the #10735 bug this replaces (`require.main ===
+ /// module` was trivially true in every compiled CommonJS module, not
+ /// just the real entry point).
+ ///
+ /// Stays `None` (JS `undefined`) for the lifetime of the heap when the
+ /// process entry is ESM — matching Node, where a CJS module reached only
+ /// via `import` from an ESM entry has `require.main === undefined`
+ /// (there is no CommonJS "main" in that process).
+ static CJS_MAIN_MODULE: std::cell::RefCell