fix(runtime): node:process default import & globalThis.process expose the full process object (#4987)#4993
Merged
Merged
Conversation
… the full process object (#4987) The bare `process` identifier lowers env/stdout/stderr/stdin/platform/ arch/argv/pid/ppid/version/versions to codegen intrinsics, but `import process from 'node:process'` and `globalThis.process` resolve through the native-module runtime dispatcher, which only knew metadata extras (argv0, config, ...) — the core properties read as undefined. - process_metadata_property now serves the core value-properties from the same runtime constructors the intrinsics call, so all three forms observe the same live objects (env/stdout are cached singletons). - PROCESS_NAMESPACE_KEYS / PROCESS_DEFAULT_KEYS gain stderr/stdin/stdout so Object.keys(process) enumerates them like Node. - New gap test test_gap_process_import_4987.ts — byte-identical vs node --experimental-strip-types. Unblocks terminal-size (ink #348 "Blocker #2: process import").
This was referenced Jun 11, 2026
proggeramlug
added a commit
that referenced
this pull request
Jun 11, 2026
…ntEmitter (#4995) (#5002) The bare core specifier `events` (default import, namespace import, or `require('events')`) produced an EventEmitter whose instances had an empty prototype — `.on`/`.emit`/`.setMaxListeners` were all undefined — while `node:events` worked. This blocked signal-exit (cli-cursor → restore-cursor), the next ink wall after #4993. Two layers, both fixed: - HIR (expr_new.rs, expr_member.rs): `new EE()` over an events default import / namespace import / CJS alias now lowers to the real `EventEmitter` constructor (Expr::New { class_name: "EventEmitter" }) instead of the empty-object placeholder. Emitter method-value reads on a native `events` instance are gated on module name (not the canonical class name) so an aliased constructor binding's reads stay PropertyGet rather than calling `events.on()` with no receiver. - Runtime + stdlib: a new `js_set_native_events_construct` dispatcher lets `js_new_function_construct` build a real emitter when `new` lands on a bound `events.EventEmitter` export value. The handle-dispatch arms in perry-stdlib now call the linker-resolved `extern "C"` events symbols (probe / on / constructors) instead of in-crate `crate::events::*`, so when the well-known flip links perry-ext-events, dynamic dispatch consults the same handle registry the constructors used — previously every dynamic `.on`/`.emit` on an emitter silently no-op'd. New `external-events-construct` feature wires the constructor registration when the flip strips `bundled-events`. Mirrors the sqlite duplicate-symbol contract (#643). New gap test test_gap_events_import_4995.ts — byte-identical vs node --experimental-strip-types. Zero gap-suite regressions vs baseline. Co-authored-by: Ralph Küpper <ralph@skelpo.com>
This was referenced Jun 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4987.
Problem
import process from 'node:process'andglobalThis.processreturned an incomplete process object —env,stdout,stderr,stdin,platform,arch,argv,pid,ppid,version,versionswere allundefined(only callables likecwdresolved). Only the bareprocessidentifier was fully populated. This was the next ink wall (#348 "Blocker #2: process import"):terminal-sizedoesconst {env, stdout, stderr} = processoff thenode:processimport and threwTypeError: Cannot read properties of undefined (reading 'COLUMNS')during ink's first render.Root cause
The bare
processidentifier lowers those members to codegen intrinsics (Expr::ProcessEnv,Expr::ProcessStdout, …). The default import andglobalThis.processare native-module namespace objects whose property reads go through the runtime dispatcher — both the static path (js_native_module_property_by_name) and the dynamic path (js_object_get_field_by_name's NATIVE_MODULE arm) funnel intoget_native_module_constant→process_metadata_property, which only knew metadata extras (argv0,execPath,config, …). The core properties fell through toundefined.Fix
process_metadata_property(crates/perry-runtime/src/process.rs) now serves the core value-properties from the same runtime constructors the codegen intrinsics call (js_process_env,js_process_stdout,js_os_platform, …), so all three forms observe identical values —envand the stream objects are live cached singletons, matching Node's identity semantics.PROCESS_NAMESPACE_KEYS/PROCESS_DEFAULT_KEYS(crates/perry-runtime/src/object/native_module.rs) gainstderr/stdin/stdoutsoObject.keys(process)enumerates them like Node (the values were previously unreachable, so the keys were missing too).Validation
test-files/test_gap_process_import_4987.ts— byte-identical vsnode --experimental-strip-types: issue repro, full member sweep on the default import,globalThis.process, value-identity with the bare identifier, and theterminal-sizedestructure shape.import * as ns from 'node:process'sweep (incl.ns.default.platform === ns.platform) byte-identical vs Node.test_gap_node_process,test_gap_process_allowed_flags,test_gap_process_features,test_gap_process_misc_3045plus,test_gap_process_emitter_3047_3046_3050.cargo test --release -p perry-runtime(RUST_TEST_THREADS=1): 1022 passed, 1 failed —date::tests::test_full_year_setters_revive_invalid_date_only, pre-existing on this base (fails with the change stashed too).perry-api-manifesttests green.cargo fmt --checkand the file-size gate pass.Code-only PR — no version bump / changelog per maintainer-folds-metadata-at-merge convention.
After this, ink's next gate is yoga-layout's WASM runtime (out of scope, #348).