npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sigx/lynx-plugin

v0.28.0

Published

Rspack/Rspeedy plugin for SignalX Lynx dual-thread rendering

Readme

@sigx/lynx-plugin

Rspack/Rspeedy plugin for SignalX on Lynx. Splits a single user app into the two bundles Lynx requires (background JS + main-thread Lepus) and runs the SWC worklet transform that powers 'main thread'-marked event handlers.

📚 Documentation

Full guides, API reference and live examples → https://sigx.dev/lynx/modules/plugin/overview/

Installation

npm install -D @sigx/lynx-plugin
// rspeedy.config.ts (or rspack.config.ts)
import { defineConfig } from '@lynx-js/rspeedy';
import { pluginSigxLynx } from '@sigx/lynx-plugin';

export default defineConfig({
  plugins: [pluginSigxLynx()],
});

What it does

  1. Two-bundle split. Lynx ships JS to two contexts on the device:

    • The background bundle — your sigx components, signals, effects, fetch logic.
    • The main-thread bundle — only the worklet handlers extracted from your source, plus the runtime bootstrap.

    The plugin sets up a separate webpack rule so user source files are processed twice: once with the BG-target transform, once with the LEPUS-target transform.

  2. Worklet transform. Files containing the string 'main thread' are run through @lynx-js/react/transform. The transform:

    • Replaces a worklet expression in the BG bundle with a {_wkltId, _c} placeholder so the BG renderer can reference it without shipping the function body.
    • Emits registerWorkletInternal("main-thread", "<id>", function(...) { ... }) calls into the MT bundle so Lynx native can invoke the worklet body when it dispatches a touch event.
    • Folds the thread defines __MAIN_THREAD__ / __BACKGROUND__ to literals per layer (BG: false/true, MT: the inverse) with dead-branch elimination, so if (__MAIN_THREAD__) { … } inside a worklet body ships only in the registered MT form and if (__BACKGROUND__) { … } only in the BG bundle. Files containing either token are transformed even without a worklet directive. App/workspace-src only — published dists pass through the MT layer verbatim (cross-layer module identity), so packages must use a runtime check instead. Types come from @sigx/lynx/client.
  3. MT-bundle bootstrap. Every file in the MT bundle gets three side-effect imports prepended:

    • @sigx/lynx-runtime-main/entry-main — installs the processData / renderPage / sigxPatchUpdate globals Lynx expects.
    • @lynx-js/react/runtime/worklet-runtime/main.js — populates lynxWorkletImpl, registerWorkletInternal, runWorklet.
    • @sigx/lynx-runtime-main/install-hybrid-worklet — registers the hybrid dispatcher used by the bindtap + main-thread-bindtap slot machine.

    Listing them as separate entries in webpack isn't sufficient because the chunk graph can evaluate user code before the bootstrap chain. Prepending side-effect imports per-file forces the dep-graph order.

  4. Async-chunk plumbing (#599). Dynamic import() emits async chunks (dist/static/js/async/<hash>.js). The plugin pins the production output.assetPrefix to / (only when you haven't set one) so chunk request URLs are root-relative and map 1:1 onto the assets @sigx/lynx-cli's release flows embed into the native app, and it logs every emitted async chunk after a production build. Set your own output.assetPrefix to host chunks remotely instead — the generated app shells fall back to http(s) for non-local chunk URLs.

  5. Zero-config web environment (#699). When a web build is requested (sigx run:web sets SIGX_WEB_ENV=1 in the rspeedy child env), the plugin adds any missing lynx / web keys to environments (creating the block when your lynx.config.ts declares none) — present keys and every other user-declared environment are untouched, and plain sigx dev / sigx build (no env var) are unaffected. On the web environment it also injects __WEB__/__NATIVE__ defines, .web.tsx-style file resolution, and a .web.js extensionAlias so per-package web shims apply through published dists (#697). Opt out with pluginSigxLynx({ web: false }).

  6. Inline CSS variables (#116). The plugin encodes enableCSSInlineVariables: true into the template's page config, so the native engine registers CSS custom properties declared in inline style (style={{ '--x': '…' }}) and descendants resolve var(--x) from the very first paint; value changes re-resolve descendants too. Requires a native host on Lynx ≥ 3.9 (the CLI's templates pin 4.0.1; registration without change-propagation shipped in 3.6). @lynx-js/web-core honors inline custom properties unconditionally. Kill switch: pluginSigxLynx({ enableCSSInlineVariables: false }).

  7. Page-config flags. LynxTemplatePlugin emits a fixed key set, but the native engine decodes more than that, so SigxPageConfigPlugin merges the rest in. Every flag here follows one rule: encode the resolved boolean, never omit the key. An absent key means "whatever the engine or host decides today", which is how a behaviour change arrives without anyone choosing it. A host older than the engine version listed below simply ignores the key, so encoding one is always safe.

    | Flag | Needs | Default | Opt in with | |---|---|---|---| | enableCSSInlineVariables (#116) | Lynx ≥ 3.6, and ≥ 3.9 to re-resolve on change | true | on by default; false is the kill switch | | enableNewSticky (#950) | Lynx ≥ 4.0 | false | pluginSigxLynx({ enableNewSticky: true }) | | enableElementApiNewRegistration (#957) | Lynx ≥ 4.0 | false | pluginSigxLynx({ enableElementApiNewRegistration: true }) | | enableCSSRule (#951) | tasm ≥ 0.0.41 to encode; Lynx ≥ 4.0 to evaluate | true | on by default; false is the kill switch |

    enableNewSticky opts into Lynx 4.0's new sticky layout for <list> headers; it changes where and when a header pins, so re-check anything doing pixel math against sticky positions.

    enableElementApiNewRegistration selects Lynx 4.0's new raw binding path for the Fiber Element API. It is pinned off because every <sigx-*> custom element is created through the generic __CreateElement and so moves as one surface — and because upstream marks this flag readSettings: true, meaning a host setting can otherwise flip it under a bundle that never asked. The page config takes precedence, so encoding it keeps the choice with the app.

    enableCSSRule routes stylesheet encoding through the tasm CSSRuleParser — the only encoder path that carries @media, @supports (ConditionRule) and @layer rules into the .lynx binary; the legacy token path silently drops them. This flag is read at encode time (unlike the others, which only the runtime decodes), so it needs @lynx-js/template-webpack-plugin ≥ 0.14 (@lynx-js/tasm ≥ 0.0.41) to have any effect; when enabled the encoder also forces enableCSSSelector and enableCSSInvalidation on. On device, prefers-color-scheme resolution additionally needs the host to drive the engine's color scheme — @sigx/lynx-appearance's publisher does (#951). The web target does not carry media/supports rules regardless (upstream WebEncodePlugin drops them).

    Because that last sentence has two ways of coming true — the kill switch and the web target — the resolved answer is also folded into the bundle as the __SIGX_CSS_RULE__ define, alongside __WEB__/__NATIVE__. Library and app code branches on it when a missing at-rule would leave something unstyled rather than merely unrefined: @sigx/lynx-zero-legacy's <ThemeProvider> reads it to choose between letting the CSS engine resolve a built-in theme's palette and declaring that palette inline (#985). The define reflects the encoder only — it can't know the host's engine version.

    Evaluated and deliberately not plumbed: enableNativeInteraction (changes platform-view interaction defaults — belongs with the gesture work, not here), syncXElementRegistry (no evidence it helps; the default is what our custom elements are verified against), enableNativeList (device-tested in #950 and it does not do what it looks like it does — custom-list-name still selects the platform node), and enableSingleSession (a devtool setting, not a page config — @sigx/lynx-dev-client's concern).

  8. Cross-package worklet pickup. The worklet rules run on every JS/TS file in the BG / MT layers, including node_modules and pre-built dist/. Any package shipping 'main thread' directives in its dist (@sigx/lynx-motion, @sigx/lynx-navigation, @sigx/lynx-gestures, future additions) is picked up automatically — no allowlist or opt-in flag. See CONTRIBUTING.md for the loader-branching details.

Worklet author quick reference

Mark an event handler as MT-thread by adding the directive as the first statement:

<view
  main-thread-bindtap={(e) => {
    'main thread';
    elRef.current?.setStyleProperties({ opacity: '0.5' });
  }}
/>

The plugin handles the rest — the handler body lives in the MT bundle, the BG bundle keeps a {_wkltId, _c} placeholder, and Lynx native dispatches the touch event directly to the MT thread.

For higher-level abstractions (drag, tap, swipe, animations), see @sigx/lynx-gestures.

Snapshot templates (default ON)

pluginSigxLynx({ snapshots: false }) // kill switch — keeps the per-element path

Compiles static JSX subtrees to main-thread snapshot templates (#620): the main thread constructs each compiled subtree itself from one snapshot op (instead of ~10 per-element ops + a thread hop), then receives hole-granular patches. Measured ~25–30x cheaper cell construction on release builds.

  • Default ON (since #642; snapshots: false remains as a kill switch for one release). Works in dev and production: under sigx dev, template registrations ride the MT hot-update bridge, an edit's stale templates are purged per file (the id's filename-hash prefix is edit-stable), and op batches that outrun a registration park and replay after the next update.
  • JSX must be statically analyzable. Dynamic parts (attribute expressions, children) become numbered holes; the subtree shape is fixed at compile time. Non-static subtrees keep today's per-element path automatically. Whole files using use:* directive attributes are pre-filtered to the per-element path silently (they panic the upstream WASM pass); only an unexpected transform failure emits a build warning naming the file that fell back. Raw <list> JSX compiles: cells are staged instance records that componentAtIndex materializes synchronously on first pull, and offscreen cells recycle through template-keyed pools (enqueueComponent re-patches a pooled tree instead of constructing).
  • App/workspace-src only. Published dists ship pre-lowered _jsx() calls and keep the per-element path.

Limitations

  • Custom worklet bodies require 'main thread' directives. Worklets aren't auto-detected from JSX shape; the directive is the marker.

  • Variables declared inside a worklet body are MT-locals. They can't cross the bridge via runOnBackground closure capture — pass them as arguments instead. See @sigx/lynx-gestures README, "Performance notes."

  • Mappers for useAnimatedStyle ship as MT-side code. Custom mappers must be registered from a MT-side module via registerMapper(name, fn) — BG-side useAnimatedStyle only carries the name across the build pipeline.

  • Worklets must survive tree-shaking on the main-thread layer. A worklet compiles into two halves: a {_wkltId} placeholder in the background bundle and a registerWorkletInternal("main-thread", "<id>", …) in the main-thread bundle, which runWorklet looks up at dispatch. On the MT layer that registration is the module's only reason to exist, so a package marked sideEffects: false gets its worklet modules dropped and every worklet in it silently never runs — no gesture response, no animation, no error (#1021). The plugin marks all MT-layer modules side-effectful to prevent this, and fails the build if any background worklet id has no matching MT registration. If you hit that error, look for a sideEffects declaration excluding worklet modules in one of your dependencies.

  • Dynamic import() produces plain JS chunks, not Lynx lazy bundles. Each async chunk is emitted to dist/static/js/async/ and loaded at runtime through lynx.requireModuleAsync, which @sigx/lynx-cli embeds into the app and serves from the native resource fetchers (#599/#612). The plugin therefore empties __webpack_require__.lynx_aci, the map that would otherwise route chunks through lynx.loadLazyBundle — a method @lynx-js/react installs as a module side effect, which is undefined in a sigx bundle because sigx substitutes its own runtime (#1015). @lynx-js/template-webpack-plugin still emits an encoded template per dynamic import under dist/lazy-bundle/; those are inert, are not embedded, and can be ignored.

    Peer versions matter here. @lynx-js/template-webpack-plugin and @lynx-js/css-extract-webpack-plugin release in lockstep and must be upgraded together — the encoder's beforeEncode/beforeEmit payloads changed shape at 0.14 (entryNameschunkGroups), and a css-extract too old for the encoder in use breaks sigx dev with args.entryNames is not iterable while leaving release builds green. css-extract ≥ 0.10.1 pairs with encoder 0.15.x.

License

MIT