Skip to content

regression: perry.define of process.env.NODE_ENV no longer applied (ignored since process became a real object) → React/react-reconciler load dev builds → 'value is not a function' crash (blocks ink) #5009

@proggeramlug

Description

@proggeramlug

Summary

perry.define of process.env.* keys (e.g. process.env.NODE_ENV) is no longer applied — neither as a compile-time constant substitution nor at runtime. process.env.NODE_ENV reads undefined regardless of the define. As a consequence, compiled packages that branch on process.env.NODE_ENV (React, react-reconciler, scheduler, …) select their development builds, and react-reconciler's dev build then throws TypeError: value is not a function during render — blocking ink (#348) at the final step.

This worked in the 2026-05-29 recheck (define folded React to its production build and tree-shaking pruned the dev build). It is a regression, almost certainly from #4993 — making process.env a real runtime object means process.env.NODE_ENV is now a live property read that bypasses the define's static substitution.

Evidence

package.jsonperry.define:

{ "process.env.NODE_ENV": "production", "process.env.DEV": "false" }

(also tested the quoted form "\"production\"" — same result)

# perry.define present, env var NOT set:
process.env.NODE_ENV  →  undefined          ✗   (define ignored)

# define ignored, but the real env IS read at runtime:
NODE_ENV=production ./app   →  process.env.NODE_ENV === "production"
FOO=bar ./app               →  process.env.FOO === "bar"

So process.env faithfully reflects the real environment (good, #4993), but the build-time define no longer intercepts process.env.*.

Consequence — react-reconciler runs its dev build and crashes

node_modules/react-reconciler/index.js:

if (process.env.NODE_ENV === 'production') module.exports = require('./cjs/react-reconciler.production.js');
else                                       module.exports = require('./cjs/react-reconciler.development.js');

With process.env.NODE_ENV unresolved (undefined), the else (development) branch is taken. ink's render()reconciler.updateContainerSync(...) then throws TypeError: value is not a function.

Native backtrace (--debug-symbols, breakpoint on throw_not_callable):

#0  perry_runtime::closure::dispatch::throw_not_callable
#1  js_closure_call1
#2  perry_closure_node_modules_react_reconciler_cjs_react_reconciler_DEVELOPMENT_js__202
#3  …react_reconciler_DEVELOPMENT_js__194
#4  …react_reconciler_DEVELOPMENT_js__573
#5  …react_reconciler_DEVELOPMENT_js__656
#6  …react_reconciler_DEVELOPMENT_js__655
#7  js_native_call_value
#8  js_native_call_method
#9  perry_method_node_modules_ink_build_ink_js__Ink__render
…
#18 perry_closure_node_modules_ink_build_render_js__0
#20 main

The frames are react_reconciler_*DEVELOPMENT*_js — confirming the dev build is what executes.

Setting NODE_ENV=production in the runtime environment does not fix it (react-reconciler's branch is resolved at module-init, before the fix would matter, and the define — not the runtime env — is the intended mechanism for build selection).

Two issues, one root

  1. Primary (this issue): perry.define must apply to process.env.* again — fold process.env.NODE_ENV to the configured constant at compile time so === 'production' resolves and the production branch is selected (and the dev build tree-shakes away). The fix should make the define authoritative even though process.env is now a real object (e.g. substitute defined process.env.X reads with the literal before the runtime lookup, and/or seed the runtime process.env from perry.define at startup).
  2. Secondary (separate): react-reconciler's development build hits a value is not a function codegen gap under Perry (frames __655/__656/__573/__194/__202). Not the target build, but a real latent gap — worth a follow-up if dev-build support is ever desired.

Impact

  • Every compilePackages project relying on define for NODE_ENV — i.e. essentially all React/Preact/Vue/etc. apps, which ship separate dev/prod builds gated on process.env.NODE_ENV. They silently get dev builds (slower, dev-only asserts) or crash on dev-only code paths.
  • ink end-to-end (Compile ink (React-based TUI framework) end-to-end via perry.compilePackages #348): this is the last wall before yoga — with the production reconciler, render() should proceed into layout (then the out-of-scope yoga-WASM terminal).

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions