The rspeedy dev server
rspeedy dev prints three URLs, a QR code, and four keyboard shortcuts, and most people use one URL and ignore everything else. The parts that get skipped are the ones that make the device loop quick, so they are worth ten minutes.
Rspeedy v0.14.5 (Rsbuild v1.7.5, Rspack v1.7.12)
➜ Web http://192.168.1.24:3000/main.web.bundle
➜ ∟ Preview http://192.168.1.24:3000/__web_preview?casename=main.web.bundle
➜ Lynx http://192.168.1.24:3000/main.lynx.bundle
● Scan with Lynx
◆ http://192.168.1.24:3000/main.lynx.bundle?fullscreen=true
◆ Usage
│ r Switch entries
│ a Switch schema
│ h Help
│ q Quit
The three URLs
Web is the raw web bundle, so opening it directly hands you compiled output rather than a running app. I use it to confirm a build produced something.
Preview is the shell you actually want in a browser. The __web_preview route wraps the web bundle in a page that boots the Lynx web runtime, and casename names the bundle to load. It is the fastest loop for layout and styling work and the least faithful one, because gestures arrive as mouse events and accessibility-* properties do nothing.
Lynx is the native bundle a device loads, fetched both by a QR-code scan and by a custom host app.
Both bundles only exist if you declare both environments:
export default defineConfig({
environments: {
web: {},
lynx: {},
},
})
Dropping web removes the preview shell, and dropping lynx leaves the QR code pointing at a bundle that was never emitted.
Why the QR URL has ?fullscreen=true
Lynx Go renders your app inside its own chrome by default, and the navigation bar above your content changes the available height and hides the top safe area, so anything you check about status-bar overlap or env(safe-area-inset-top) gives the wrong answer.
Appending fullscreen=true drops the chrome and hands your app the whole screen, which is what a real build gets. The schema option is where it goes:
pluginQRCode({
schema(url) {
return `${url}?fullscreen=true`
},
})
Returning a plain string replaces the QR code's URL outright, and the default without a schema option is (url) => ({ http: url }), which gives the bare bundle URL and the chrome-wrapped rendering.
Cycling schemas with a
schema also accepts Record<string, string>, and the a key cycles through whatever you put in it, so one rspeedy dev session serves several ways of opening the same bundle without a restart.
import { pluginQRCode } from '@lynx-js/qrcode-rsbuild-plugin'
pluginQRCode({
schema(url) {
return {
fullscreen: `${url}?fullscreen=true`,
chrome: url,
host: `vyhost://lynx?url=${encodeURIComponent(url)}`,
}
},
})
Pressing a rotates between the three and prints the label above the code, so you know which one is on screen. The pattern earns its keep once you have more than one way in, with fullscreen for layout checks, chrome for Lynx Go's reload button, and a custom scheme for your own host build.
That custom scheme is how a host app joins the same workflow. Register it under CFBundleURLTypes in the host's Info.plist, read the url query parameter on launch, and load it as the bundle URL, so scanning opens your host instead of Lynx Go and saves a copy-paste every time your LAN address changes.
Cycling entries with r
Multiple entries in source.entry become multiple bundles, and r cycles the QR code between them:
export default defineConfig({
source: {
entry: {
main: './src/index.ts',
onboarding: './src/onboarding.ts',
},
},
})
Both build on every save, and r re-renders the code for the other one. Splitting a heavy flow into its own entry lets you iterate without booting through the rest of the app, at the cost of two bundles rebuilding on every keystroke.
h reprints the usage block, and q shuts the server down.
Picking a surface
The more faithful the surface, the slower the loop, so work tends to move down this list as it gets closer to done.
| Surface | Good for | Blind to |
|---|---|---|
__web_preview in a browser | Layout, spacing, colors, copy, composition | Touch gestures, native a11y, native animation timing, safe areas |
| Lynx Go over QR | Gesture feel, scroll physics, real fonts and safe areas, native rendering | Anything needing a native module or Info.plist configuration |
| Custom host in Xcode | Native modules, permissions, custom fonts, release behavior | Nothing, at the cost of a native build |
Vy UI's own component previews run the web path, which is why the docs describe native behavior in prose beside them.
When the QR code does not work
The phone cannot reach the URL. The address is whichever LAN interface rspeedy picked, so a phone on guest Wi-Fi or another VLAN fails, and the phone's browser confirms that in seconds.
The port moved. rspeedy increments past an occupied port, so a second project shifts to 3001 and the code you scanned five minutes ago points at the wrong server.
The app opens with visible chrome. The active schema is the plain one, so press a.
Nothing rebuilds on save. Files outside the configured entry graph are not watched, which bites when you edit a file nothing imports yet.
Related
Testing with Xcode
Build a minimal iOS host app that embeds the Lynx runtime, loads your bundle from the dev server, and compiles in the native modules Lynx Go cannot provide.
Tailwind on Lynx
Why Lynx apps are pinned to Tailwind v3, what the Lynx preset actually does to your utility set, and how to run v3 and v4 in the same repository.