Skip to content

feat: add React 19 support and modernize build tooling - #2

Open
samiashi wants to merge 7 commits into
lyst:masterfrom
samiashi:chore/upgrade-dependencies
Open

samiashi wants to merge 7 commits into
lyst:masterfrom
samiashi:chore/upgrade-dependencies

Conversation

@samiashi

@samiashi samiashi commented Aug 25, 2026 •

Copy link
Copy Markdown

Summary

Adds React 19 support to @lyst/hypernova-react alongside React 18, brings the toolchain up to date, and fixes two bugs found while testing it end to end against a real Hypernova server and browser. Released as 3.2.0: nothing in the published API, typings or runtime requirements gets stricter, so existing React 18 consumers can upgrade without changes.

What changed

  • React 19 support: peerDependencies widened to react/react-dom ^18.2.0 || ^19.0.0.
  • Fix: React 19 inside Hypernova's VM. createVM, which createGetComponent uses to evaluate server bundles, runs them in a sandbox without queueMicrotask or performance. React 19's server renderer needs both, so every server render failed. The package now provides them where they're missing. React 18 is unaffected.
  • Fix: server-render failures no longer cause client errors. When a server render fails, Hypernova sends an empty container. Hydrating it threw uncaught Hydration failed errors, so each server failure also showed up as client errors in error monitoring. Empty containers are now client-rendered with createRoot.
  • Build: Babel 8 with @babel/preset-env and @babel/preset-react replaces the unmaintained babel-preset-airbnb. lib/ is still CommonJS, and no longer requires @babel/runtime, which was never declared as a dependency.
  • CI: tests run on Node 22, 24 and 26 against both React 18 and 19, and each job runs build, typecheck and tests.

Verification

  • Every commit builds and passes typecheck and tests on its own; the full suite also passes with React 18.3.1 installed.
  • End-to-end in a browser, React 18.3.1 and 19.3.1. I packed the tarball, installed it into clean apps, rendered through a real Hypernova server evaluating the bundle via createVM, and hydrated in the browser. Results:
    • All components are server-rendered.
    • Hydration reuses the server DOM nodes, and interactivity works.
    • Context, lazy, effects, transitions and useSyncExternalStore work.
    • A failed server render falls back to a client render with no errors.
    • Props round-trip intact (including </script>), and nothing gets injected.
    • On React 19, ref as a prop, <title> hoisting, use(), form actions and useOptimistic also work.
  • Consumers: CommonJS require, Node ESM import (named and default) and esbuild bundling work. The typings check under nodenext, node16 and bundler resolution with TypeScript 5.9 and 7, with skipLibCheck: false.

Bump the package to 3.2.0 and modernize the full build stack. This is a
minor release: React 19 support is additive and nothing in the published
API, typings or runtime requirements becomes stricter.

- Widen peerDependencies to React ^18.2.0 || ^19.0.0 so existing React 18
  consumers keep working while React 19 is now supported; move devDependencies
  to current versions (react/react-dom ^19.3.1, @babel/* 8, jest 30,
  jest-environment-jsdom 30, prettier 3, TypeScript 7).
- Replace the unmaintained babel-preset-airbnb with the maintained
  @babel/preset-env and @babel/preset-react on Babel 8. Helpers are now
  inlined, so lib/ no longer requires the undeclared @babel/runtime.
- Force modules: "commonjs" in .babelrc: Babel 8's CLI self-reports ESM
  support, so the default 'auto' mode would leave ESM in lib/ and break
  CommonJS consumers of main: lib/index.js.
- Add @types/react and jest-environment-jsdom devDependencies for the
  typecheck and client-hydration test suite.
- Refresh transitive dev dependencies with npm audit fix. The remaining
  sprintf-js advisory has no patched release and is reachable only through
  jest's coverage tooling; the package itself has no runtime dependencies.
react-dom/client always has hydrateRoot in React 18+, so the old check for
whether hydrateRoot exists always chose hydration, and its createRoot
fallback never ran.

Hydrate containers that hold server markup, and render empty containers
with createRoot instead. Hypernova falls back to an empty container when
the server render fails; hydrating it made React 18 and 19 throw
"Hydration failed" before recovering, reported as uncaught errors on
window, so every server-side failure also showed up as client errors in
error monitoring. A component that renders nothing on the server also takes
the createRoot path, which gives the same result.
Serialize a server-rendered Hypernova payload (via renderToString +
serialize), render it into a jsdom document, then call renderReact's client
branch inside act() and assert that hydration actually ran: the server DOM
node is reused, a click handler attached during hydration updates state, and
React logs no hydration errors. The assertions fail if hydrateRoot is not
called, since the server HTML alone already contains the initial text.

Uses jest-environment-jsdom since the hypernova() client path runs only when
a DOM is present, plus a jest setup file that provides TextEncoder, which
jsdom lacks and React 18's browser react-dom/server build needs on import.

A second test serializes an empty container, as Hypernova does when the
server render fails, and checks that it is client-rendered without
hydration errors.
- Type renderReact for its server branch and export the
  ServerRenderedComponent (props -> HTML string) mapping. A second overload
  for the client branch would take the same parameters and so could never be
  selected; the client return value is not used in practice.
- Import only the types needed from react with `import type`, so the
  declarations do not depend on consumers enabling esModuleInterop.
- Add tsconfig.json (strict, NodeNext module resolution) and tests/types.ts,
  a set of compile-time assertions (including @ts-expect-error cases for bad
  props), plus typecheck and check scripts, so npm run typecheck fails if
  the public types regress.
- Bump checkout and setup-node to v7 in both CI and publish workflows.
- Test on Node 22.x, 24.x and 26.x instead of the old 16.x-20.x matrix (Babel 8
  requires Node ^22.18.0 || >=24.11.0), and publish on Node 24 LTS.
- Run every Node version against both React 18 and React 19, since the peer
  range supports both but the devDependencies only install 19.
- CI now runs an explicit build plus tsc typecheck before the jest suite so
  compile output, type checking, and tests are all verified.
Update the README tagline to reflect the widened peer dependency range.
Hypernova's createVM, which createGetComponent uses to evaluate server
bundles, runs them in a sandbox that only exposes Buffer, console, process
and the timer functions. React 19's server renderer calls queueMicrotask
as soon as it loads, and performance.now() while rendering in development,
so with React 19 every bundle evaluated that way failed to load with
"queueMicrotask is not defined", even with plain renderToString.
React 18 does not need either.

Provide both, only where they are missing, from a module imported before
react-dom/server. queueMicrotask rethrows errors from a timer, like the
native one; performance.now() falls back to Date.now(), which React only
uses for render timing.

A test compiles src/ and runs it through Hypernova's real createVM.
@samiashi
samiashi force-pushed the chore/upgrade-dependencies branch from 42f657d to 45547e2 Compare October 15, 2026 07:37
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