Skip to content

Add @amritk/mini-lynx-rsbuild-plugin and a device starter app - #40

Merged
amritk merged 1 commit into
mainfrom
claude/rspeedy-explorer-integration-6md5jk
Aug 25, 2026
Merged

Add @amritk/mini-lynx-rsbuild-plugin and a device starter app#40
amritk merged 1 commit into
mainfrom
claude/rspeedy-explorer-integration-6md5jk

Conversation

@amritk

@amritk amritk commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Getting a mini-lynx app onto a phone took an undocumented pile of rspack wiring
that every consumer would have written once each, wrongly. This is that wiring,
published, plus the four-file app it builds and a record of what about the loop
is actually verified.

A Lynx template is not a bundle with an entry point. It is a container with two
code slots — a main-thread chunk the engine executes to build the first screen
and a background chunk it loads as /app-service.js — plus CSS the encoder
compiles. rspeedy builds the container, but which code goes in which slot is the
framework's to say, and every framework says it in a plugin of its own:
ReactLynx's is @lynx-js/react-rsbuild-plugin, and there is no framework-agnostic
one beneath it — @lynx-js/rsbuild-plugin, which sounds like one, is the dev
server and the per-thread minifier split.

pluginMiniLynx() splits one source.entry into a main-thread entry and a
background entry, adds LynxTemplatePlugin and LynxEncodePlugin, sets the
lynx:main-thread asset flag the encoder splits on, wraps the background chunk
for the engine's module loader, and points the JSX transform at
@amritk/mini-lynx. The asset flag is the sharp edge: nothing sets it for a
runtime that is not ReactLynx and nothing complains when it is missing, so
without it the build succeeds, the template encodes, and the device shows a
blank screen.

In dev the background chunk also gets the dev-server client and
@rspack/core/hot/dev-server. Nothing accepts a hot update and nothing should — a
component runs once here, so there is no live module graph for a diff to be
applied to — and an update with no acceptor falls through to a page reload
through the devtool, which is correct precisely because renderPage claims
removeComponents.

Two things about the package are unlike the rest of the repo. Its module load
must stay side-effect free, because LynxEncodePlugin opens a worker pool on
import and dist-smoke imports every published module under Node while
consumer-e2e imports the entry from an install carrying no @lynx-js/* at all;
both heavy imports are dynamic. And its test runs the artifact rather than
asserting on a config: two real rspeedy builds, with and without a background
module, then the built main-thread chunk executed in a node:vm context whose
globals are createFakeEngine's Element PAPI, asserting on the tree it renders,
on firstScreen, and on a tap mutating the existing element rather than
replacing it.

apps/starter-mini-lynx is the consumer: counter, globalProps, and the native
bridge's round-trip on screen, because a missing background chunk is otherwise
invisible. Its own suite drives the entry the way the engine does, which doubles
as the shortest demonstration that an app on this runtime needs no device to be
tested.

docs/mini-lynx-explorer.md documents the loop and, more usefully, splits it into
what is checked in CI, what was checked by hand against the real dev server
(server starts, the LAN URL serves a real encoded template, an edit rebuilds and
the next fetch carries it), and what no device was available to answer: that
Explorer renders this template, that the reload fires on save, that events reach
handlers on hardware, and layout. The phone half is unproven and says so.

📦 Bundle-size delta vs main (c2516a9ce247f2)

Package Entry main PR Δ
mini core (.) 3,156 B 3,156 B +0 B (+0.0%) ⚪
mini flow 4,138 B 4,138 B +0 B (+0.0%) ⚪
mini router 4,328 B 4,328 B +0 B (+0.0%) ⚪
mini forms 4,333 B 4,333 B +0 B (+0.0%) ⚪
mini query 1,988 B 1,988 B +0 B (+0.0%) ⚪
mini-lynx core (.) 5,769 B 5,769 B +0 B (+0.0%) ⚪
mini-lynx flow 5,138 B 5,138 B +0 B (+0.0%) ⚪
mini-lynx router 6,276 B 6,276 B +0 B (+0.0%) ⚪
mini-lynx forms 5,572 B 5,572 B +0 B (+0.0%) ⚪
mini-lynx query 1,990 B 1,990 B +0 B (+0.0%) ⚪
mini-lynx testing 2,464 B 2,464 B +0 B (+0.0%) ⚪

Gzipped bytes of each bundled entry (esbuild, minified, browser/es2022, optional peer deps external). Bundling is deterministic, so unlike a timed benchmark these numbers carry no noise and every Δ is exact. ⚪ within ±0.5% · 🟢 smaller · 🔴 larger. Each package's core (.) must stay flat as subpath features land — that is the whole charter, and src/core-size-budget.test.ts holds the absolute ceiling.

Getting a mini-lynx app onto a phone took an undocumented pile of rspack wiring
that every consumer would have written once each, wrongly. This is that wiring,
published, plus the four-file app it builds and a record of what about the loop
is actually verified.

A Lynx template is not a bundle with an entry point. It is a container with two
code slots — a main-thread chunk the engine executes to build the first screen
and a background chunk it loads as /app-service.js — plus CSS the encoder
compiles. rspeedy builds the container, but which code goes in which slot is the
framework's to say, and every framework says it in a plugin of its own:
ReactLynx's is @lynx-js/react-rsbuild-plugin, and there is no framework-agnostic
one beneath it — @lynx-js/rsbuild-plugin, which sounds like one, is the dev
server and the per-thread minifier split.

pluginMiniLynx() splits one source.entry into a main-thread entry and a
background entry, adds LynxTemplatePlugin and LynxEncodePlugin, sets the
lynx:main-thread asset flag the encoder splits on, wraps the background chunk
for the engine's module loader, and points the JSX transform at
@amritk/mini-lynx. The asset flag is the sharp edge: nothing sets it for a
runtime that is not ReactLynx and nothing complains when it is missing, so
without it the build succeeds, the template encodes, and the device shows a
blank screen.

In dev the background chunk also gets the dev-server client and
@rspack/core/hot/dev-server. Nothing accepts a hot update and nothing should — a
component runs once here, so there is no live module graph for a diff to be
applied to — and an update with no acceptor falls through to a page reload
through the devtool, which is correct precisely because renderPage claims
removeComponents.

Two things about the package are unlike the rest of the repo. Its module load
must stay side-effect free, because LynxEncodePlugin opens a worker pool on
import and dist-smoke imports every published module under Node while
consumer-e2e imports the entry from an install carrying no @lynx-js/* at all;
both heavy imports are dynamic. And its test runs the artifact rather than
asserting on a config: two real rspeedy builds, with and without a background
module, then the built main-thread chunk executed in a node:vm context whose
globals are createFakeEngine's Element PAPI, asserting on the tree it renders,
on firstScreen, and on a tap mutating the existing element rather than
replacing it.

apps/starter-mini-lynx is the consumer: counter, globalProps, and the native
bridge's round-trip on screen, because a missing background chunk is otherwise
invisible. Its own suite drives the entry the way the engine does, which doubles
as the shortest demonstration that an app on this runtime needs no device to be
tested.

docs/mini-lynx-explorer.md documents the loop and, more usefully, splits it into
what is checked in CI, what was checked by hand against the real dev server
(server starts, the LAN URL serves a real encoded template, an edit rebuilds and
the next fetch carries it), and what no device was available to answer: that
Explorer renders this template, that the reload fires on save, that events reach
handlers on hardware, and layout. The phone half is unproven and says so.
@amritk
amritk merged commit c03d563 into main Aug 25, 2026
3 checks passed
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.

2 participants