diff --git a/scripts/build.ts b/scripts/build.ts index 43bacd4e2..9af3373db 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -29,9 +29,15 @@ const productionReactModules = new Map([ 'react/jsx-runtime', join(reactPackageDir, 'cjs/react-jsx-runtime.production.js'), ], + // NOT react-jsx-dev-runtime.production.js: that file exports + // `jsxDEV: undefined` on purpose (production code is expected to use the + // non-dev transform), but Bun transpiles our JSX to jsxDEV() calls, so the + // real production file would leave every component invoking undefined() + // and the UI would never render. The shim dispatches to production + // jsx/jsxs; its own `react/jsx-runtime` import is remapped by this plugin. [ 'react/jsx-dev-runtime', - join(reactPackageDir, 'cjs/react-jsx-dev-runtime.production.js'), + join(import.meta.dir, 'reactJsxDevRuntimeProductionShim.js'), ], [ 'react-reconciler', diff --git a/scripts/reactJsxDevRuntimeProductionShim.js b/scripts/reactJsxDevRuntimeProductionShim.js new file mode 100644 index 000000000..40fa14edd --- /dev/null +++ b/scripts/reactJsxDevRuntimeProductionShim.js @@ -0,0 +1,18 @@ +// React's cjs/react-jsx-dev-runtime.production.js deliberately exports +// `jsxDEV: undefined` — production bundles are expected to compile JSX with +// the non-dev transform. Our CLI build runs Bun's transpiler without +// NODE_ENV=production, so every JSX callsite compiles to a jsxDEV() call. +// Mapping the specifier straight to React's production file therefore left +// the whole UI invoking undefined() and nothing past the startup banner ever +// rendered. Implement jsxDEV in terms of the production jsx/jsxs instead — +// the same dispatch React's own dev runtime performs, minus dev-only +// validation. The extra dev-transform args (source, self) are ignorable. +import { Fragment, jsx, jsxs } from 'react/jsx-runtime' + +export { Fragment } + +export function jsxDEV(type, config, maybeKey, isStaticChildren) { + return isStaticChildren + ? jsxs(type, config, maybeKey) + : jsx(type, config, maybeKey) +} diff --git a/scripts/reactJsxDevRuntimeProductionShim.test.ts b/scripts/reactJsxDevRuntimeProductionShim.test.ts new file mode 100644 index 000000000..210754606 --- /dev/null +++ b/scripts/reactJsxDevRuntimeProductionShim.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, test } from 'bun:test' +// eslint-disable-next-line no-restricted-imports -- comparing shim output against the real runtime is the point +import { Fragment, jsx, jsxs } from 'react/jsx-runtime' +import { + Fragment as ShimFragment, + jsxDEV, +} from './reactJsxDevRuntimeProductionShim.js' + +// The CLI bundle compiles every JSX callsite to jsxDEV() (Bun's dev +// transform) but bundles production React, whose jsx-dev-runtime exports +// `jsxDEV: undefined` — that combination rendered the entire TUI blank with +// no error (see the shim's header comment). These tests pin the shim's +// dispatch so a future edit can't silently reintroduce that failure. +describe('reactJsxDevRuntimeProductionShim', () => { + test('jsxDEV is a function (the whole reason the shim exists)', () => { + expect(typeof jsxDEV).toBe('function') + }) + + test('re-exports the real Fragment', () => { + expect(ShimFragment).toBe(Fragment) + }) + + test('routes to jsx() when isStaticChildren is false', () => { + const props = { className: 'a', children: 'hi' } + expect(jsxDEV('div', props, 'k', false)).toEqual(jsx('div', props, 'k')) + }) + + test('routes to jsxs() when isStaticChildren is true', () => { + const children = ['one', 'two'] + const props = { children } + expect(jsxDEV('div', props, undefined, true)).toEqual( + jsxs('div', props, undefined), + ) + }) + + test('returns the expected element shape for a trivial element', () => { + const el = jsxDEV('span', { children: 'x' }, 'key1', false) + expect(el.$$typeof).toBe(Symbol.for('react.transitional.element')) + expect(el.type).toBe('span') + expect(el.key).toBe('key1') + expect(el.props).toEqual({ children: 'x' }) + }) +})