mithril-lynx 0.0.1

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.
@@ -0,0 +1,82 @@
1
+ // src/worklet-runtime.js
2
+ //
3
+ // Minimal, self-contained substitute for @lynx-js/react's real
4
+ // `runtime/lib/worklet-runtime` — needed because native's gesture (and,
5
+ // more generally, "Main Thread Script"/worklet) callback dispatch does NOT
6
+ // call a JS function value directly. Confirmed by reading the real, shipped
7
+ // @lynx-js/react v0.123.0 source (not guesswork — the same technique that
8
+ // resolved list.js's Tier 2 silent-failure bug):
9
+ //
10
+ // - runtime/lib/worklet-runtime/index.js is a side-effecting module: on
11
+ // import, if `globalThis.lynxWorkletImpl === undefined`, it calls
12
+ // `initWorklet()`, which sets `globalThis.registerWorklet` and
13
+ // `globalThis.runWorklet`. mithril-lynx never did this — those globals
14
+ // were simply undefined in every app built with it.
15
+ // - runtime/lib/worklet-runtime/workletRuntime.js's own comment on
16
+ // `runWorklet(ctx, params, options)`: "Entrance of all worklet calls.
17
+ // Native event touch handler will call this function." — i.e. native
18
+ // invokes gesture callbacks BY NAME through this global, not by calling
19
+ // the callback value itself.
20
+ // - `validateWorklet(ctx)` there requires
21
+ // `typeof ctx === 'object' && ctx !== null && ('_wkltId' in ctx || '_lepusWorkletHash' in ctx)`.
22
+ // A plain JS function has `typeof fn === 'function'`, which fails this
23
+ // check outright — explaining gesture.js's previous silent, error-free
24
+ // failure on a real device: nothing ever rejected the plain-function
25
+ // callback, native (or the missing `runWorklet`) just never ran it.
26
+ // - The real function body is registered separately, keyed by a
27
+ // `_wkltId`, via `globalThis.registerWorklet(type, id, fn)` — normally
28
+ // done by ReactLynx's compiled snapshot codegen, which this project
29
+ // deliberately has none of, so it's done by hand here at
30
+ // `createGesture()` call time instead (see gesture.js).
31
+ //
32
+ // What THIS module deliberately does NOT replicate from the real
33
+ // workletRuntime.js (out of scope for v1, same spirit as list.js's/
34
+ // gesture.js's own documented cuts): WorkletRef resolution, the
35
+ // JsFunctionLifecycleManager refcounting (`runOnBackground` cross-thread
36
+ // closures), and `addEventMethodsIfNeeded`'s event-method injection
37
+ // (`e.stopPropagation()`-style calls from inside a worklet body). A gesture
38
+ // callback that only reads its event argument and/or calls other
39
+ // mithril-lynx APIs (setState, background.runOnBackground, ...) is fully
40
+ // covered; one relying on those extra pieces is not, in v1.
41
+
42
+ function ensureWorkletRuntime() {
43
+ if (globalThis.lynxWorkletImpl !== undefined) return;
44
+ globalThis.lynxWorkletImpl = { _workletMap: {} };
45
+ globalThis.registerWorklet = function (_type, id, fn) {
46
+ globalThis.lynxWorkletImpl._workletMap[id] = fn;
47
+ };
48
+ globalThis.runWorklet = function (ctx, params) {
49
+ if (typeof ctx !== "object" || ctx === null || !("_wkltId" in ctx)) return;
50
+ var fn = globalThis.lynxWorkletImpl._workletMap[ctx._wkltId];
51
+ if (typeof fn !== "function") return;
52
+ var args = Array.isArray(params) ? params : params != null ? [params] : [];
53
+ // Deliberately NOT fn.apply(ctx, args): native passes gesture
54
+ // callbacks a 2nd argument — a native "gesture controller" object
55
+ // ({__SetGestureState, __ConsumeGesture}) — that throws "TypeError:
56
+ // not a object" when it crosses Function.prototype.apply()'s
57
+ // argument-list marshalling, but is fine passed positionally.
58
+ // Confirmed on-device by bisecting each call step; matches
59
+ // @lynx-js/react's own runWorkletImpl, which never uses apply()/
60
+ // call() either — it does `worklet(...params_)`, a plain spread call.
61
+ return fn.bind(ctx)(...args);
62
+ };
63
+ }
64
+
65
+ let nextWorkletId = 1;
66
+
67
+ /**
68
+ * Registers `fn` as a callable worklet and returns the ctx object native
69
+ * expects in its place (`{ _wkltId }`), per `validateWorklet()`'s real
70
+ * contract. `workletType` matches ReactLynx's own values ("main-thread" is
71
+ * the only one mithril-lynx currently has a use for — see gesture.js).
72
+ */
73
+ function wrapWorkletCallback(fn, workletType) {
74
+ if (typeof fn !== "function") return fn;
75
+ ensureWorkletRuntime();
76
+ const id = "mithril-lynx-worklet-" + nextWorkletId++;
77
+ globalThis.registerWorklet(workletType || "main-thread", id, fn);
78
+ return { _wkltId: id };
79
+ }
80
+
81
+ module.exports.ensureWorkletRuntime = ensureWorkletRuntime;
82
+ module.exports.wrapWorkletCallback = wrapWorkletCallback;
package/testing.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ // Ambient declaration for the ESM testing.js (the file itself is not
2
+ // type-checked; this describes its runtime export shape for TS consumers).
3
+
4
+ /**
5
+ * Installs the @lynx-js/testing-environment PAPI polyfill mithril-lynx apps
6
+ * need for their own tests. Assign to
7
+ * globalThis.onInjectMainThreadGlobals in a test setup file. See the
8
+ * project plan, Phase 9.
9
+ */
10
+ export function installTestingPolyfills(target: any): void;
package/testing.js ADDED
@@ -0,0 +1,91 @@
1
+ // testing.js
2
+ //
3
+ // Reusable @lynx-js/testing-environment PAPI polyfill for mithril-lynx apps
4
+ // (project plan, Phase 9). @lynx-js/testing-environment implements the
5
+ // string/worklet event PAPI ReactLynx uses (__AddEvent); the shim binds REAL
6
+ // JS function listeners through __AddEventListener, so that family — plus a
7
+ // handful of other PAPI functions the testing environment doesn't implement
8
+ // at all (used by element.js/gesture.js/list.js) — is polyfilled here.
9
+ //
10
+ // Usage, in a test setup file (e.g. test/setup.ts):
11
+ //
12
+ // import { installTestingPolyfills } from "mithril-lynx/testing";
13
+ // globalThis.onInjectMainThreadGlobals = installTestingPolyfills;
14
+ //
15
+ // Originally lived duplicated in mithril-app's own test/setup.ts and this
16
+ // package's; extracted here once both copies needed the exact same fixes
17
+ // (getEngine alias, __SetGestureState, __InvokeUIMethod) independently.
18
+
19
+ const papiCalls = [];
20
+
21
+ function record(name, fn) {
22
+ const wrapped = (...args) => {
23
+ papiCalls.push({ fn: name, args });
24
+ return fn(...args);
25
+ };
26
+ wrapped.__papiRecorded = true;
27
+ return wrapped;
28
+ }
29
+
30
+ /**
31
+ * Installs the polyfill on the main-thread globals object
32
+ * @lynx-js/testing-environment hands to onInjectMainThreadGlobals.
33
+ */
34
+ export function installTestingPolyfills(target) {
35
+ // The testing environment exposes the native page-lifecycle event bus via
36
+ // lynx.getNative() (addEventListener/removeEventListener/dispatchEvent),
37
+ // but doesn't alias it to getEngine() (what real Lynx and mithril-lynx's
38
+ // main-thread.js call). Alias it here so setupApp()/setupRenderer() can
39
+ // register for __RenderPage/__UpdatePage/__DestroyLifetime and tests can
40
+ // drive them via lynx.getEngine().dispatchEvent(...).
41
+ target.lynx.getEngine = target.lynx.getNative;
42
+
43
+ // Function-based event listeners (the testing env only implements the
44
+ // string/worklet __AddEvent family). Stored per element so tests can
45
+ // simulate a user gesture by invoking them.
46
+ target.__AddEventListener = (node, name, handler) => {
47
+ node.__vanillaListeners ??= {};
48
+ (node.__vanillaListeners[name] ??= new Set()).add(handler);
49
+ };
50
+
51
+ target.__RemoveEventListener = (node, name, handler) => {
52
+ node.__vanillaListeners?.[name]?.delete(handler);
53
+ };
54
+
55
+ target.__GetChildren = (node) => Array.from(node.childNodes ?? []);
56
+
57
+ target.__ElementIsEqual = (left, right) => left === right;
58
+
59
+ // Not implemented by the testing environment — used by gesture.js's
60
+ // Gesture.setState(). Recorded on the element for tests to inspect.
61
+ target.__SetGestureState = (node, id, state) => {
62
+ node.gestureState = { id, state };
63
+ };
64
+
65
+ // Not implemented by the testing environment at all — used by element.js's
66
+ // invoke(). Always succeeds, echoing back the call, so tests can assert on
67
+ // what element.js passed through without this polyfill guessing at real
68
+ // native success/failure semantics.
69
+ target.__InvokeUIMethod = (_node, method, params, callback) => {
70
+ callback({ code: 0, data: { method, params } });
71
+ return [];
72
+ };
73
+
74
+ // Tree-traversal helpers the shim needs that the testing env does not provide.
75
+ target.__GetParent = (node) => node?.parentNode ?? null;
76
+ target.__NextElement = (node) => node?.nextSibling ?? null;
77
+ target.__ReplaceElements = (parent, newChildren, oldChildren) => {
78
+ for (const child of oldChildren ?? []) {
79
+ if (child.parentNode === parent) parent.removeChild(child);
80
+ }
81
+ for (const child of newChildren ?? []) parent.appendChild(child);
82
+ };
83
+
84
+ // Record every PAPI call so tests can assert on side effects.
85
+ for (const name of Object.getOwnPropertyNames(target)) {
86
+ if (name.startsWith("__") && typeof target[name] === "function" && !target[name].__papiRecorded) {
87
+ target[name] = record(name, target[name]);
88
+ }
89
+ }
90
+ target.__papiCalls = papiCalls;
91
+ }