diff --git a/crates/perry-codegen/src/ext_registry.rs b/crates/perry-codegen/src/ext_registry.rs index c86e6a98d..eddb8f256 100644 --- a/crates/perry-codegen/src/ext_registry.rs +++ b/crates/perry-codegen/src/ext_registry.rs @@ -462,6 +462,47 @@ const FFI_REGISTRY: &[(&str, OwnerKind)] = &[ ("js_url_create_object_url", OwnerKind::Stdlib { feature: Some("http-client") }), ("js_url_revoke_object_url", OwnerKind::Stdlib { feature: Some("http-client") }), ("js_buffer_resolve_object_url", OwnerKind::Stdlib { feature: Some("http-client") }), + + // ── #5140: EventEmitter without a source-level `import "events"` ── + // `new EventEmitter()` / `.on` / `.emit` / … lower to these + // `js_event_emitter_*` helpers purely off the class NAME being + // `EventEmitter` (`lower_call/builtin.rs` + the events native-table + // rows), regardless of which package the binding came from. The + // canonical implementations live in `perry-ext-events` (the + // `[bindings.events]` well-known crate); `import "events"` flips that + // wrapper onto the link line via `ctx.native_module_imports`. But a + // program that gets `EventEmitter` from a *different* package — e.g. + // `import EventEmitter from "eventemitter3"` under + // `perry.compilePackages` — never inserts `"events"` into the import + // set, so the wrapper stays off the link line and the build fails with + // `Undefined symbols: _js_event_emitter_new_with_options` (and the + // `_on` / `_emit` / `_remove_all_listeners` companions). Tagging the + // emitted symbols here makes the well-known flip fire off codegen + // provenance instead of imports — same mechanism as the #846 / #3954 + // http/net rows above. Flipping `"events"` also activates perry-stdlib's + // `external-events-construct` feature (see optimized_libs.rs), which the + // default-import dynamic-`new` path relies on (#4995). + // + // Only the core surface defined by perry-ext-events is listed; the + // `js_event_emitter_async_resource_*` helpers live in perry-stdlib and + // are out of scope here (`EventEmitterAsyncResource` is node:events-only). + ("js_event_emitter_new", OwnerKind::WellKnown("events")), + ("js_event_emitter_new_with_options", OwnerKind::WellKnown("events")), + ("js_event_emitter_on", OwnerKind::WellKnown("events")), + ("js_event_emitter_once", OwnerKind::WellKnown("events")), + ("js_event_emitter_prepend_listener", OwnerKind::WellKnown("events")), + ("js_event_emitter_prepend_once_listener", OwnerKind::WellKnown("events")), + ("js_event_emitter_emit", OwnerKind::WellKnown("events")), + ("js_event_emitter_emit0", OwnerKind::WellKnown("events")), + ("js_event_emitter_remove_listener", OwnerKind::WellKnown("events")), + ("js_event_emitter_remove_all_listeners", OwnerKind::WellKnown("events")), + ("js_event_emitter_listener_count", OwnerKind::WellKnown("events")), + ("js_event_emitter_listeners", OwnerKind::WellKnown("events")), + ("js_event_emitter_raw_listeners", OwnerKind::WellKnown("events")), + ("js_event_emitter_event_names", OwnerKind::WellKnown("events")), + ("js_event_emitter_set_max_listeners", OwnerKind::WellKnown("events")), + ("js_event_emitter_get_max_listeners", OwnerKind::WellKnown("events")), + ("js_event_emitter_domain_value", OwnerKind::WellKnown("events")), ]; /// Process-wide collector of provider keys observed during codegen. @@ -632,6 +673,42 @@ mod tests { } } + /// #5140 regression: `new EventEmitter()` / `.on` / `.emit` / + /// `.removeAllListeners` lower to `js_event_emitter_*` helpers off the + /// class name alone, so a program that imports `EventEmitter` from a + /// non-`events` package (`import EventEmitter from "eventemitter3"`) + /// emits these symbols without ever inserting `"events"` into the import + /// set. Each must route to `WellKnown("events")` so the well-known flip + /// pulls `perry-ext-events` onto the link line; before the fix the build + /// failed with `Undefined symbols: _js_event_emitter_new_with_options`. + #[test] + fn emitted_event_emitter_symbols_route_to_events() { + let _guard = PROVIDER_TEST_LOCK + .lock() + .expect("provider test lock poisoned"); + for symbol in [ + "js_event_emitter_new", + "js_event_emitter_new_with_options", + "js_event_emitter_on", + "js_event_emitter_once", + "js_event_emitter_prepend_listener", + "js_event_emitter_prepend_once_listener", + "js_event_emitter_emit", + "js_event_emitter_emit0", + "js_event_emitter_remove_listener", + "js_event_emitter_remove_all_listeners", + "js_event_emitter_listener_count", + "js_event_emitter_listeners", + "js_event_emitter_raw_listeners", + "js_event_emitter_event_names", + "js_event_emitter_set_max_listeners", + "js_event_emitter_get_max_listeners", + "js_event_emitter_domain_value", + ] { + assert_symbol_routes_to(symbol, OwnerKind::WellKnown("events")); + } + } + /// #2013 regression: net validation fixtures emit provider-only /// `perry-ext-net` symbols through native-table lowering. Each one must /// route to the net well-known archive so `PERRY_NO_AUTO_OPTIMIZE=1`