Skip to content

EPBDS-16211 G: compile JSX with the automatic runtime - #129

Merged
AlexSamBY merged 1 commit into
masterfrom
g-automatic-jsx-runtime
Sep 30, 2026
Merged

AlexSamBY merged 1 commit into
masterfrom
g-automatic-jsx-runtime

Conversation

@AlexSamBY

Copy link
Copy Markdown
Member

What changes

  • babel.config.js: @babel/preset-react now runs with runtime: 'automatic'. JSX compiles to calls into react/jsx-runtime instead of React.createElement. React 19 expects the new transform (§8), and every React the library supports ships the runtime, from the 16.14 floor up.
  • webpack.library.config.mjs: react/jsx-runtime is an external beside react, so the host's React supplies it. Bundled, it would be this repository's React 18 copy, creating elements through internals that belong to a different React than the host renders them with. react/jsx-dev-runtime is not listed: Babel emits it only under preset-react's development option, which this config does not set.

The shared Babel config covers the demo and jest as well. On the React 16/17 legs, the existing ^react/(.*)$ mapping already routes react/jsx-runtime to the pinned copy.

Measured on the published bundle (build-lib, before → after)

before after
dist/index.js 337,438 bytes 328,053 bytes (−2.8%)
createElement occurrences 532 163
external requires moment, react, react-dom the same plus react/jsx-runtime
other published files — byte-identical (.d.ts, every stylesheet)
  • Where the 163 remaining createElement come from, attributed through the source map:
    • 161 are in bundled libraries compiled with the classic transform: 116 in rc-picker, 16 in @rc-component/trigger, 13 in rc-overflow, and the rest in small ones;
    • 1 is the explicit call in Render.js;
    • 1 is a DOM document.createElement in downloadFile.js.
  • No copy of React's runtime is bundled. The strings react.element / react.transitional.element were in the bundle before this change too; they come from a bundled library. The source map lists react/jsx-runtime only as external umd.
  • The packed artifact on each React. The packed consumer server-rendered it on React 18.3.1 (test:pack) and on 16.14.0 and 17.0.2 (test:pack:peers). A probe on 19.2.0 also passed (node scripts/test-packed-react-matrix.js 19.2.0).

Left as is, on purpose

The import React lines stay: they are harmless under the new runtime, and tsc still checks JSX with "jsx": "react". Removing them and switching tsc to react-jsx is churn for a PR of its own.

Gates

All exited 0:

  • lint:js, lint:css, typecheck;
  • css:fixture:check, docs:props:check, docs:views:check;
  • test:env-flags, test:types, test:coverage, build;
  • test:pack, test:pack:peers;
  • npx jest, test:react16, test:react17, 2907 tests on each;
  • test:e2e, 41 Playwright tests;
  • the manifest contract.

docs/UPGRADE-PLAN.md (§8, §9.8, §10, Appendix C) and CLAUDE.md record the change.

🤖 Generated with Claude Code

JSX now compiles to calls into `react/jsx-runtime` instead of
React.createElement. React 19 expects the new transform, and every React the
library supports ships the runtime, from the 16.14 floor up.

The library build lists `react/jsx-runtime` as an external beside `react`,
so a host's own React supplies it. Bundled, it would be this repository's
React 18 copy creating elements through a different React's internals.
`react/jsx-dev-runtime` is not listed: Babel emits it only under
preset-react's `development` option, which this config does not set.

Measured on the published bundle:
- dist/index.js shrinks from 337,438 to 328,053 bytes.
- `createElement` occurrences drop from 532 to 163. Through the source map,
  161 are in bundled libraries compiled with the classic transform (116 in
  rc-picker), one is the explicit call in Render.js, and one is a DOM
  `document.createElement`.
- The external requires are moment, react and react-dom, plus
  react/jsx-runtime. The source map shows the runtime only as an external;
  no copy of it is bundled.
- Everything else published is byte-identical: the .d.ts and every
  stylesheet.

The packed consumer server-rendered the bundle on React 16.14.0, 17.0.2 and
18.3.1, and on 19.2.0 as a probe. The `import React` lines stay: they are
harmless under the new runtime, and tsc still checks JSX with "jsx": "react".
@AlexSamBY
AlexSamBY merged commit baff861 into master Sep 30, 2026
5 checks passed
@AlexSamBY
AlexSamBY deleted the g-automatic-jsx-runtime branch September 30, 2026 12:11
AlexSamBY added a commit that referenced this pull request Sep 30, 2026
…#130)

## The problem, introduced by #128

#128 moved `webpack-dev-server` to 6.
`@pmmmwh/react-refresh-webpack-plugin` 0.5.17 declares it as an optional
peer only up to 5.x:

```
peerOptional webpack-dev-server@"3.x || 4.x || 5.x" from @pmmmwh/[email protected]
```

`npm ci` installs the lock regardless, which is why #128's and #129's CI
stayed green. But every `npm install <package>` since then aborts with
`ERESOLVE`. It was found while adding a devDependency for the React 19
leg.

## The fix

`@pmmmwh/react-refresh-webpack-plugin` `^0.5.16` → `^0.6.3`. 0.6.3
(2026-08-27) adds `webpack-dev-server` 6.x to that peer range
([#1046](pmmmwh/react-refresh-webpack-plugin#1046)
upstream). The one breaking release between them, 0.6.0, raised minimums
we already exceed (Node 18.12, webpack 5.2, webpack-dev-server 4.8). It
also removed `overlay.sock*` options we never set: the demo uses `new
ReactRefreshWebpackPlugin()` with no options.

## Checked

- **The install that failed now succeeds**, run as `npm install
--dry-run --save-dev <the same packages>`.
- **`npm ls --all` reports no invalid peer**, and `npm audit` stays at
0.
- **Fast refresh works on the new plugin.** In the running demo (`npm
start`), the refresh runtime is injected (`__reactRefreshInjected`). A
temporary edit to `NavTabs.jsx`, reverted after, is hot-applied: `[HMR]
Updated modules: ./src/demo/components/NavTabs.jsx` and `[HMR] App is up
to date.` A marker set on `window` beforehand survives, so the page did
not reload.
- **The published package is unchanged.** All 282 files `build-lib`
publishes are byte-identical to master's; the plugin is demo-only.

## Gates

All exited 0:
- `lint:js`, `lint:css`, `typecheck`;
- `test:env-flags`, `test:coverage`, `build`, `build-lib`;
- `test:react16` and `test:react17`, 2907 tests on each;
- the manifest contract.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
AlexSamBY added a commit that referenced this pull request Sep 30, 2026
The peers now read `^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0`, widened
additively, so hosts on 16.14, 17 and 18 change nothing. §8's fast path is
complete: gates 1 and 3 were met, gate 2 lapsed with the dependency trim, and
this is gate 4.

- The suite. React 19.3.0 is a fixture leg like the 16.14 and 17 ones: an
  install-only package (scripts/fixtures/react19) that plain `npm ci` nests,
  mapped in at resolve time, and `npm run test:react19` runs it locally.
  The harness self-checks the loaded version in every worker, as before.
  React 19 has no legacy root and ships react-dom/client, so this leg renders
  through createRoot: floors.js records `legacyRoot: false`, which skips the
  RTL patch, the ReactDOM.render assertion and the react-dom/client stub.
- CI. `react-19` replaces the `continue-on-error` `react-19-advisory` job,
  which installed 19 over the lock with --legacy-peer-deps. The new job
  needs neither.
- The packed artifact. `test:pack:peers` now server-renders it on 16.14.0,
  17.0.2 and 19.3.0; `test:pack:consumer` covers 18.3.1.
- The types. `test:types` compiles the published declarations against
  @types/react 19 as well, both import styles; 19 is the version that
  removed the global JSX namespace. gen-ts stays on the dev tree's 18.

README, the demo's docs and changelog, CLAUDE.md and the plan record it. The
changelog also records #129's `react/jsx-runtime` import, which a host that
aliases `react` can see.

Local gates exit 0: the React 18, 16, 17 and 19 legs pass 2907 tests each.
Two first runs lost a worker to SIGSEGV (the Node 24 V8 crash,
nodejs/node#62393): test:react19 once and test:react17 once. Both passed
2907/2907 on rerun.
AlexSamBY added a commit that referenced this pull request Sep 30, 2026
On master since #130 merged, which fixed the `npm install` peer conflict
this change needed gone to add a devDependency. The rebased tree is
identical to the one the local gates below ran on.

## The flip

`peerDependencies`: `react` and `react-dom` now read **`^16.14.0 ||
^17.0.0 || ^18.0.0 || ^19.0.0`**. The range is widened additively, so
hosts on 16.14, 17 and 18 change nothing. This closes §8's fast path:
- gates 1 (Semantic UI React gone) and 3 (`defaultProps`, then the
automatic JSX runtime in #129) were met;
- gate 2 (the form stack) lapsed when `dependencies` was emptied (#128
records why);
- this PR is gate 4.

## How 19 is gated, in three matrices

| | 16.14 | 17 | 18 | 19 |
|---|---|---|---|---|
| whole jest suite | `test:react16` | `test:react17` | `npx jest` /
`test:coverage` | **`test:react19` (new)** |
| packed artifact server-rendered | `test:pack:peers` |
`test:pack:peers` | `test:pack:consumer` | **`test:pack:peers` (added
19.3.0)** |
| published `.d.ts` compiled | `test:types` | `test:types` |
`test:types` | **`test:types` (added `@types/react` 19.3.0)** |

- **The suite on React 19 is a fixture leg, like 16.14 and 17.**
`scripts/fixtures/react19` is an install-only package pinning
`react`/`react-dom` 19.3.0. Plain `npm ci` nests it, it is mapped in at
resolve time, and `npm run test:react19` runs it locally too. The
harness still asserts the loaded React (19.3.0) and scheduler (0.28) in
every worker.
- **What differs for 19.** React 19 has no legacy root and ships
`react-dom/client`, so the leg renders through `createRoot` as the
default suite does. `floors.js` records `legacyRoot: false`, which skips
three things that exist only for 16/17: the RTL `legacyRoot` patch, the
`ReactDOM.render` assertion and the `react-dom/client` stub.
- **CI.** The new `react-19` job replaces the `continue-on-error`
`react-19-advisory` job, which installed React 19 over the lock with
`--legacy-peer-deps`; the new job needs neither. No required checks are
configured on master today, so `react-19` gates exactly as
`react-16-floor` and `react-17` do. If branch protection ever named
`react-19-advisory`, it should name `react-19` now.
- **Types.** The published declarations compile against `@types/react`
19, with both the interop default import and the direct CommonJS import.
That matters because 19 removed the global `JSX` namespace. `gen-ts`
stays on the dev tree's 18; the plan's "pin `@types/react@19` for
`gen-ts`" item was written assuming the dev tree would move to 19, and
it does not.

## Docs

- README and the demo's docs page state the new range.
- The demo's changelog (Unreleased) says React 19 is supported and how
it is tested. It also records #129's change: the bundle imports
`react/jsx-runtime` from the host's React, which a host that aliases
`react` to one copy has to alias too. Two older Unreleased paragraphs
that said the range was not widened yet are updated, so the section does
not contradict itself.
- `CLAUDE.md` lists the three React legs.
- The plan closes §8's gate 4, the CI-coverage row, the `gen-ts` row,
the Appendix C "React 19 flip" checklist and §10's Phase 7 React 19
item.

## Gates

All exited 0:
- `lint:js`, `lint:css`, `typecheck`, `typecheck:contract`;
- `css:fixture:check`, `docs:props:check`, `docs:views:check`;
- `test:env-flags`, `test:types`, `test:coverage`, `build`;
- `test:pack`, `test:pack:peers` (16.14.0, 17.0.2, 19.3.0);
- `test:react16`, `test:react17`, `test:react19`, 2907 tests on each, as
on 18;
- `test:e2e`, 41 Playwright tests;
- the manifest contract.

Two first runs lost a jest worker to `SIGSEGV`, each killing one suite
before its tests ran: `test:react19` once and `test:react17` once. That
is the Node 24 V8 garbage-collector crash
([nodejs/node#62393](nodejs/node#62393)),
fixed on `v24.x-staging` but not in a release yet. Both passed 2907/2907
on rerun. CI runs Linux, where it has not been seen.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant