Summary
The @gridland/web published bundle polyfills process using a local var declaration at the top of dist/index.js:
if (typeof process === "undefined") var process = { env: { NODE_ENV: "production" } };
Because this declares a local process variable, Vite's (and esbuild's) AST-aware define substitution treats every downstream process.env.NODE_ENV reference inside the bundle as a reference to the local variable and skips substitution. The result: no matter what NODE_ENV the host project is running in, the bundle's internal checks always read "production" at runtime from the polyfill.
Impact
This is a latent sharp edge, not a crash. After the react-reconciler externalization fix in 75b39d7, the primary crash (dispatcher.getOwner is not a function) is resolved because react-reconciler no longer lives inside the shadowed scope — the host bundler resolves it in its own module where substitution works normally.
However, the bundle still contains ~6 process.env.NODE_ENV !== "production" guards from OpenTUI core code, all wrapping dev-only console.warn calls, e.g.:
if (renderable.isDestroyed) {
if (process.env.NODE_ENV !== "production") {
console.warn(\`Renderable with id \${renderable.id} was already destroyed, skipping add\`);
}
}
Because the polyfill pins NODE_ENV to "production" at runtime, these dev warnings never fire — not even in vite dev or next dev. That's a DX degradation: developers lose the safety-net warnings they'd expect in dev mode.
It's also a footgun for the future: if anyone ever re-bundles a library like react-reconciler that branches on NODE_ENV at load time, the polyfill will silently force it into production mode and reintroduce a getOwner-style mismatch.
Proposed fix
Change the require-shim banner in packages/web/build-browser.mjs (and the equivalent one in packages/utils/build-utils.mjs) from a local var polyfill to a global-scoped polyfill that does not shadow:
- if (typeof process === "undefined") var process = { env: { NODE_ENV: "production" } };
+ if (typeof globalThis.process === "undefined") globalThis.process = { env: { NODE_ENV: "production" } };
With no var declaration, esbuild's AST analysis sees process as a free identifier and applies define substitution normally. In vite dev / next dev the bundle's process.env.NODE_ENV references get substituted to "development" at the host's build time and OpenTUI's dev warnings fire as expected. In production builds they get substituted to "production" and the warnings are tree-shaken.
Additional considerations
There are a few things to double-check when implementing:
-
Interaction with the existing define: { "process.env": JSON.stringify({}) } in packages/web/src/vite-plugin.ts. With shadowing removed, this define would actually apply during dep pre-bundling. esbuild uses longest-prefix matching so process.env.NODE_ENV (set by Vite's default) still wins over process.env, but any other process.env.FOO reference would become {}.FOO (undefined). Grep the bundle post-rebuild to confirm only NODE_ENV is ever read from process.env.
-
Global pollution. Assigning to globalThis.process affects the whole browser tab, not just @gridland/web. The typeof globalThis.process === "undefined" guard protects against overwriting a host-provided polyfill, but if another library expects process to be undefined, there's a very small behavioral change.
-
Test lock-in. Add an assertion to packages/web/src/build-output.test.ts that the bundle does not contain var process = — so a future change can't silently reintroduce the shadow.
Acceptance criteria
Related
Summary
The
@gridland/webpublished bundle polyfillsprocessusing a localvardeclaration at the top ofdist/index.js:Because this declares a local
processvariable, Vite's (and esbuild's) AST-aware define substitution treats every downstreamprocess.env.NODE_ENVreference inside the bundle as a reference to the local variable and skips substitution. The result: no matter whatNODE_ENVthe host project is running in, the bundle's internal checks always read"production"at runtime from the polyfill.Impact
This is a latent sharp edge, not a crash. After the
react-reconcilerexternalization fix in 75b39d7, the primary crash (dispatcher.getOwner is not a function) is resolved becausereact-reconcilerno longer lives inside the shadowed scope — the host bundler resolves it in its own module where substitution works normally.However, the bundle still contains ~6
process.env.NODE_ENV !== "production"guards from OpenTUI core code, all wrapping dev-onlyconsole.warncalls, e.g.:Because the polyfill pins
NODE_ENVto"production"at runtime, these dev warnings never fire — not even invite devornext dev. That's a DX degradation: developers lose the safety-net warnings they'd expect in dev mode.It's also a footgun for the future: if anyone ever re-bundles a library like
react-reconcilerthat branches onNODE_ENVat load time, the polyfill will silently force it into production mode and reintroduce agetOwner-style mismatch.Proposed fix
Change the require-shim banner in
packages/web/build-browser.mjs(and the equivalent one inpackages/utils/build-utils.mjs) from a localvarpolyfill to a global-scoped polyfill that does not shadow:With no
vardeclaration, esbuild's AST analysis seesprocessas a free identifier and applies define substitution normally. Invite dev/next devthe bundle'sprocess.env.NODE_ENVreferences get substituted to"development"at the host's build time and OpenTUI's dev warnings fire as expected. In production builds they get substituted to"production"and the warnings are tree-shaken.Additional considerations
There are a few things to double-check when implementing:
Interaction with the existing
define: { "process.env": JSON.stringify({}) }inpackages/web/src/vite-plugin.ts. With shadowing removed, this define would actually apply during dep pre-bundling. esbuild uses longest-prefix matching soprocess.env.NODE_ENV(set by Vite's default) still wins overprocess.env, but any otherprocess.env.FOOreference would become{}.FOO(undefined). Grep the bundle post-rebuild to confirm onlyNODE_ENVis ever read fromprocess.env.Global pollution. Assigning to
globalThis.processaffects the whole browser tab, not just@gridland/web. Thetypeof globalThis.process === "undefined"guard protects against overwriting a host-provided polyfill, but if another library expectsprocessto be undefined, there's a very small behavioral change.Test lock-in. Add an assertion to
packages/web/src/build-output.test.tsthat the bundle does not containvar process =— so a future change can't silently reintroduce the shadow.Acceptance criteria
packages/web/dist/index.jsline 1 no longer containsvar process =console.warnguards fire invite devandnext devwhen the underlying conditions are truevite buildandnext buildpackages/create-gridlandpackages/web/src/build-output.test.tsasserts the polyfill is onglobalThis.process, not a localvarRelated
dispatcher.getOwner is not a functioncrash in Vite/Next dev servers (react-reconcileris now externalized, so the shadowing issue no longer affects React's dispatcher path).NODE_ENV="production"in the bundle).