@sigx/lynx-runtime 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/animated/animated-value.d.ts +2 -2
  2. package/dist/animated/animated-value.js +15 -0
  3. package/dist/animated/shared-value.d.ts +1 -1
  4. package/dist/animated/shared-value.js +94 -0
  5. package/dist/animated/use-animated-style.d.ts +3 -3
  6. package/dist/animated/use-animated-style.js +53 -0
  7. package/dist/animated-bridge.js +71 -0
  8. package/dist/bg-bridge.js +63 -0
  9. package/dist/event-registry.js +75 -0
  10. package/dist/flush.d.ts +1 -1
  11. package/dist/flush.js +8 -0
  12. package/dist/hmr.js +119 -39
  13. package/dist/index.d.ts +24 -22
  14. package/dist/index.js +37 -849
  15. package/dist/jsx.d.ts +29 -3
  16. package/dist/jsx.js +19 -0
  17. package/dist/main-thread-ref.js +134 -0
  18. package/dist/model-processor.js +76 -0
  19. package/dist/mt-hmr-bridge.js +125 -53
  20. package/dist/native/gesture-detector.d.ts +1 -1
  21. package/dist/native/gesture-detector.js +340 -0
  22. package/dist/native/index.d.ts +2 -2
  23. package/dist/native/index.js +1 -0
  24. package/dist/nodeOps.d.ts +1 -1
  25. package/dist/nodeOps.js +319 -0
  26. package/dist/op-queue.js +213 -0
  27. package/dist/render.d.ts +1 -1
  28. package/dist/render.js +125 -0
  29. package/dist/run-on-background.d.ts +1 -1
  30. package/dist/run-on-background.js +201 -0
  31. package/dist/shadow-element.js +91 -0
  32. package/dist/threading.d.ts +1 -1
  33. package/dist/threading.js +124 -0
  34. package/dist/types.d.ts +1 -1
  35. package/dist/types.js +10 -0
  36. package/dist/use-element-layout.d.ts +72 -0
  37. package/dist/use-element-layout.js +40 -0
  38. package/package.json +10 -8
  39. package/dist/hmr.js.map +0 -1
  40. package/dist/index.js.map +0 -1
  41. package/dist/mt-hmr-bridge.js.map +0 -1
package/dist/jsx.d.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  * the imports — see lynx-runtime README for details.
18
18
  */
19
19
  import type { Model } from '@sigx/runtime-core';
20
- import type { MainThreadRef } from './main-thread-ref';
20
+ import type { MainThreadRef } from './main-thread-ref.js';
21
21
  export type LynxEventHandler<E = any> = (event: E) => void;
22
22
  export declare namespace MainThread {
23
23
  /** Element handle available in main-thread event handlers via MainThreadRef.current. */
@@ -97,12 +97,21 @@ export interface LynxCommonAttributes {
97
97
  catchtouchend?: LynxEventHandler;
98
98
  bindtouchcancel?: LynxEventHandler;
99
99
  catchtouchcancel?: LynxEventHandler;
100
+ /**
101
+ * Fires when this element's measured layout (frame, padding, border)
102
+ * changes. Lynx 3.7+. Works on any element; for `<text>` specifically
103
+ * see also the text-only `bindlayout` event which carries extra
104
+ * baseline/line metrics.
105
+ */
106
+ bindlayoutchange?: LynxEventHandler;
107
+ catchlayoutchange?: LynxEventHandler;
100
108
  onTap?: LynxEventHandler;
101
109
  onLongpress?: LynxEventHandler;
102
110
  onTouchstart?: LynxEventHandler;
103
111
  onTouchmove?: LynxEventHandler;
104
112
  onTouchend?: LynxEventHandler;
105
113
  onTouchcancel?: LynxEventHandler;
114
+ onLayoutchange?: LynxEventHandler;
106
115
  /** Bind a MainThreadRef to this element for synchronous MT access. */
107
116
  'main-thread:ref'?: MainThreadRef<MainThread.Element | null>;
108
117
  'main-thread-bindtap'?: LynxEventHandler;
@@ -129,8 +138,25 @@ export interface TextAttributes extends LynxCommonAttributes {
129
138
  'number-of-lines'?: number;
130
139
  /** Text overflow mode */
131
140
  'text-overflow'?: 'clip' | 'ellipsis';
132
- /** Selectable text */
133
- selectable?: boolean;
141
+ /**
142
+ * Enable native text selection (long-press to select, system context
143
+ * menu for copy/share). Lynx 3.7+.
144
+ */
145
+ 'text-selection'?: boolean;
146
+ /**
147
+ * Suppress the system context menu after selection so the app can
148
+ * render its own. Only takes effect when `text-selection` is enabled.
149
+ * Lynx 3.7+.
150
+ */
151
+ 'custom-text-selection'?: boolean;
152
+ /** Fires when the selection range changes (selection start/end). */
153
+ bindselectionchange?: LynxEventHandler;
154
+ /** Fires when text layout is computed (frame/baseline/line metrics). */
155
+ bindlayout?: LynxEventHandler;
156
+ /** Convenience alias for `bindselectionchange`. */
157
+ onSelectionchange?: LynxEventHandler;
158
+ /** Convenience alias for `bindlayout`. */
159
+ onLayout?: LynxEventHandler;
134
160
  }
135
161
  export interface ImageAttributes extends LynxCommonAttributes {
136
162
  /** Image source URI */
package/dist/jsx.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Lynx JSX intrinsic element type definitions for SignalX.
3
+ *
4
+ * Importing this file (which happens automatically when you import
5
+ * `@sigx/lynx-runtime`) globally augments `JSX.IntrinsicElements` so
6
+ * that <view>, <text>, <image>, <scroll-view>, <list>, <list-item>,
7
+ * <input>, <textarea>, <page>, <svg>, <filter-image> are recognised
8
+ * with their proper attribute types.
9
+ *
10
+ * Same pattern as packages/runtime-dom/src/jsx.tsx (DOM elements) and
11
+ * packages/runtime-terminal/src/index.ts (<box>/<text>/<br>).
12
+ *
13
+ * Hybrid web+lynx codebases that import both @sigx/runtime-dom and
14
+ * @sigx/lynx-runtime will hit a TypeScript merge error on <input>
15
+ * because runtime-dom declares it via InputHTMLAttributes and we
16
+ * declare it via InputAttributes. Pick one platform per app or alias
17
+ * the imports — see lynx-runtime README for details.
18
+ */
19
+ export {};
@@ -0,0 +1,134 @@
1
+ /**
2
+ * MainThreadRef — a ref whose `.current` value lives on the Main Thread.
3
+ *
4
+ * On the Background Thread, `.current` is always the initial value (typically
5
+ * `null`). On the Main Thread, `.current` is set to the real Lynx element
6
+ * handle when the ref is bound via `main-thread:ref={ref}`.
7
+ *
8
+ * This is the sigx equivalent of react-lynx's `useMainThreadRef()` and
9
+ * vue-lynx's `useMainThreadRef()`. It enables zero-latency style updates
10
+ * and animations by giving main-thread event handlers synchronous access
11
+ * to native elements.
12
+ *
13
+ * Architecture:
14
+ * BG: useMainThreadRef(init) → MainThreadRef { wvid, current: init }
15
+ * → pushOp(INIT_MT_REF, wvid, init)
16
+ * BG: patchProp('main-thread:ref', ref) → pushOp(SET_MT_REF, elId, wvid)
17
+ * MT: INIT_MT_REF → workletRefs.set(wvid, { current: init })
18
+ * MT: SET_MT_REF → workletRefs.get(wvid).current = elements.get(elId)
19
+ */
20
+ import { onUnmounted } from '@sigx/runtime-core';
21
+ import { OP, pushOp, scheduleFlush } from './op-queue.js';
22
+ // ---------------------------------------------------------------------------
23
+ // Worklet variable ID generator
24
+ // ---------------------------------------------------------------------------
25
+ let nextWvid = 1;
26
+ export function resetWvidCounter() {
27
+ nextWvid = 1;
28
+ }
29
+ // ---------------------------------------------------------------------------
30
+ // MainThreadRef class
31
+ // ---------------------------------------------------------------------------
32
+ /**
33
+ * A ref whose `.current` property is managed on the Main Thread.
34
+ *
35
+ * On the BG thread, `.current` returns the `initValue` and is read-only
36
+ * (setting it has no effect — the real value lives on MT).
37
+ *
38
+ * In main-thread event handlers and `runOnMainThread` callbacks, `.current`
39
+ * is the real Lynx element handle with methods like `setStyleProperties()`,
40
+ * `getComputedStyleProperty()`, and `animate()`.
41
+ */
42
+ export class MainThreadRef {
43
+ /**
44
+ * Worklet variable ID — uniquely identifies this ref across threads.
45
+ * Underscored to match the field name `transformWorklet` walks for
46
+ * in @lynx-js/react/worklet-runtime when expanding `_c` captures.
47
+ */
48
+ _wvid;
49
+ /**
50
+ * Initial value snapshot — sent to MT in INIT_MT_REF and used by
51
+ * the worklet-runtime to seed the firstScreen ref map.
52
+ */
53
+ _initValue;
54
+ /**
55
+ * On BG: the init value (read-only snapshot).
56
+ * On MT: the real element handle (set by SET_MT_REF op).
57
+ */
58
+ current;
59
+ constructor(initValue) {
60
+ this._wvid = nextWvid++;
61
+ this._initValue = initValue;
62
+ this.current = initValue;
63
+ }
64
+ }
65
+ /**
66
+ * Walk a captured `_c` map and serialize MainThreadRef instances to plain
67
+ * `{ _wvid, _initValue }` objects so they survive the JSON round-trip across
68
+ * the BG→MT bridge. Upstream's worklet-runtime walks `_c` looking for `_wvid`
69
+ * to recognize ref captures and resolve them via
70
+ * `lynxWorkletImpl._refImpl._workletRefMap`.
71
+ *
72
+ * Used by both the SET_WORKLET_EVENT path (`nodeOps.patchProp`) and the
73
+ * SET_GESTURE_DETECTOR path (`native/gesture-detector.ts`).
74
+ */
75
+ export function sanitizeCaptured(captured) {
76
+ const out = {};
77
+ for (const k in captured) {
78
+ const v = captured[k];
79
+ if (v instanceof MainThreadRef) {
80
+ out[k] = { _wvid: v._wvid, _initValue: v._initValue };
81
+ }
82
+ else {
83
+ out[k] = v;
84
+ }
85
+ }
86
+ return out;
87
+ }
88
+ // ---------------------------------------------------------------------------
89
+ // useMainThreadRef composable
90
+ // ---------------------------------------------------------------------------
91
+ /**
92
+ * Create a ref that provides synchronous access to a native element on the
93
+ * Main Thread. Bind it to an element via `main-thread:ref={ref}`.
94
+ *
95
+ * @example
96
+ * ```tsx
97
+ * const elRef = useMainThreadRef<MainThread.Element>(null);
98
+ *
99
+ * function handleScroll(e: ScrollEvent) {
100
+ * 'main thread';
101
+ * const offset = e.detail.scrollTop;
102
+ * elRef.current?.setStyleProperties({
103
+ * transform: `translateY(${-offset}px)`,
104
+ * });
105
+ * }
106
+ *
107
+ * return (
108
+ * <scroll-view
109
+ * main-thread-bindscroll={handleScroll}
110
+ * >
111
+ * <view main-thread:ref={elRef}>
112
+ * <text>Sticky header</text>
113
+ * </view>
114
+ * </scroll-view>
115
+ * );
116
+ * ```
117
+ */
118
+ export function useMainThreadRef(initValue) {
119
+ const ref = new MainThreadRef(initValue);
120
+ // Tell the MT to create a worklet ref holder with this ID and initial value.
121
+ pushOp(OP.INIT_MT_REF, ref._wvid, initValue);
122
+ scheduleFlush();
123
+ // Release the holder when the owning component unmounts. Without this, the
124
+ // MT-side `_workletRefMap` grows monotonically across mount/unmount cycles
125
+ // (router-driven apps with frequent navigation hit this fastest).
126
+ // `onUnmounted` no-ops if called outside a component setup; callers that
127
+ // construct refs ad-hoc (e.g. tests) just won't get a release op, which is
128
+ // the same as today's behaviour.
129
+ onUnmounted(() => {
130
+ pushOp(OP.RELEASE_MT_REF, ref._wvid);
131
+ scheduleFlush();
132
+ });
133
+ return ref;
134
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Platform-specific model processor for Lynx form elements.
3
+ *
4
+ * Wires sigx's `model={() => state.field}` two-way binding directive to
5
+ * Lynx's <input> and <textarea> elements. The processor runs at JSX
6
+ * creation time (called from runtime-core/jsx-runtime.ts when an
7
+ * intrinsic element has a `model` prop) and rewrites the props to:
8
+ *
9
+ * 1. Set the initial `value` from the bound state
10
+ * 2. Install a `bindinput` handler that pushes the new value back into state
11
+ *
12
+ * Lynx differs from DOM in two important ways:
13
+ *
14
+ * - Lynx <input> fires `bindinput` with `event.detail.value` containing
15
+ * the new text. There is no DOM-style `target.value` property — the
16
+ * value lives on the event detail object.
17
+ * - Lynx Go (and most current Lynx hosts) have no native checkbox or
18
+ * radio elements. We only handle text-style <input> and <textarea>
19
+ * here. Adding checkbox/radio later is straightforward — mirror the
20
+ * branches in packages/runtime-dom/src/model-processor.ts.
21
+ *
22
+ * Mirrors packages/runtime-dom/src/model-processor.ts.
23
+ */
24
+ import { setPlatformModelProcessor } from '@sigx/runtime-core/internals';
25
+ setPlatformModelProcessor((type, props, [stateObj, key], _originalProps) => {
26
+ // Helper to set value — uses onUpdate handler if available (for props
27
+ // model forwarding through component boundaries).
28
+ const setValue = (v) => {
29
+ const updateHandler = stateObj[`onUpdate:${key}`];
30
+ if (typeof updateHandler === 'function') {
31
+ updateHandler(v);
32
+ }
33
+ else {
34
+ stateObj[key] = v;
35
+ }
36
+ };
37
+ // Text <input>
38
+ if (type === 'input') {
39
+ props.value = stateObj[key] ?? '';
40
+ const existingBindInput = props.bindinput;
41
+ props.bindinput = (e) => {
42
+ const v = e?.detail?.value ?? '';
43
+ setValue(v);
44
+ if (existingBindInput)
45
+ existingBindInput(e);
46
+ };
47
+ const existingHandler = props['onUpdate:modelValue'];
48
+ props['onUpdate:modelValue'] = (v) => {
49
+ setValue(v);
50
+ if (existingHandler)
51
+ existingHandler(v);
52
+ };
53
+ return true;
54
+ }
55
+ // <textarea>
56
+ if (type === 'textarea') {
57
+ props.value = stateObj[key] ?? '';
58
+ const existingBindInput = props.bindinput;
59
+ props.bindinput = (e) => {
60
+ const v = e?.detail?.value ?? '';
61
+ setValue(v);
62
+ if (existingBindInput)
63
+ existingBindInput(e);
64
+ };
65
+ const existingHandler = props['onUpdate:modelValue'];
66
+ props['onUpdate:modelValue'] = (v) => {
67
+ setValue(v);
68
+ if (existingHandler)
69
+ existingHandler(v);
70
+ };
71
+ return true;
72
+ }
73
+ // Not handled — fall back to the generic modelValue/onUpdate:modelValue
74
+ // pair so component-level model forwarding still works.
75
+ return false;
76
+ });
@@ -1,58 +1,130 @@
1
- //#region src/mt-hmr-bridge.ts
2
- function e() {
3
- let e = __webpack_require__?.c;
4
- if (e) for (let t in e) {
5
- if (!t.includes("@rspack") || !t.endsWith("emitter.js")) continue;
6
- let n = e[t]?.exports;
7
- if (n && typeof n.on == "function" && typeof n.emit == "function") return n;
8
- }
1
+ "use strict";
2
+ /**
3
+ * BG → MT hot-update bridge.
4
+ *
5
+ * The MT bundle has rspack's HMR runtime in code (because `module.hot.accept`
6
+ * is referenced) but no transport feeding it updates. After a save, BG's HMR
7
+ * client patches App.tsx and re-renders, generating worklet placeholders with
8
+ * new content-hash `_wkltId`s. MT's `_workletMap` still holds the old IDs, so
9
+ * the lookup fails (`bind of undefined`) when the user taps.
10
+ *
11
+ * This bridge closes the gap. We hook the same `webpackHotUpdate` event the
12
+ * rspack HMR client subscribes to. On every cycle:
13
+ * 1. Fetch the matching `main__main-thread.<hash>.hot-update.js` over the
14
+ * dev server URL (`__webpack_require__.p`).
15
+ * 2. Extract every `registerWorkletInternal(...)` call from the response.
16
+ * 3. Forward the concatenated calls to MT via
17
+ * `callLepusMethod('sigxApplyMtHotUpdate', { code }, ...)`.
18
+ * The MT handler `eval`s them in the existing realm, registering the new IDs
19
+ * into the live `_workletMap` before the user taps a freshly re-rendered
20
+ * button.
21
+ *
22
+ * Loaded only in dev mode — wired into the BG entry by `lynx-plugin`'s
23
+ * `applyEntry` when `enabledHMR` is true.
24
+ */
25
+ function findRspackEmitter() {
26
+ const cache = __webpack_require__?.c;
27
+ if (!cache)
28
+ return undefined;
29
+ for (const id in cache) {
30
+ if (!id.includes('@rspack') || !id.endsWith('emitter.js'))
31
+ continue;
32
+ const exp = cache[id]?.exports;
33
+ if (exp
34
+ && typeof exp.on === 'function'
35
+ && typeof exp.emit === 'function') {
36
+ return exp;
37
+ }
38
+ }
39
+ return undefined;
9
40
  }
41
+ // Defer subscription via a microtask: the entry chain prepends this bridge
42
+ // before `@rspack/core/hot/dev-server`, so at module-eval time the emitter
43
+ // isn't in the webpack cache yet. By the next microtask, dev-server has run
44
+ // its top-level code and the emitter module is cached.
10
45
  Promise.resolve().then(() => {
11
- let n = e();
12
- if (!n) {
13
- console.log("[sigx-mt-hmr-bridge] rspack emitter not found — bridge inactive");
14
- return;
15
- }
16
- n.on("webpackHotUpdate", () => {
17
- t();
18
- });
46
+ const emitter = findRspackEmitter();
47
+ if (!emitter) {
48
+ console.log('[sigx-mt-hmr-bridge] rspack emitter not found — bridge inactive');
49
+ return;
50
+ }
51
+ emitter.on('webpackHotUpdate', () => {
52
+ fetchAndForward();
53
+ });
19
54
  });
20
- function t() {
21
- let e = __webpack_require__?.p ?? "", t = __webpack_require__?.hu;
22
- if (typeof t != "function") return;
23
- let r = e + t("main__main-thread"), i = lynx?.requireModuleAsync;
24
- typeof i == "function" && i(r, (e, t) => {
25
- if (e) {
26
- console.log("[sigx-mt-hmr-bridge] requireModuleAsync failed:", String(e));
27
- return;
28
- }
29
- let r = "";
30
- for (let e in t.modules) {
31
- let n = t.modules[e];
32
- typeof n == "function" && (r += n.toString() + "\n");
33
- }
34
- let i = n(r);
35
- if (!i) return;
36
- let a = lynx?.getNativeApp?.();
37
- !a || typeof a.callLepusMethod != "function" || a.callLepusMethod("sigxApplyMtHotUpdate", { code: i }, () => {});
38
- });
55
+ function fetchAndForward() {
56
+ // The `webpackHotUpdate` event payload is the *new* hash, but hot-update
57
+ // chunks are named with the *previous* hash (the "delta-from" hash). Use
58
+ // `__webpack_require__.hu` same helper rspack's own loader uses — which
59
+ // reads `__webpack_require__.h()` to build the URL with the right hash.
60
+ const publicPath = __webpack_require__?.p ?? '';
61
+ const hu = __webpack_require__?.hu;
62
+ if (typeof hu !== 'function')
63
+ return;
64
+ const url = publicPath + hu('main__main-thread');
65
+ // Lynx's BG runtime doesn't have a working `fetch` for arbitrary URLs in
66
+ // many hosts; rspack's own HMR loader uses `lynx.requireModuleAsync` (see
67
+ // `loadUpdateChunk` in the BG bundle). It returns the parsed hot-update
68
+ // shape `{ modules, runtime }` — each `modules[id]` is the compiled JS
69
+ // factory function. We extract registerWorkletInternal calls from
70
+ // `factory.toString()` so we don't have to actually evaluate the factory
71
+ // (which would import worklet-runtime / install-hybrid into BG's webpack
72
+ // module graph, with unwanted side effects).
73
+ const requireModuleAsync = lynx?.requireModuleAsync;
74
+ if (typeof requireModuleAsync !== 'function')
75
+ return;
76
+ requireModuleAsync(url, (err, update) => {
77
+ if (err) {
78
+ console.log('[sigx-mt-hmr-bridge] requireModuleAsync failed:', String(err));
79
+ return;
80
+ }
81
+ let combined = '';
82
+ for (const id in update.modules) {
83
+ const factory = update.modules[id];
84
+ if (typeof factory === 'function')
85
+ combined += factory.toString() + '\n';
86
+ }
87
+ const code = extractRegistrations(combined);
88
+ if (!code)
89
+ return;
90
+ const app = lynx?.getNativeApp?.();
91
+ if (!app || typeof app.callLepusMethod !== 'function')
92
+ return;
93
+ app.callLepusMethod('sigxApplyMtHotUpdate', { code }, () => { });
94
+ });
39
95
  }
40
- function n(e) {
41
- let t = [], n = 0;
42
- for (;;) {
43
- let r = e.indexOf("registerWorkletInternal(", n);
44
- if (r === -1) break;
45
- let i = 0, a = r + 24 - 1;
46
- for (; a < e.length; a++) {
47
- let t = e[a];
48
- if (t === "(") i++;
49
- else if (t === ")" && (i--, i === 0)) break;
50
- }
51
- let o = a + 1;
52
- o < e.length && e[o] === ";" && o++, t.push(e.slice(r, o)), n = o;
53
- }
54
- return t.join("\n");
96
+ /**
97
+ * Extract `registerWorkletInternal(...)` calls from a hot-update body.
98
+ *
99
+ * Mirrors `lynx-plugin/src/loaders/worklet-utils.ts:extractRegistrations`
100
+ * (duplicated here to avoid a runtime → build-time dep). Bracket-depth count
101
+ * handles nested braces in the function body.
102
+ */
103
+ function extractRegistrations(source) {
104
+ const out = [];
105
+ const marker = 'registerWorkletInternal(';
106
+ let from = 0;
107
+ while (true) {
108
+ const idx = source.indexOf(marker, from);
109
+ if (idx === -1)
110
+ break;
111
+ let depth = 0;
112
+ let i = idx + marker.length - 1; // points at the opening '('
113
+ for (; i < source.length; i++) {
114
+ const ch = source[i];
115
+ if (ch === '(')
116
+ depth++;
117
+ else if (ch === ')') {
118
+ depth--;
119
+ if (depth === 0)
120
+ break;
121
+ }
122
+ }
123
+ let end = i + 1;
124
+ if (end < source.length && source[end] === ';')
125
+ end++;
126
+ out.push(source.slice(idx, end));
127
+ from = end;
128
+ }
129
+ return out.join('\n');
55
130
  }
56
- //#endregion
57
-
58
- //# sourceMappingURL=mt-hmr-bridge.js.map
@@ -16,7 +16,7 @@
16
16
  * SET_MT_REF op (pushed during the first JSX render) is applied before
17
17
  * the SET_GESTURE_DETECTOR op tries to resolve the workletRefMap entry.
18
18
  */
19
- import { MainThreadRef } from '../main-thread-ref';
19
+ import { MainThreadRef } from '../main-thread-ref.js';
20
20
  export declare const GestureType: {
21
21
  readonly COMPOSED: -1;
22
22
  readonly PAN: 0;