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
package/plugin.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// plugin.js
|
|
2
|
+
//
|
|
3
|
+
// Generalized version of lynx-examples/examples/vanilla/plugin.ts's
|
|
4
|
+
// dual-bundle build pattern, published as a reusable Rsbuild/Rspeedy plugin
|
|
5
|
+
// instead of being copy-pasted per app.
|
|
6
|
+
//
|
|
7
|
+
// Convention: for each configured entry, the entry's import path names a
|
|
8
|
+
// "main-thread" file. If a sibling "background.ts"/"background.js" exists
|
|
9
|
+
// next to it, it is picked up automatically and compiled as a second Lynx
|
|
10
|
+
// bundle chunk (the background/JS-thread bundle), while the main-thread file
|
|
11
|
+
// is always compiled and encoded as lepus (main-thread/Lepus VM chunk).
|
|
12
|
+
//
|
|
13
|
+
// This is pure multi-entry bundling, no AST transform of user code: apps
|
|
14
|
+
// author two plain files (main-thread.ts + optional background.ts) and this
|
|
15
|
+
// plugin wires them into the two Lynx bundle slots. See mithril-lynx's
|
|
16
|
+
// project plan, Phase 2.
|
|
17
|
+
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { createRequire } from "node:module";
|
|
21
|
+
|
|
22
|
+
import { RuntimeWrapperWebpackPlugin } from "@lynx-js/runtime-wrapper-webpack-plugin";
|
|
23
|
+
import { LynxEncodePlugin, LynxTemplatePlugin } from "@lynx-js/template-webpack-plugin";
|
|
24
|
+
|
|
25
|
+
const PLUGIN_NAME = "mithril-lynx-template-webpack";
|
|
26
|
+
|
|
27
|
+
const BACKGROUND_CANDIDATES = ["background.ts", "background.js"];
|
|
28
|
+
const STYLE_CANDIDATES = ["style.css"];
|
|
29
|
+
|
|
30
|
+
function findSibling(dir, candidates) {
|
|
31
|
+
for (const name of candidates) {
|
|
32
|
+
const candidate = path.join(dir, name);
|
|
33
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function pluginMithrilLynx(options = {}) {
|
|
39
|
+
const targetSdkVersion = options.targetSdkVersion ?? "3.5";
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
name: PLUGIN_NAME,
|
|
43
|
+
setup(api) {
|
|
44
|
+
// Keep the template plugin discoverable by Rspeedy's Lynx internals.
|
|
45
|
+
api.expose(Symbol.for("LynxTemplatePlugin"), { LynxTemplatePlugin });
|
|
46
|
+
api.modifyBundlerChain((chain) => {
|
|
47
|
+
// mithril-lynx's own src/lynx-mithril-shim.js deep-imports mithril's
|
|
48
|
+
// internal render/cachedAttrsIsStaticMap.js (and its emptyAttrs
|
|
49
|
+
// singleton). If the app's own `require("mithril")` resolves to a
|
|
50
|
+
// DIFFERENT physical copy of the package than the one mithril-lynx
|
|
51
|
+
// itself was installed/linked with — the norm for a `file:`-linked
|
|
52
|
+
// local package, whose own node_modules (built for ITS OWN tests)
|
|
53
|
+
// shadows Node's normal directory-walk resolution once linked — the
|
|
54
|
+
// two copies' emptyAttrs singletons differ. The shim then can't
|
|
55
|
+
// recognize the app's legitimately-reused empty-attrs object as
|
|
56
|
+
// such, and Mithril's own updateAttrs() misfires its "Don't reuse
|
|
57
|
+
// attrs object" warning on every plain `m(tag, null, ...)` element,
|
|
58
|
+
// every redraw. Force a single resolution by aliasing "mithril" to
|
|
59
|
+
// whatever copy the app itself resolves from its own project root.
|
|
60
|
+
try {
|
|
61
|
+
const appRequire = createRequire(path.join(process.cwd(), "package.json"));
|
|
62
|
+
// Resolve the PACKAGE DIRECTORY (not mithril's own main entry
|
|
63
|
+
// file) — a prefix alias needs "mithril/render/x" to rewrite to
|
|
64
|
+
// "<dir>/render/x", which only works aliased to a directory.
|
|
65
|
+
const mithrilDir = path.dirname(appRequire.resolve("mithril/package.json"));
|
|
66
|
+
chain.resolve.alias.set("mithril", mithrilDir);
|
|
67
|
+
} catch {
|
|
68
|
+
// App has no local "mithril" resolvable from its own root —
|
|
69
|
+
// leave resolution as-is rather than guessing.
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const rawEntries = Object.entries(chain.entryPoints.entries() ?? {});
|
|
73
|
+
chain.entryPoints.clear();
|
|
74
|
+
|
|
75
|
+
for (const [name, entry] of rawEntries) {
|
|
76
|
+
const value = entry.values()?.[0];
|
|
77
|
+
const imports = typeof value === "string" || Array.isArray(value) ? value : value?.import;
|
|
78
|
+
const mtSource = Array.isArray(imports) ? imports[0] : imports;
|
|
79
|
+
if (typeof mtSource !== "string") continue;
|
|
80
|
+
|
|
81
|
+
const dir = path.dirname(mtSource);
|
|
82
|
+
const bgSource = findSibling(dir, BACKGROUND_CANDIDATES);
|
|
83
|
+
const cssSource = findSibling(dir, STYLE_CANDIDATES);
|
|
84
|
+
|
|
85
|
+
const bgEntry = `${name}__background`;
|
|
86
|
+
const mtEntry = `${name}__main-thread`;
|
|
87
|
+
const bgAsset = `.rspeedy/${name}/background.js`;
|
|
88
|
+
const mtAsset = `.rspeedy/${name}/main-thread.js`;
|
|
89
|
+
const hasBackground = bgSource != null;
|
|
90
|
+
|
|
91
|
+
// Each entry always has main-thread code and may opt into a
|
|
92
|
+
// background thread by adding a sibling background.ts file.
|
|
93
|
+
if (hasBackground) {
|
|
94
|
+
chain.entry(bgEntry).add({
|
|
95
|
+
import: bgSource,
|
|
96
|
+
filename: bgAsset,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
chain.entry(mtEntry).add({
|
|
101
|
+
import: cssSource != null ? [mtSource, cssSource] : [mtSource],
|
|
102
|
+
filename: mtAsset,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
chain.plugin(`template-${name}`).use(LynxTemplatePlugin, [
|
|
106
|
+
{
|
|
107
|
+
...LynxTemplatePlugin.defaultOptions,
|
|
108
|
+
filename: `${name}.bundle`,
|
|
109
|
+
intermediate: `.rspeedy/${name}`,
|
|
110
|
+
chunks: hasBackground ? [bgEntry, mtEntry] : [mtEntry],
|
|
111
|
+
dsl: "react_nodiff",
|
|
112
|
+
targetSdkVersion,
|
|
113
|
+
cssPlugins: [],
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
if (hasBackground) {
|
|
118
|
+
// Background chunks run in the JavaScript thread and need the
|
|
119
|
+
// Lynx runtime wrapper; main-thread chunks are encoded as lepus.
|
|
120
|
+
chain.plugin(`runtime-wrapper-${name}`).use(
|
|
121
|
+
RuntimeWrapperWebpackPlugin,
|
|
122
|
+
[
|
|
123
|
+
{
|
|
124
|
+
targetSdkVersion,
|
|
125
|
+
test: new RegExp(`${name}/background\\.js$`),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
chain.plugin("encode").use(LynxEncodePlugin, []);
|
|
133
|
+
|
|
134
|
+
chain.plugin("before-encode").use({
|
|
135
|
+
apply(compiler) {
|
|
136
|
+
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
|
|
137
|
+
// The default grouping only routes a chunk to lepus (main thread)
|
|
138
|
+
// when its asset carries `lynx:main-thread`. These hand-built
|
|
139
|
+
// entries don't, so main-thread JS lands in `manifest` and lepus
|
|
140
|
+
// stays empty. Re-map it here: background JS to manifest, the
|
|
141
|
+
// main-thread chunk to lepus. CSS is already grouped correctly.
|
|
142
|
+
const hooks = LynxTemplatePlugin.getLynxTemplatePluginHooks(compilation);
|
|
143
|
+
hooks.beforeEncode.tap(PLUGIN_NAME, (args) => {
|
|
144
|
+
const pageName = args.intermediate ? path.basename(args.intermediate) : "";
|
|
145
|
+
if (!pageName) return args;
|
|
146
|
+
|
|
147
|
+
const bgAsset = `.rspeedy/${pageName}/background.js`;
|
|
148
|
+
const mtAsset = `.rspeedy/${pageName}/main-thread.js`;
|
|
149
|
+
|
|
150
|
+
const backgroundAsset = compilation.getAsset(bgAsset);
|
|
151
|
+
const mainThreadAsset = compilation.getAsset(mtAsset);
|
|
152
|
+
|
|
153
|
+
if (!mainThreadAsset) return args;
|
|
154
|
+
|
|
155
|
+
args.encodeData.compilerOptions.targetSdkVersion = targetSdkVersion;
|
|
156
|
+
args.encodeData.compilerOptions.enableEventRefactor = true;
|
|
157
|
+
|
|
158
|
+
// Route tap/gesture events through the refactored main-thread
|
|
159
|
+
// path so `__AddEventListener` handlers fire. This page-config
|
|
160
|
+
// flag was dropped from `@lynx-js/config-rsbuild-plugin` 0.2.0's
|
|
161
|
+
// schema, so set it on the page config directly.
|
|
162
|
+
args.encodeData.sourceContent.config.enableEventHandleRefactor = true;
|
|
163
|
+
|
|
164
|
+
args.encodeData.manifest = backgroundAsset
|
|
165
|
+
? {
|
|
166
|
+
[backgroundAsset.name]: backgroundAsset.source
|
|
167
|
+
.source()
|
|
168
|
+
.toString(),
|
|
169
|
+
}
|
|
170
|
+
: {};
|
|
171
|
+
args.encodeData.lepusCode = {
|
|
172
|
+
root: mainThreadAsset,
|
|
173
|
+
chunks: [],
|
|
174
|
+
filename: mainThreadAsset.name,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return args;
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Ambient declaration for the ESM renderer/background.js (the file itself is
|
|
2
|
+
// not type-checked; this describes its runtime export shape for TS consumers).
|
|
3
|
+
|
|
4
|
+
export interface RenderAppOptions {
|
|
5
|
+
/** Called exactly once to build the root Mithril vnode against the virtual tree. */
|
|
6
|
+
root(): unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Renders `root()` against a virtual (op-log-recording) tree and sends the
|
|
11
|
+
* resulting patch to the main thread. Call once, at background.ts's top
|
|
12
|
+
* level. See the project plan, Phase 4 ("renderer mode").
|
|
13
|
+
*/
|
|
14
|
+
export function renderApp(options: RenderAppOptions): void;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Re-invokes the app's view() (via the shim's own redraw()) and flushes the
|
|
18
|
+
* resulting patch to the main thread. Call this instead of importing the
|
|
19
|
+
* shim's redraw() directly in renderer-mode event handlers.
|
|
20
|
+
*/
|
|
21
|
+
export function redraw(): void;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// renderer/background.js
|
|
2
|
+
//
|
|
3
|
+
// "Renderer mode" (project plan, Phase 4): runs Mithril's UNMODIFIED diff
|
|
4
|
+
// algorithm on the background thread against a VirtualNodeWrapper tree
|
|
5
|
+
// (internal/virtual-node.js) instead of a real Element PAPI tree. Every
|
|
6
|
+
// mutation the diff makes is recorded as a serializable op; ops accumulated
|
|
7
|
+
// during one render/redraw pass are flushed as a single batch to the main
|
|
8
|
+
// thread, which replays them against a real LynxNodeWrapper tree (see
|
|
9
|
+
// renderer/main-thread.js's applyPatch()).
|
|
10
|
+
//
|
|
11
|
+
// This reuses the shim's OWN render()/redraw() convenience API unmodified —
|
|
12
|
+
// the algorithm never knows it's talking to a virtual tree, because it only
|
|
13
|
+
// ever calls generic dom.* methods (see ../CONTRACT.md). Note: render(), not
|
|
14
|
+
// renderToPage() — renderToPage() expects a RAW native page handle and calls
|
|
15
|
+
// __GetElementUniqueID() on it internally; render() takes an
|
|
16
|
+
// already-constructed wrapper directly, which is what the virtual root is.
|
|
17
|
+
|
|
18
|
+
import shim from "../src/lynx-mithril-shim.js";
|
|
19
|
+
import { createVirtualDocument } from "../internal/virtual-node.js";
|
|
20
|
+
import { rendererEventEventName, rendererPatchEventName } from "../internal/constants.js";
|
|
21
|
+
|
|
22
|
+
// A real app calls renderApp() exactly once, establishing this state for the
|
|
23
|
+
// lifetime of the app; redraw() operates on whatever the latest renderApp()
|
|
24
|
+
// call set up. Kept in module scope (rather than passed around) only because
|
|
25
|
+
// redraw() is a separate export with no other way to reach it — NOT shared
|
|
26
|
+
// across independent virtual trees the way a naively module-level
|
|
27
|
+
// styleProxies/vid-counter would be (createVirtualDocument() is called AFRESH
|
|
28
|
+
// inside renderApp(), so each call gets its own independent document, vid
|
|
29
|
+
// counter, and style-proxy registry).
|
|
30
|
+
let opLog = [];
|
|
31
|
+
let flushStyleProxies = () => {};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Renders `root()` once against the virtual tree and sends the initial
|
|
35
|
+
* patch. Call exactly once, at background.ts's top level (mirrors
|
|
36
|
+
* setupApp()'s root() contract in data-channel mode: subsequent updates
|
|
37
|
+
* flow through redraw(), which re-invokes the component's view(), not
|
|
38
|
+
* root() again).
|
|
39
|
+
*/
|
|
40
|
+
export function renderApp(options) {
|
|
41
|
+
const { root } = options;
|
|
42
|
+
opLog = [];
|
|
43
|
+
// vid -> VirtualNodeWrapper, so forwarded main-thread events (addressed by
|
|
44
|
+
// vid) can be dispatched to the right node's listener.
|
|
45
|
+
const nodesByVid = new Map();
|
|
46
|
+
|
|
47
|
+
const virtualDocument = createVirtualDocument(
|
|
48
|
+
(op) => opLog.push(op),
|
|
49
|
+
(wrapper) => nodesByVid.set(wrapper._vid, wrapper),
|
|
50
|
+
);
|
|
51
|
+
flushStyleProxies = virtualDocument.flushStyleProxies;
|
|
52
|
+
|
|
53
|
+
const rootWrapper = virtualDocument.createRootWrapper();
|
|
54
|
+
|
|
55
|
+
shim.render(rootWrapper, root());
|
|
56
|
+
flush();
|
|
57
|
+
|
|
58
|
+
lynx.getCoreContext().addEventListener(rendererEventEventName, (event) => {
|
|
59
|
+
const { vid, type, payload } = event.data;
|
|
60
|
+
const node = nodesByVid.get(vid);
|
|
61
|
+
if (node == null) return;
|
|
62
|
+
node.dispatchEvent({
|
|
63
|
+
type,
|
|
64
|
+
currentTarget: node,
|
|
65
|
+
preventDefault() {},
|
|
66
|
+
stopPropagation() {},
|
|
67
|
+
...payload,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function flush() {
|
|
73
|
+
flushStyleProxies();
|
|
74
|
+
if (opLog.length === 0) return;
|
|
75
|
+
const ops = opLog;
|
|
76
|
+
opLog = [];
|
|
77
|
+
lynx.getCoreContext().dispatchEvent({ type: rendererPatchEventName, data: ops });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Re-invokes the app's view() and flushes the resulting patch. Call this — not shim.redraw() directly — after mutating state in an event handler. */
|
|
81
|
+
export function redraw() {
|
|
82
|
+
shim.redraw();
|
|
83
|
+
flush();
|
|
84
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Ambient declaration for the ESM renderer/main-thread.js (the file itself
|
|
2
|
+
// is not type-checked; this describes its runtime export shape for TS
|
|
3
|
+
// consumers).
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wires the Lynx engine's page lifecycle and the renderer-mode patch/event
|
|
7
|
+
* channel: replays op-log patches from the background thread's virtual tree
|
|
8
|
+
* against a real Element PAPI tree, and forwards real PAPI events back to
|
|
9
|
+
* the background thread by vid. Call once, at main-thread.ts's top level.
|
|
10
|
+
* See the project plan, Phase 4 ("renderer mode").
|
|
11
|
+
*/
|
|
12
|
+
export function setupRenderer(): void;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// renderer/main-thread.js
|
|
2
|
+
//
|
|
3
|
+
// "Renderer mode" (project plan, Phase 4) main-thread half: replays the
|
|
4
|
+
// op-log produced by renderer/background.js's virtual tree against a real
|
|
5
|
+
// LynxNodeWrapper tree, and forwards real Element PAPI events back to the
|
|
6
|
+
// background thread by vid (see renderer/background.js's header for the
|
|
7
|
+
// full picture).
|
|
8
|
+
//
|
|
9
|
+
// applyPatch() deliberately does NOT reimplement any PAPI-mapping logic —
|
|
10
|
+
// every op is replayed by calling the EXACT SAME method (setAttribute,
|
|
11
|
+
// className =, style.setProperty, ...) a normal main-thread-owned Mithril
|
|
12
|
+
// app would call on a real LynxNodeWrapper, so it inherits that (already
|
|
13
|
+
// tested) code's correctness for free. The one exception is style: see the
|
|
14
|
+
// setStyleProps case below for why it calls __SetInlineStyles directly.
|
|
15
|
+
|
|
16
|
+
import shim from "../src/lynx-mithril-shim.js";
|
|
17
|
+
import {
|
|
18
|
+
destroyLifetimeEventName,
|
|
19
|
+
renderPageEventName,
|
|
20
|
+
rendererEventEventName,
|
|
21
|
+
rendererPatchEventName,
|
|
22
|
+
} from "../internal/constants.js";
|
|
23
|
+
|
|
24
|
+
// The native engine unconditionally invokes a global `processData(initData)`
|
|
25
|
+
// hook on every __RenderPage, regardless of framework or rendering mode —
|
|
26
|
+
// found missing here via real-device testing (main-thread.js, data-channel
|
|
27
|
+
// mode, already had this fix from Phase 1; this file was never given it).
|
|
28
|
+
Object.assign(globalThis, {
|
|
29
|
+
processData: (data) => data,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Waits for __RenderPage to create the real page (matching data-channel
|
|
34
|
+
* mode's timing, in case native requires it before the tree can be built),
|
|
35
|
+
* then wires the patch/event channel. The background thread's own initial
|
|
36
|
+
* renderApp() runs independently and may finish before or after
|
|
37
|
+
* __RenderPage fires, so patches arriving early are buffered and replayed
|
|
38
|
+
* in order once the page exists. Call once, at main-thread.ts's top level.
|
|
39
|
+
*/
|
|
40
|
+
export function setupRenderer() {
|
|
41
|
+
// vid -> LynxNodeWrapper. Scoped to this setupRenderer() call (a real app
|
|
42
|
+
// calls it exactly once) rather than module-level, so it can never hold
|
|
43
|
+
// stale entries from a previous, unrelated render.
|
|
44
|
+
const vidMap = new Map();
|
|
45
|
+
// (vid + ":" + type) -> the forwarding function passed to addEventListener,
|
|
46
|
+
// so a later removeEvent op can pass the SAME reference to
|
|
47
|
+
// removeEventListener (LynxNodeWrapper matches listeners by identity).
|
|
48
|
+
const forwarders = new Map();
|
|
49
|
+
|
|
50
|
+
function applyOp(op) {
|
|
51
|
+
switch (op.op) {
|
|
52
|
+
case "createElement": {
|
|
53
|
+
const wrapper = vidMap.get(0).ownerDocument.createElement(op.tag);
|
|
54
|
+
vidMap.set(op.vid, wrapper);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
case "createText": {
|
|
58
|
+
const wrapper = vidMap.get(0).ownerDocument.createTextNode(op.value);
|
|
59
|
+
vidMap.set(op.vid, wrapper);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case "appendChild":
|
|
63
|
+
vidMap.get(op.parentVid).appendChild(vidMap.get(op.childVid));
|
|
64
|
+
break;
|
|
65
|
+
case "insertBefore":
|
|
66
|
+
vidMap.get(op.parentVid).insertBefore(
|
|
67
|
+
vidMap.get(op.childVid),
|
|
68
|
+
op.refVid != null ? vidMap.get(op.refVid) : null,
|
|
69
|
+
);
|
|
70
|
+
break;
|
|
71
|
+
case "removeChild":
|
|
72
|
+
vidMap.get(op.parentVid).removeChild(vidMap.get(op.childVid));
|
|
73
|
+
vidMap.delete(op.childVid);
|
|
74
|
+
break;
|
|
75
|
+
case "setProp":
|
|
76
|
+
vidMap.get(op.vid)[op.key] = op.value;
|
|
77
|
+
break;
|
|
78
|
+
case "setAttribute":
|
|
79
|
+
vidMap.get(op.vid).setAttribute(op.key, op.value);
|
|
80
|
+
break;
|
|
81
|
+
case "removeAttribute":
|
|
82
|
+
vidMap.get(op.vid).removeAttribute(op.key);
|
|
83
|
+
break;
|
|
84
|
+
case "setStyleProps": {
|
|
85
|
+
// The virtual side's styles object is already the COMPLETE, current
|
|
86
|
+
// style set (VirtualStyleProxy._flush recomputes it from scratch
|
|
87
|
+
// every time, exactly like the real LynxStyleProxy._flush does).
|
|
88
|
+
// Calling __SetInlineStyles directly with that complete object —
|
|
89
|
+
// rather than replaying individual `wrapper.style[key] = value`
|
|
90
|
+
// assignments through the real LynxStyleProxy — is what correctly
|
|
91
|
+
// propagates REMOVALS: the real proxy only accumulates/overwrites via
|
|
92
|
+
// setProperty, so a key absent from this op (because it was removed
|
|
93
|
+
// on the virtual side) would otherwise never get cleared.
|
|
94
|
+
//
|
|
95
|
+
// Every node gets a style proxy flushed unconditionally (matching
|
|
96
|
+
// the real shim's own flushTree(), which does the same for EVERY
|
|
97
|
+
// LynxStyleProxy ever created) — including raw text nodes, which
|
|
98
|
+
// don't support inline styles at all. The real shim's flushTree()
|
|
99
|
+
// silently swallows that failure (`try { ... } catch {}`) since it
|
|
100
|
+
// runs the flush and the PAPI call in one step, same-thread; here
|
|
101
|
+
// the two are split across the wire, so the try/catch has to live
|
|
102
|
+
// here, on the replay side, instead.
|
|
103
|
+
try {
|
|
104
|
+
__SetInlineStyles(vidMap.get(op.vid)._handle, op.styles);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
/* ignore, e.g. raw text nodes have no .style */
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case "setText":
|
|
111
|
+
vidMap.get(op.vid).nodeValue = op.value;
|
|
112
|
+
break;
|
|
113
|
+
case "addEvent": {
|
|
114
|
+
const wrapper = vidMap.get(op.vid);
|
|
115
|
+
const forward = (ev) => {
|
|
116
|
+
const { currentTarget, preventDefault, stopPropagation, ...payload } = ev;
|
|
117
|
+
lynx.getJSContext().dispatchEvent({
|
|
118
|
+
type: rendererEventEventName,
|
|
119
|
+
data: { vid: op.vid, type: ev.type, payload },
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
forwarders.set(op.vid + ":" + op.type, forward);
|
|
123
|
+
wrapper.addEventListener(op.type, forward, {});
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case "removeEvent": {
|
|
127
|
+
const key = op.vid + ":" + op.type;
|
|
128
|
+
const forward = forwarders.get(key);
|
|
129
|
+
if (forward != null) {
|
|
130
|
+
vidMap.get(op.vid).removeEventListener(op.type, forward, {});
|
|
131
|
+
forwarders.delete(key);
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
default:
|
|
136
|
+
throw new Error(`renderer/main-thread.js: unknown op "${op.op}"`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function applyPatch(ops) {
|
|
141
|
+
for (const op of ops) applyOp(op);
|
|
142
|
+
__FlushElementTree();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const engine = lynx.getEngine();
|
|
146
|
+
const background = lynx.getJSContext();
|
|
147
|
+
let pageReady = false;
|
|
148
|
+
let pendingPatches = [];
|
|
149
|
+
|
|
150
|
+
const onPatch = (event) => {
|
|
151
|
+
if (!pageReady) {
|
|
152
|
+
pendingPatches.push(event.data);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
applyPatch(event.data);
|
|
156
|
+
};
|
|
157
|
+
background.addEventListener(rendererPatchEventName, onPatch);
|
|
158
|
+
|
|
159
|
+
const onRenderPage = () => {
|
|
160
|
+
const page = __CreatePage("0", 0);
|
|
161
|
+
vidMap.set(0, shim.createPageWrapper(page));
|
|
162
|
+
pageReady = true;
|
|
163
|
+
for (const ops of pendingPatches) applyPatch(ops);
|
|
164
|
+
pendingPatches = [];
|
|
165
|
+
};
|
|
166
|
+
engine.addEventListener(renderPageEventName, onRenderPage);
|
|
167
|
+
|
|
168
|
+
const onDestroyLifetime = () => {
|
|
169
|
+
background.dispatchEvent({ type: destroyLifetimeEventName, data: undefined });
|
|
170
|
+
background.removeEventListener(rendererPatchEventName, onPatch);
|
|
171
|
+
engine.removeEventListener(renderPageEventName, onRenderPage);
|
|
172
|
+
engine.removeEventListener(destroyLifetimeEventName, onDestroyLifetime);
|
|
173
|
+
};
|
|
174
|
+
engine.addEventListener(destroyLifetimeEventName, onDestroyLifetime);
|
|
175
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Ambient declaration for the CJS lynx-mithril-shim.js (the file itself is
|
|
2
|
+
// not type-checked; this describes its runtime export shape for main-thread.ts).
|
|
3
|
+
|
|
4
|
+
declare const shim: {
|
|
5
|
+
(dom: unknown, vnodes: unknown, redraw?: () => void): void;
|
|
6
|
+
render(rootWrapper: unknown, vnode: unknown): void;
|
|
7
|
+
redraw(): void;
|
|
8
|
+
createPageWrapper(pageElement: unknown): unknown;
|
|
9
|
+
renderToPage(pageElement: unknown, vnode: unknown): unknown;
|
|
10
|
+
createLynxWindow(pageElement: unknown): unknown;
|
|
11
|
+
LynxNodeWrapper: unknown;
|
|
12
|
+
LynxStyleProxy: unknown;
|
|
13
|
+
normalizeEvent(rawEv: unknown, node: unknown): unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default shim;
|