chore(bindings): remove axios native binding, compile real axios from source - #10679
proggeramlug wants to merge 126 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Renamed the axios-removal changeset fragment now that the PR number is known, and pointed PENDINGCS-compile-smoke-known.md's dangling placeholder reference at it.
Several event/listener dispatch loops clone listener callbacks and/or call arguments into plain Rust locals (a Vec<Listener>, a Vec<f64>, or a raw array pointer), then call into user code that can allocate and trigger a moving minor collection, then reuse those unrooted copies for the next listener. Root every such copy through a RuntimeHandleScope and re-read the current (possibly relocated) value before each dispatch, instead of trusting the pre-call copy. Reproduced at crates/perry-runtime/src/node_stream_event_emitter.rs's emit_stream_event/call_listener_args (the path `class X extends EventEmitter` actually dispatches through): a 3-listener emitter whose second listener allocates heavily segfaults dereferencing the third listener's stale closure pointer, reliably, with no GC env knobs. Gone under PERRY_GEN_GC=0 (full mark-sweep, non-moving). Under PERRY_GC_DIAG=1 PERRY_GC_PROTECT_FROMSPACE=1 the from-space quarantine reports the exact fault: a retired-from-space deref of a GC_TYPE_CLOSURE object, matching a symbolized backtrace through js_native_call_value <- call_listener_args <- emit_stream_event. The same pattern is fixed at the four other sites an earlier code-read audit named: perry-stdlib's events.rs (js_event_emitter_emit/emit0, dispatch_error_monitor, emit_meta_event), domain.rs (emit_domain_event), worker_threads/worker_surface.rs (stream_emit_event), and events/warnings.rs (emit_warning). events.rs's own asynchronous dispatch branch already rooted its callback/receiver/ args via js_async_resource_run_in_async_scope; the synchronous branch did not, which is the same finding by construction, independent of the runtime repro.
#8595's entry outliner only ever chunked hir.init. For a CommonJS module, cjs_wrap::wrap_commonjs_for_target wraps the whole body as text inside a `function __perry_cjs_factory() {...}` closure nested in an anonymous IIFE; hir.init ends up with only a handful of wrapper statements, so admission never fired and the real body stayed one giant function. On typescript 5.9.3's _tsc.js this was a single 463,716-instruction/6.30 MB closure, past the machine-pipeline budget. find_cjs_factory_closure(_mut) locates that closure by walking hir.init's statement/expression tree (it is a Stmt::Let naming an Expr::Closure, not a hir.functions entry, since it is lexically nested). outline_entry_module now tries hir.init first (unchanged #8595 behavior) and falls back to the factory's body with the identical chunk_statements/analyze_stmts_outlining machinery, so a module is only ever outlined from one origin per compile. The factory always captures its own name from the wrapper's IIFE scope (`__cjs_module.__perry_cjs_factory = __perry_cjs_factory;`, which perry-runtime's module_require.rs calls through on a circular-require recovery path). A chunk is a plain, non-capturing function and can't read a captured id, so classify_for_chunking keeps any statement referencing one inline in the residual body rather than promoting it to a module global -- a global would turn a per-invocation-fresh capture into one program-wide instance and could silently break that recovery path. module_globals_emit.rs folds the factory's own logical statements into the same cross-chunk-let promotion emit_module_globals already does for hir.init, so a var shared across the factory's new chunks gets the same @perry_global_* treatment hir.init cross-chunk lets get. Verified on a synthetic 2107-statement CJS fixture (cross-chunk vars plus a closure created early and invoked from far-later statements) against Node's own output, and on a full typescript 5.9.3 build: the 463,716-instruction closure is gone, entry-outline reports "cjs factory: ... candidate=true", nm shows __perry_entry_chunk_* symbols, and `--noEmit demo.ts` / `--version` output and exit codes are byte-identical to before.
… numeric conversion refresh() called set_timer_ref_state(id, true) unconditionally, so refreshing an unrefd Timeout/Interval re-refd it -- hasRef() flipped to true and the process stayed alive for a callback that had been deliberately detached from the event loop. Node refresh() reschedules only and never touches ref state; drop the forced ref-state write and let the existing entry (set at schedule time, updated by any ref()/unref() since, pinned while the timer is queued) stand. js_number_coerce gave setImmediate handles the same numeric-conversion shortcut as Timeout handles, so +setImmediate(...) returned a number instead of NaN. Node only gives Timeout (setTimeout/setInterval) a numeric conversion; Immediate has none. Add is_immediate_timer_id and gate the shortcut on it so an Immediate falls through to the generic toPrimitive/toString path, which already yields NaN. Fixes #10541 Fixes #10542
…lue, not just the bare-import name class X extends AsyncResource threw "Class constructor AsyncResource cannot be invoked without 'new'" at super() for every heritage shape except a bare import binding. A local alias, a namespace member, and a CJS destructured require() all resolve to the identical bound native export value the canonical import does, but only the bare import shape was recognized statically at HIR-lowering time, so super() fell through to a plain call of the export -- which AsyncResource throws on by design. Recognize the bound export VALUE in js_fetch_or_value_super, exactly as the existing WASI arm does, and run the same native-backing init the canonical path already uses.
…per-evaluation identity
A class expression returned from a function (a mixin/factory —
function withCommands(Base) { return class extends Base {}; }) had no
per-evaluation identity when the function lived in a non-entry module:
every call returned the SAME shared-template class object, re-parented
to the most recently passed Base. specialize_captured_class_factories
already fixes this for same-module callers by cloning a distinct class
per call site, but it only ever sees call sites in the SAME module as
the factory -- a caller in another module reaches the factory through
an ordinary cross-module call that pass never visits, so an exported
factory's own template stayed shared and got silently re-parented on
each call.
Give an exported factory real per-evaluation identity directly: when
its body is nothing but the single-statement
return class extends <expr> {} shape, upgrade the class's own
ClassRef to ClassExprFresh, exactly what the same class expression
would already lower to had it needed per-evaluation statics/captures/a
private brand. This closes the gap for every caller, local or
cross-module, without touching the existing same-module
specialization (a locally-cloned call site never calls the factory at
runtime at all, so it is unaffected).
A subclass constructor assigning this.<name> where <name> is a method inherited from a parent class allocated an own inline field slot for it, hiding the inherited method from the moment super() returned. Track own+inherited instance method names per class (mirroring the existing accessor-name tracking) and consult the union when deciding whether a constructor-body this.<name> = ... assignment is a new data field or a method override.
A hoisted var reaches lower_let as two Stmt::Lets sharing one local id (a body-entry predefine, then the real declaration); the second takes the #1803 redeclaration early return before ctx.local_types is updated. proven_local_types (consulted by is_numeric_expr) IS refreshed on redeclaration, but local_types (consulted by expr_may_return_boxed_value_from_raw_f64_fallback) was not, so the two predicates disagreed about the same local: a strict-equality compare against an out-of-bounds/hole read of a var-declared number array took the bare-fcmp numeric fast path, which cannot represent the NaN-boxed undefined tag such a read can produce. Refresh local_types on the redeclaration path too.
… parent
A subclass with no explicit constructor, extending a capture-bearing
class expression held in a local (const Base = class {...}; const Sub
= class extends Base {...};), never forwarded Base's captured
enclosing-scope locals to the synthesized subclass constructor: the
lowering deliberately drops the static extends_name for such a
lexically-local heritage identifier (avoiding a same-named-class
collision, #5437), and capture propagation was keyed off that same
name. Resolve the heritage identifier through resolve_class_alias
instead - the same table Expr::New's own capture lookup already uses
for let X = class {...}; new X() - for capture forwarding only,
gated to subclasses with no own constructor (an explicit constructor
already forwards captures correctly via a separate mechanism).
…uiltin namespace Math[k], JSON[k], Object[k] and other builtin namespace/constructor member reads with a non-literal computed key collapsed to the bare GlobalGet(0) intrinsic sentinel, so the read landed on the number 0 instead of the real object (Math[key](x) threw "(number).x is not a function"). #973's value-form reroute wraps these idents as PropertyGet{GlobalGet(0), name}; member_tail.rs undoes that reroute in member-object position so the intrinsic call/constant-fold paths for a STATICALLY-KNOWN member name (Math.max(...)) keep their pre-#973 bare receiver. That undo is only safe when the member name is known at lowering time -- outer_static_member is None for a computed non-literal key, which zeroed out the outer_is_reified_*/ outer_is_inherited_* guards instead of blocking the undo itself. Add outer_is_dynamic_computed_key to the existing conjunction so any dynamic key keeps the reified receiver, letting the runtime property lookup resolve against the real namespace/constructor object. Verified this needs no console carve-out (console[m](...) already falls back correctly to the generic dynamic-dispatch path once its receiver survives). Literal-key paths are untouched by construction -- the flag only fires for MemberProp::Computed with a non-string-literal key.
…rted sibling by value An exported function whose body references another exported function BY VALUE (`x === f`, not just as a call target) was a candidate for the cross-module function inliner, which bundled a private clone of the sibling into the destination module under a fresh symbol. Every function value materializes into a heap closure keyed by its wrapper symbol, so the clone's `f` and the canonical `f` every importer resolves through produced two distinct closures -- an in-module identity check silently disagreed with every importer's own view of the same function. gather_cross_module_functions now refuses a candidate whose dependency graph would need to bundle a separately-exported sibling referenced by value; it falls back to the ordinary cross-module call instead, which resolves through the shared canonical wrapper. Self-recursion is unaffected. Fixes #10554
Object.prototype.toString.call(x) fell through to the generic [object Object] for URL, URLSearchParams, Headers, Request, Response, FormData, Blob, File, AbortController, AbortSignal, TextEncoder, TextDecoder, EventTarget, Event and CustomEvent, and x[Symbol.toStringTag] read back undefined -- breaking the standard cross-realm type check utility/HTTP libraries use (axios decides body serialization this way). Two representations, two gaps: the Web Fetch family and TextEncoder/ TextDecoder are small-integer registry handles with no brand/property case; URL/URLSearchParams and AbortController/AbortSignal/EventTarget/ Event/CustomEvent are real objects whose instances are never linked to their .prototype via object_static_prototype, so a property installed only there would never be reached from an instance. A new web_builtin_to_string_tag answers both Object.prototype.toString and x[Symbol.toStringTag] from one place, and a real, correctly-shaped descriptor is also installed on each constructor's own .prototype for reflection. Fixes #10555
…evaluation instanceof's class-chain walk resolved a dynamic parent purely by the shared TEMPLATE class_id, so evaluating a heritage-carrying class expression more than once shadowed an EARLIER evaluation's parent once a LATER evaluation of the same factory ran. Each per-evaluation class object already pins its own heritage (js_class_object_pin_parent, consulted by super() and capture resolution since #9364); instanceof never consulted it. Pin the constructing class object onto each new instance too, and give instanceof a value-aware chain walk that prefers a pinned VALUE at each hop (falling back to the plain class_id registry once no further per-evaluation precision is available). Gated behind a monotone latch armed only when a class object is ever pinned, so the common never-evaluated-twice case pays a single idle-load check.
The wrapped-`let` fold changes what the detector can count, so the recorded 561 and the measured 581 are two different yardsticks. That is what the script's audited-migration exemption is for, and it is the same situation as the 1 -> 2 migration. The ratchet is unchanged: it still fails on finding 582, verified by planting one. The exemption becomes an explicit AUDITED_MIGRATIONS list naming each migration and its reason, instead of a hard-coded (1, BASELINE_SCHEMA) pair. Every unlisted schema change is still rejected, and --self-test now asserts that 2 -> 4 is refused and that BASELINE_SCHEMA cannot be bumped without naming its own migration -- otherwise a renumber would exempt every PR from the ratchet. 581 = 558 + 34 newly visible - 11 false positives. Two of the 34 were inspected and are genuine unrooted-across-allocation shapes; the other 32 are unaudited exposure surface, not known bugs.
`raw_handle_debt.py --no-raise-vs` compares ceilings strictly per path, so a pure file move — which the 2000-line cap forces regularly — reads as new debt: the bare run demands the emptied source's line be deleted, and the merge-base run then rejects the destination as "was not listed at the merge base", though the total never moved and the bodies are byte-identical. A ledger entry may now carry `# moved-from: <path>`. The destination is credited with what the source actually surrendered between the base and head ledgers, and nothing else: the total check is untouched, the credit is bounded by a real reduction in the same diff, two destinations sharing one source drain one pool, and the annotation goes inert once the move lands. A malformed entry comment is now a parse failure rather than an ignored comment, and `--update` carries surviving annotations through (writer split out as `render_ledger` so the round trip is assertable). `--self-test` +12 cases: undeclared move still rejected, declared move and a 1+1 three-way split pass, laundering / over-draw / double-spend / stale annotation / self-reference each rejected by their own diagnostic. No ceiling changed.
…10581) The fixture raced a `* * * * * *` CronJob against a fixed `Date.now() + 10_000`. The deadline is a timeout, not a barrier: when it expired first the loop exited and the fixture printed `false` on two lines expected to read `true`, dropping `tick 1`/`tick 2` as well — a four-line divergence the harness reports as a `parity_fail`, i.e. as a miscompile. It is inside pr-gate's gap shards and absent from gap_snapshot.json, and it already held #10530 out of a train. The wait now has no deadline, so the printed text is a function of CronJob's behaviour alone: the ticks arrive, or PERRY_RUN_TIMEOUT kills the run and the harness classifies that as a crash/timeout rather than a parity mismatch. No fallback bound — any bound that prints or throws on expiry is the same defect with a longer fuse, and the old 10s was unreachable anyway because PERRY_RUN_TIMEOUT is also 10s. The never-started job now prints `neverTicks === 0` from a real counter instead of a hardcoded `true`, checked after the barrier. Output bytes unchanged. Verified with an identical 11s event-loop stall injected into both the old and new fixtures: under Node 26.5.1 and Perry v0.5.1598 the old one diverges and the new one is byte-identical to the unstalled oracle. Harness run exits 0 with journal status `pass`; 8 Node runs gave one distinct output in 1.86-2.04s.
#10711 reports that a function read from an object property silently drops its own call to a second function passed to it as a parameter — commander's `_displayError` shape, where `outputError(str, write)` invokes the `writeErr` it was handed: this._outputConfiguration.outputError( message, this._outputConfiguration.writeErr); It does not reproduce. The reporter's own isolated repro prints the expected text on all three trees that matter — current main (v0.5.1598), the main commit their branch forks from (8df83f8), and their actual tree (PR #10712 on top of #10699, head 463c4fa) — and real commander 14.0.3 compiled from source via `perry.compilePackages` matches Node 26.5.1 byte for byte across the whole output surface the issue names: `--help`, `--version`, missing required argument, unknown option, unknown command and `program.error()`, under both the default output configuration and a `configureOutput()` override. 32 further shapes of the same indirection agree with Node too. So this adds the regression lock rather than a fix. The shape is worth gating: #10689 — an inherited property read folding to the constant `undefined` on a scalar-replaced object — landed one commit before this issue was filed and is the same family, silent in the same way. The fixture covers the reported form verbatim plus the method-shorthand, class-field, `configureOutput`-override, spread, nested-receiver, cross-object-writer and in-loop spellings. Two of the cases exist to keep the fixture from passing vacuously. One traces `before` / `typeof write` / `after` around the inner call, so "the outer body ran and the inner call evaporated" cannot read as a pass. The other omits the writer entirely and asserts a TypeError: that a missing callee is LOUD is the property that keeps this bug class from ever presenting as a plausible wrong answer. Every writer sinks to stdout because the parity harness merges stdout and stderr into one compared stream; the stream is incidental to the indirection. Refs #10711
|
Acceptance complete — this is unblocked and out of draft. Real npm axios with its full 27-package transitive graph, no One divergence was found and has been fully attributed rather than waved through: The attribution, because "pre-existing" is a claim that deserves evidence:
So real-source axios is strictly better than the binding being removed: status and data correct with headers incomplete, versus nothing correct followed by a crash. The removal improves this path rather than regressing it. Two notes for whoever queues this:
|
…r gate `scripts/native_result_ledger.py` is red on pristine `main`, blocking the path-filtered `Native Result Ledger / check` workflow on every PR that touches `native_table/**` or the ledger itself. Two independent defects, one hiding the other. 1. Stale row count (bookkeeping). #10658's `net.Socket` surface cluster landed in merge train 221 and grew `native_table/net_events.rs` from 53 to 58 typed rows. `EXPECTED_ROWS` stayed at 371, so the gate failed with `expected 371 classified rows, found 376`. 2. Four unclassified providers (the real defect). Those five new rows carry four runtime symbols that were never added to `native_result_ledger.tsv`, so the table declared a result class the provider inventory had no opinion about. An unclassified `result_kind` misrepresents to the GC what a native call returns. The count check runs FIRST and raises, so the classification-coverage check never executed: the stale constant was acting as a mask. Bumping the constant alone would have turned the gate green and shipped (2). Each of the four providers was read, not name-matched. All four return their `handle: i64` argument unchanged -- a `next_id_or_throw()` registry id and key into `statics::sockets()`, not a heap address -- which is exactly `NativeRetKind::HandleId` ("an integer registry id or provider sentinel"): js_ext_net_socket_on perry-ext-net/src/handle_exports.rs:65 js_net_socket_prepend_listener perry-ext-net/src/lifecycle.rs:1040 js_net_socket_prepend_once_listener perry-ext-net/src/lifecycle.rs:1060 js_net_socket_unpipe perry-ext-net/src/pipe.rs:325 `js_ext_net_socket_on` backs two rows (`on` and `addListener` share the symbol), hence five rows from four symbols. The sibling `js_net_socket_pipe` returns `f64`/`NR_F64`, which the scanner does not classify, so it needs no row. Constants: EXPECTED_ROWS 371 -> 376, EXPECTED_PROVIDERS 322 -> 326. These describe `main` as it stands at 023dc0b; in-flight binding-removal PRs that also move `EXPECTED_ROWS` re-derive their own number at rebase time.
Removes the native uuid binding so `import { v4, parse, stringify, NIL }
from "uuid"` (no perry.compilePackages entry) resolves to the real npm
package compiled from source, per the owner's decision to stop shipping
hand-written Rust reimplementations of npm packages.
The native binding is missing `parse`/`stringify` entirely (a
`parse`/`stringify` roundtrip throws: `bytes` comes back `undefined`),
and `NIL` reads as `undefined` (js_uuid_nil exists in the deleted source
but was never wired into either NativeModSig dispatch table or the API
manifest, so property access on the uuid module namespace fell through
to undefined). v1/v3/v4/v5/validate/version were correct in both.
Per #10678 (duplicate extern "C" exports across perry-ext-*/perry-stdlib
pairs), this binding existed twice: crates/perry-ext-uuid/ (the
governance-tracked binding crate) and crates/perry-stdlib/src/uuid.rs (a
second, independent implementation behind the now-removed bundled-uuid
feature). Both are deleted, along with the 7-entry NativeModSig dispatch
block in native_table/utils_crypto.rs, the well_known_bindings.toml
entry, the "uuid" NATIVE_MODULES entry and its 7 manifest rows, and the
6 Android stub exports.
crypto/random.rs entanglement: crypto.randomUUID()/randomUUID({v7:true})
and nodemailer's message-id generation both call the `uuid` Cargo crate
(uuid::Uuid::new_v4()/now_v7()) directly and unconditionally — neither
site has a cfg(feature) gate. The `uuid` crate dependency in
perry-stdlib/Cargo.toml was therefore never actually optional in
practice even though it was declared `optional = true` behind the
bundled-uuid npm-binding feature; removing that feature without also
dropping `optional = true` would have broken the build the moment
bundled-uuid stopped being enabled. Made `uuid` a required (non-optional)
dependency and retargeted the `ids` feature umbrella to
`["bundled-nanoid"]`. crypto.randomUUID()/randomUUID({v7:true}) and
node:crypto's randomBytes are unaffected — verified below.
Retargeted the one dts-shape regression test that used uuid.v4() as its
zero-arg-module-function fixture (perry-api-manifest's
dts_uuid_v4_has_no_args) to perry/gc.minor(), which is unrelated to any
binding-removal churn.
Built on perrymaster (--profile perry-dev). A real `npm install uuid`
project (no perry.compilePackages entry) exercising v1/v4/v5/v3,
validate/version, a parse/stringify roundtrip, and NIL, diffed against
`node --experimental-strip-types` (Node 26.5.1): byte-for-byte identical,
including the deterministic v5/v3 (name+namespace) values. Confirmed
against a pristine origin/main (8df83f8) baseline build that the same
program crashes there: `NIL: undefined`, `parse instanceof Uint8Array:
false`, then `TypeError: Cannot read properties of undefined (reading
'length')` on the roundtrip — reproducing the reported bug exactly.
crypto.randomUUID() and node:crypto's randomBytes/randomUUID were
re-checked against the fix and still work (both call the `uuid` crate
directly, unaffected by the binding removal).
- cargo build --profile perry-dev -p perry -p perry-runtime-static -p
perry-stdlib-static: clean; confirmed .a mtimes moved.
- cargo check --workspace --all-targets (host-compatible exclusion set
via workspace_architecture.py --print-excluded-scope) under
-D warnings: clean.
- cargo test -p perry-api-manifest: 39+4 passing (after retargeting the
uuid-fixture test).
- cargo test -p perry-codegen --test manifest_consistency: 5/5 passing.
- cargo test -p perry --bin perry -- well_known: 27/27 passing.
- cargo test -p perry-hir: full suite passing (test_lower_native_module_
registration uses "uuid" only as synthetic example data for a generic
register/lookup mechanism — unaffected by the registry removal).
- python3 scripts/binding_governance.py --check: OK (39 extension
crates).
- node scripts/binding_pins.mjs --check: OK (37 pinned, lock-step
holds).
- python3 scripts/workspace_architecture.py --check: OK.
- python3 scripts/native_result_ledger.py: OK, 371 rows/322 providers
unchanged (none of uuid's dispatch rows used a ledger-tracked NR_*
kind).
- python3 scripts/string_payload_access_inventory.py --write-baseline:
perry-stdlib inline-offset 40 -> 38 (uuid.rs's own 2 sites).
- Regenerated docs/api/perry.d.ts + docs/src/api/reference.md
(perry --print-api-manifest) and docs/src/native-libraries/
governance.md (binding_governance.py --table) from the fixed manifest.
- cargo fmt --all -- --check: clean.
- scripts/run_lint_gates.sh (SKIP_COMPILE_GATES=1): 76 of 77 passed; the
one failure (Public benchmark evidence freshness) is the pre-existing,
known-red-on-every-PR gate per this campaign's contract.
- Compile tier of run_lint_gates.sh (known-red on Linux per this
campaign's contract).
- Full gap suite (host stalls under auto-optimize per contract).
- test-files/test_parity_uuid.ts is already excluded from the parity
gate (test-parity/known_failures.json, "ci-env": Node's own oracle run
fails ERR_MODULE_NOT_FOUND because uuid was never added to the repo's
root package.json/package-lock.json — the same pre-existing gap
documented for nanoid, #8271). Its `@covers` comment now points at a
deleted file (crates/perry-stdlib/src/uuid.rs); leaving it untouched,
matching how the sibling nanoid PR (#10693) left its own equivalent
parity fixture alone.
- No version bump / CLAUDE.md edit — per this campaign's convention, the
maintainer bumps at merge time.
- crates/perry-ui-android/src/stdlib_stubs.rs was edited to remove the
matching 6 js_uuid_* stub exports (following the established pattern
from the sibling removals) but not build-verified — this host has no
Android NDK and the package is excluded from the host-compatible
check scope.
… source Deletes the perry-ext-axios crate, perry-stdlib's axios.rs shim, the js_axios_* FFI surface, and every NATIVE_MODULES/manifest/HIR/codegen special-case that existed only to route the native binding. A plain `import axios from "axios"` with no perry.compilePackages entry now resolves and compiles the real npm package (and its transitive deps) from source instead. Must not merge before #10673 (agent-base namespace/export= fallback fix) -- axios's https-proxy-agent dependency needs it to compile.
Renamed the axios-removal changeset fragment now that the PR number is known, and pointed PENDINGCS-compile-smoke-known.md's dangling placeholder reference at it.
a397a18 to
041b7ed
Compare
Renamed the axios-removal changeset fragment now that the PR number is known, and pointed PENDINGCS-compile-smoke-known.md's dangling placeholder reference at it.
|
Landed in merge train 226 (#10751), released as v0.5.1605 — main is now Closing rather than merging is how trains work here: the four PRs were cherry-picked onto one tree, validated together, and landed under the train's own commit, so GitHub cannot mark this one merged even though your change is on main. The workspace count triple was re-derived on the assembled tree rather than taken from any PR's recorded value: 78 members / externalize=29 / keep=44. Both #10679 and #10691 correctly recorded 78/29/44 against Validation: all nine cheap gates, |
Summary
Removes the axios native binding (
perry-ext-axios,perry-stdlib/src/axios.rs, thejs_axios_*FFI surface, and everyNATIVE_MODULES/manifest/HIR/codegen special-casethat existed only to route it) so
import axios from "axios"resolves to the real npmpackage instead of Perry's hand-written reimplementation. Removal only — no other
binding touched, no behavior changes to anything that stays.
Do not merge before #10673 (
fix/10662-namespace-export-equals-fallback). Thisbranch is based on it, not on
main— axios does not compile without #10673'sagent-base/https-proxy-agentnamespaceexport =fallback fix. If this PR mergesfirst, real-source axios (and anything depending on it) breaks.
What was deleted
crates/perry-ext-axios(crate +Cargo.tomlworkspace membership)crates/perry-stdlib/src/axios.rsand itsmod/pub useinlib.rs, and the axiosresponse-property arm in
common/dispatch/property_dispatch.rs[bindings.axios]block inwell_known_bindings.toml"axios"NATIVE_MODULESentry and its 11 manifest method rows (entries.rs,entries/part_4.rs)(
lower_call/options/fetch.rs), and thejs_axios_*FFI declarations(
runtime_decls/stdlib_ffi/third_party.rs) plus their runtime/no-op-stubimplementations (
perry-runtime/src/closure/v8_stubs.rs+closure/mod.rs'sre-export,
perry-ui-android/src/stdlib_stubs.rs)Response" local-instance tagging that existedonly to route
.status/.data/.statusTextthrough the now-deleted native dispatch(
local_natives.rs,destructuring/var_decl/native_new.rs,lower/module_decl.rs,lower/stmt.rs)emit.rs's "callable default export" special-case, which existed only for axios'saxios(config)shape (its two axios-specific.d.tstests went with it)axios_response_property_lowering.rs) and the native-dispatchNaN-boxing regression test (
test_issue_340_axios_response_props.ts+ itsknown_failures.jsonentry) — both tested internals of the deleted shimworkspace-architecture.json's crate entry and baseline counts (workspace_members83→82,
decision_counts.externalize33→32)Regenerated
docs/api/perry.d.ts,docs/src/api/reference.md, anddocs/src/native-libraries/governance.md's table. Dropped axios's row/section fromdocs/native-libraries.mdand its line from theperry native listexample indocs/src/cli/commands.md. Swappedunimplemented_api_check.rs'ssupported_module_with_unknown_member_is_rejectedwitness from axios tonode-fetch(still native) since the test needs a live
NATIVE_MODULESmember.Left alone (still legitimate as npm-package-name examples, unrelated to the native
binding): axios entries in
crates/perry/src/commands/install/scanner/typosquat.rs+top_packages.txt(typosquat popularity data),crates/perry-hir/src/capability.rs(per-package capability-policy test fixtures), and comments across
perry-stdlib/perry-runtime/perry-codegendescribing real axios behavior thatmotivated generic (non-axios-specific) fixes.
The acceptance test that matters
A plain
import axios from "axios"with noperry.compilePackagesentry for axios atall compiles and runs correctly. Verified two ways:
tests/release/packages/axios-get— itspackage.jsonalready had nocompilePackagesblock (it didn't need one when axios was native) and needs none noweither.
bash fixture.shpasses: real GET/HEAD/OPTIONS round-trip against anin-process
node:httpserver, diffed againstexpected.txt.{"dependencies": {"axios": "^1"}}+npm install,compiled and run against a live server —
RESULT: PASS.No
package.jsonconfiguration beyond a plain"axios"dependency is required.Perry's default "compile npm package source when nothing claims the specifier natively"
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) automatically —
perry compilereports "130 module(s): 130 native, 0JavaScript" for them. The earlier probe's
perry.compilePackageslist (used to forcenative-source compilation before this removal) is not needed post-removal — that was
testing something different from what a user actually needs to do.
Validation
cargo check --workspace --tests(minus cross-host UI crates): clean except twopre-existing breaks on this base branch, confirmed via
git stashA/B (neithertouches anything this PR changed):
perry-codegen's test target fails to compile(
ImportedClassmissingconstructor_has_synthetic_argumentsin two unrelated testfiles) and
perry-runtime --testscarries one pre-existing dead-code warning inbox.rs.cargo test:perry-api-manifest41/41,perry-hirall suites 0 failed,perry-stdlib139/139 (RUST_TEST_THREADS=1),perry1136 lib tests + the 5integration 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. Notrun: the full
crates/perry/tests/*.rssweep (~100 files, ~3 min each on the sharedbuild host) — unrelated to this diff, and CI's
e2e-scopedonly runs integrationtests the diff actually names, 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 in this repo.
Compile tier not run (known-red on Linux per the campaign brief).
cargo fmt --all -- --check: clean.axios/js_axios_/perry-ext-axiosacross the whole repo before andafter; the only post-removal matches are historical (
CHANGELOG.md, dated auditsnapshots,
docs/po/*.po), illustrative comments/examples unrelated to the nativebinding (typosquat data, capability-policy test fixtures, generic bug-history prose),
and the still-passing
axios-getrelease fixture.Acceptance result: this is an improvement, not a neutral swap
Real axios with its full 27-package transitive graph, no
perry.compilePackagesentry: 130 modules, all native, correct status codes and response bodies for GET and POST, exit 0, matching Node 26.5.1.One divergence exists —
response.headersis an empty object — and it is pre-existing, filed as #10744, ruled out as this PR's doing in four steps: a dependency-freenode:httpcontrol does not diverge; the key is absent rather than miscased or mistyped; it is not #10668's documentedrawHeaderscasing limitation (that PR explicitly statesres.headersis correct); and on pristinemainwith the native binding verified to be serving the import,status,dataandheadersare allundefinedand the program crashes withCannot read properties of undefined (reading 'content-type').So real-source axios is strictly better than the binding this PR removes: status and data correct with headers incomplete, versus nothing correct followed by a crash. This removal is a clear improvement with one known remaining gap, not a neutral swap that introduces one.
Counts corrected, twice, as main moved. This PR previously recorded
workspace_members83→82 /externalize33→32 (stale against pre-train-223 main), then 79/30/44 (stale against pre-train-225 main, from when #10701's uuid removal hadn't landed yet). Main is now053b9ccac4(train 225, v0.5.1604), which already carries the uuid removal, so it starts at 79 workspace members / 30 externalize / 44 keep, not 80/31/44. Removing the axios crate (decision: "externalize") from that base gives workspace_members = 78, externalize = 29, keep = 44 (merge=1, remove=1, review=3 unchanged; sum 78).workspace_architecture.py --check --print-summaryindependently reproduces 78/29/44 from the resolved tree.This rebase hit the exact false-merge trap #10739 describes:
workspace-architecture.json's baseline numbers and the deletedperry-ext-axioscrate entry live on separate lines, so git's 3-way merge silently kept79/30/44(both this branch's prior resolution and train 225's uuid removal had independently rewritten the same line to the same value, so git saw "no conflict" rather than "two decrements need composing"). Caught by recomputing from the tree rather than trusting the clean auto-merge; corrected by hand before continuing.unrooted_local_shape_baseline.json(578, unchanged — no diff vs main, confirmed rather than assumed) andnative_result_ledger(green on main post-#10738: 376 rows / 326 providers,EXPECTED_ROWSuntouched — no axios rows in the tsv on either side) both re-derived clean.Cargo.tomlversion takes main's (0.5.1604);Cargo.lockregenerated viacargo metadata --offline, not hand-merged. Confirmed none of the at-cap files (perry-codegen/src/stmt/let_stmt.rs,perry-hir/src/lower/stmt_loops.rs,perry-runtime/src/gc/tests/copying.rs,perry-runtime/src/object/native_module/module_keys.rs, and others within #10750's list) are touched by this diff.#10691 and #10701 also computed to 79/30/44 against the pre-225 main; #10701 has since landed (consuming that value in 225), #10691 is next to rebase and — depending on landing order relative to this PR — will need either 78/29/44 or 77/28/44, re-derived at that time rather than copied from here.