Add @amritk/mini-lynx-rsbuild-plugin and a device starter app - #40
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 (
c2516a9→ce247f2)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, andsrc/core-size-budget.test.tsholds the absolute ceiling.