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.
- package/CONTRACT.md +151 -0
- package/LICENSE +21 -0
- package/README.md +227 -0
- package/background.d.ts +54 -0
- package/background.js +169 -0
- package/element.d.ts +34 -0
- package/element.js +79 -0
- package/gesture.d.ts +40 -0
- package/gesture.js +117 -0
- package/internal/constants.js +26 -0
- package/internal/virtual-node.js +388 -0
- package/list.d.ts +31 -0
- package/list.js +185 -0
- package/main-thread.d.ts +43 -0
- package/main-thread.js +165 -0
- package/package.json +100 -0
- package/plugin.d.ts +17 -0
- package/plugin.js +185 -0
- package/renderer/background.d.ts +21 -0
- package/renderer/background.js +84 -0
- package/renderer/main-thread.d.ts +12 -0
- package/renderer/main-thread.js +175 -0
- package/src/lynx-mithril-shim.d.ts +16 -0
- package/src/lynx-mithril-shim.js +1477 -0
- package/src/worklet-runtime.js +82 -0
- package/testing.d.ts +10 -0
- package/testing.js +91 -0
|
@@ -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
|
+
}
|