Skip to content

Polyfill in @gridland/web dist bundle shadows Vite's define substitution #42

@to-be-coder

Description

@to-be-coder

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:

  1. 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.

  2. 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.

  3. 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

  • packages/web/dist/index.js line 1 no longer contains var process =
  • OpenTUI's dev console.warn guards fire in vite dev and next dev when the underlying conditions are true
  • The same guards are tree-shaken (or at minimum never fire) in vite build and next build
  • All existing tests still pass, including the e2e suite in packages/create-gridland
  • New test in packages/web/src/build-output.test.ts asserts the polyfill is on globalThis.process, not a local var

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions