Using Build Plugin
Register one plugin and your Rspeedy build produces the dual-thread bundles Lynx needs — background JS, main-thread Lepus, and the worklet transform that stitches them together.
Basic usage
@sigx/lynx-plugin is a build-time plugin for Rspack / Rspeedy. Install it as a dev dependency and register pluginSigxLynx() in your Lynx config. That single call wires SignalX into Lynx's dual-thread (Background + Main Thread) build pipeline.
pnpm add -D @sigx/lynx-plugin// lynx.config.ts (also works as rspeedy.config.ts / rspack.config.ts)
import { defineConfig } from '@lynx-js/rspeedy';
import { pluginSigxLynx } from '@sigx/lynx-plugin';
export default defineConfig({
plugins: [pluginSigxLynx()],
});
That's the whole setup for most apps. The plugin splits each entry into two bundles, transpiles to the ES2019 target the Lynx engine expects, configures the SignalX JSX runtime, and runs the main-thread worklet transform.
Note: this is a build tool, not a native module — there are no iOS or Android sources to link. The device-side runtime behavior is provided by sibling @sigx/lynx-* packages. You only need to register the plugin here.
Configuring CSS and debug output
pluginSigxLynx() accepts an options object that maps to the Lynx template's CSS behavior and debug-info placement. All fields are optional and have sensible defaults.
// lynx.config.ts
import { defineConfig } from '@lynx-js/rspeedy';
import { pluginSigxLynx } from '@sigx/lynx-plugin';
export default defineConfig({
plugins: [
pluginSigxLynx({
// CSS selectors in the Lynx template (default: true)
enableCSSSelector: true,
// CSS inheritance in the Lynx engine (default: false)
enableCSSInheritance: true,
// extra properties to inherit — only used when enableCSSInheritance is true
customCSSInheritanceList: ['font-size', 'color'],
// keep debug info outside the template bundle (default: true)
debugInfoOutside: true,
}),
],
});
customCSSInheritanceList only takes effect when enableCSSInheritance is true; otherwise it is ignored. See the API reference for the full option table.
Authoring main-thread worklets
The plugin moves marked event handlers onto the main thread so native touch events dispatch without a round trip to the background bundle. To opt a handler in, make 'main thread' the first statement of the function body. There is no auto-detection from JSX — the directive is required.
<view
main-thread-bindtap={(e) => {
'main thread';
elRef.current?.setStyleProperties({ opacity: '0.5' });
}}
/>
At build time the plugin runs that body through the main-thread transform: the background bundle keeps a compact placeholder, the main-thread bundle gets the real handler, and Lynx native dispatches the touch event straight to the main thread.
Worklet gotchas to keep in mind:
- The
'main thread'directive must be the first statement in the function — JSX alone does not mark a worklet. - Variables declared inside a worklet body are main-thread locals; they cannot cross to the background thread through
runOnBackgroundclosure capture. Pass them as arguments instead. useAnimatedStylecustom mappers ship as main-thread code and must be registered on the main thread viaregisterMapper(name, fn)— the background bundle only carries the name.
Cross-package worklets are picked up automatically: the transform runs over all JS/TS in the background and main-thread layers, including node_modules and prebuilt dist, so any package that ships 'main thread' directives (for example @sigx/lynx-motion, @sigx/lynx-navigation, @sigx/lynx-gestures) works with no allowlist.
How dynamic import() chunks load
Code splitting on Lynx works differently from the web and from upstream Lynx, and getting the model wrong produces a blank app rather than an error, so it is worth knowing how the pieces fit:
- A dynamic
import()emits a plain JS chunk todist/static/js/async/. At runtime it is loaded throughlynx.requireModuleAsync. sigx devserves those chunks from the dev server. The@sigx/lynx-clirelease flows —sigx run:* --releaseandsigx prebuild --embed-bundle— embed them into the app, and the generated native resource fetchers serve them locally, so they resolve offline.- OTA updates don't carry async chunks.
sigx updates:publishrefuses to publish whiledist/static/js/async/is non-empty, unless you pass--allow-async-chunksfor a setup that hosts the chunks remotely. See Code splitting. - Setting your own
output.assetPrefixhosts the chunks remotely instead. The generated app shells fall back to http(s) for chunk URLs that aren't local. - sigx does not use Lynx lazy bundles (
lynx.loadLazyBundle).@lynx-js/template-webpack-pluginstill writes an encoded template per dynamic import underdist/lazy-bundle/; those files are inert, are not embedded, and can be ignored.
Reading the app env in lynx.config.ts
The typed env from signalx.config.ts (see App environment) is baked into the bundle by this plugin. To use the same values in the rspeedy config, such as a dev-server proxy target, call appEnv():
// lynx.config.ts
import { defineConfig } from '@lynx-js/rspeedy';
import { appEnv, pluginSigxLynx } from '@sigx/lynx-plugin';
const { apiBaseUrl } = appEnv<{ apiBaseUrl: string }>();
export default defineConfig({
plugins: [pluginSigxLynx()],
server: { proxy: { '/api': apiBaseUrl } },
});
@sigx/lynx-cli passes the resolved env to the build, so run it through sigx build / sigx dev. Under bare rspeedy, appEnv() and the app's env are both {}.
Keep the @lynx-js/* build plugins in step
@lynx-js/template-webpack-plugin (the encoder) and @lynx-js/css-extract-webpack-plugin release in lockstep and must be upgraded together. The encoder's beforeEncode / beforeEmit hook payloads replaced entryNames with chunkGroups at 0.14, so a css-extract too old for the encoder in use fails with args.entryNames is not iterable. It fails only in sigx dev — the offending hook sits behind an HMR check, so release builds stay green and hide the mismatch.
@sigx/lynx-plugin peers the pair it is tested with: the encoder at exactly 0.15.0 and css-extract at ^0.10.1. If you pin these yourself in an app, move them together and keep the same shape — an open >= floor lets an install resolve a combination that was never tested.
Troubleshooting
TypeError: m.loadLazyBundle is not a functionat bundle load, and the app stays blank. A dynamic import was routed onto the Lynx lazy-bundle path, which a sigx bundle doesn't implement.@sigx/lynx-pluginkeeps async chunks on therequireModuleAsyncpath; this means your build isn't using a@sigx/lynx-pluginthat does — update it together with the rest of the@sigx/lynx*family.args.entryNames is not iterablewhensigx devstarts. Your@lynx-js/css-extract-webpack-pluginis too old for the installed@lynx-js/template-webpack-plugin. Align the two to the versions@sigx/lynx-pluginpeers (see above) and reinstall.
Dev server, ports, and devices
In development (NODE_ENV !== 'production') the plugin adds device-friendly behavior automatically — you do not need to call any extra API:
- It boots a small WebSocket server on
devServerPort + 1to receive batched device console logs and print them to your terminal. On Android the Lynx background runtime has nofetch/XHR, so WebSocket is the only log transport. - It rewrites wildcard hosts (
0.0.0.0,::) in the dev server URLs to your real LAN IPv4 so phones and emulators can reach the dev server. Virtual adapters (Hyper-V, WSL, Docker, VPN) are skipped during detection; it falls back to the first external IPv4, then127.0.0.1. - It narrows the file watcher's ignore list and supports forced polling, a workaround for Rspack 1.7.x file-watch noise.
Environment variables the plugin honors:
# Override the dev server port (set automatically by @sigx/lynx-cli)
SIGX_LYNX_DEV_PORT=3000
# Force file-watch polling (useful on network drives / some VMs)
SIGX_LYNX_WATCH_POLL=1
SIGX_LYNX_DEV_PORT exists because the Rspeedy CLI has no --port flag; @sigx/lynx-cli sets it for you. You normally won't set these by hand.
Notes
pluginSigxLynx()is the only API most projects need.LAYERSandapplyEntryare low-level escapes for custom build setups — see the API reference.- The returned plugin is named
lynx:sigxand declarespre: ['lynx:rsbuild:plugin-api', 'lynx:config', 'lynx:rsbuild:dev'], so it runs after Rspeedy's own config and dev plugins. That ordering is required for the dev-server URL fixes to apply. - The peer Lynx plugins (
@lynx-js/*template / css-extract / runtime-wrapper / dev-transport) are declared as optional peers. Install the ones your build needs — and keep the encoder and css-extract in step.
See also
- Overview — what the plugin does and why.
- Installation — project setup.
- API reference — every export, typed.
