mithril-lynx 0.0.5 → 0.0.7
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/README.md +15 -4
- package/element.js +5 -1
- package/package.json +1 -1
- package/plugin.js +97 -5
- package/src/dev-reload-client.js +122 -0
- package/src/lynx-mithril-shim.js +13 -3
package/README.md
CHANGED
|
@@ -264,10 +264,11 @@ Use a plain CSS `@font-face` rule — not `lynx.addFont()`. That JS API (backgro
|
|
|
264
264
|
}
|
|
265
265
|
```
|
|
266
266
|
|
|
267
|
-
|
|
267
|
+
Three gotchas, all confirmed on real hardware:
|
|
268
268
|
|
|
269
|
-
- **The font file must be `.ttf`, not `.woff2
|
|
270
|
-
- **`font-family` set on `:root` (or any ancestor) does not cascade to descendants by default
|
|
269
|
+
- **The font file must be `.ttf`, not `.woff2`** (2026-09-10). A `.woff2` `@font-face` compiles fine — a valid `url('data:font/woff2;base64,...')` lands in the bundle, no build error, no runtime error — but the native text renderer silently never applies it, even with a maximally-distinctive test font (swapping the default sans-serif for a cursive/marker-style face produced zero visual change). Re-pointing the exact same rule at a `.ttf` of the same font worked immediately, no other change needed. Fontsource-distributed packages only ship woff/woff2; get a `.ttf` from the font's original source instead (e.g. [`google/fonts`](https://github.com/google/fonts) for anything Google-Fonts-hosted).
|
|
270
|
+
- **`font-family` set on `:root` (or any ancestor) does not cascade to descendants by default** (2026-09-10). `@lynx-js/config-rsbuild-plugin`'s `pluginLynxConfig()` has an `enableCSSInheritance` option that's off unless set explicitly; without it, only the exact element the property is set on gets it — confirmed by setting `font-family: serif` on `:root` and seeing zero change anywhere in the tree. Turn it on (`pluginLynxConfig({ enableCSSInheritance: true })` in `lynx.config.ts`) to use a single `:root` declaration instead of repeating `font-family` on every class.
|
|
271
|
+
- **A declarative `@font-face` resolves synchronously on the first native `__FlushElementTree()` call, and that cost scales with how many text nodes/CSS rules end up resolving it** (2026-09-10/11) — up to +2s of cold start observed on a mid/low-end device (Samsung Galaxy A07) applying a font broadly via a `text{}` type selector, root-caused via node-count bisection and a native-Android control app with zero Lynx dependencies (~30-100ms there for the same font on the same number of views). Filed upstream as [lynx-family/lynx#9431](https://github.com/lynx-family/lynx/issues/9431), which also documents a working native-side workaround (not a mithril-lynx-level fix — it lives in the consuming Android app, calling `FontFaceManager.getInstance().prefetchFont(...)` before `renderTemplateUrl()` to warm Lynx's typeface cache ahead of the JS bundle even starting): brought cold start down to ~750-800ms, indistinguishable from the fontless baseline, in `indicadores-android` (shipped there, not part of this package). Check that issue for upstream maintainer responses before re-investigating the same symptom.
|
|
271
272
|
|
|
272
273
|
## Known permanent gaps
|
|
273
274
|
|
|
@@ -286,6 +287,8 @@ Beyond hyperscript (`m(...)`) itself, only `m.fragment` and `m.censor` are used
|
|
|
286
287
|
|
|
287
288
|
Mithril was never hooks-based, so — unlike React — there's no special rules-of-hooks compatibility story to build: `m.redraw()` after any state mutation already works with any plain-JS state library (a simple pub/sub store, streams, whatever). Nothing in this package needs to shim a specific state-management library for that reason; if something in the ecosystem doesn't work, it's not because of a hooks-equivalence gap.
|
|
288
289
|
|
|
290
|
+
**The main-thread JS engine is QuickJS-based, not V8 or a browser engine** — don't assume full modern JS API coverage just because something works under `@lynx-js/testing-environment`'s jsdom polyfill (which runs on Node's V8) or in editor tooling. Confirmed missing on real hardware: `Array.prototype.at()` (ES2022) — calling it throws a generic `TypeError: not a function` with a near-useless native backtrace (no indication it's an API-support gap rather than a framework bug), and since it only fails once an array actually has content, the crash can look like a much bigger, unrelated regression than it is. This bit a consuming app (`mithril-lynx-ui`'s demo) hard enough to cost a full debugging session before being traced back to this. Use `arr[arr.length - 1]` instead; `.slice(-N)` (pre-ES2022, negative index) is fine. If you reach for a newer Array/Object/String method, verify it on a real device build before trusting it — the jsdom suite will not catch this class of gap.
|
|
291
|
+
|
|
289
292
|
## Testing
|
|
290
293
|
|
|
291
294
|
```bash
|
|
@@ -295,4 +298,12 @@ bun run test
|
|
|
295
298
|
|
|
296
299
|
Tests run against `@lynx-js/testing-environment`'s jsdom-backed PAPI polyfill. The polyfill itself is published as `mithril-lynx/testing`'s `installTestingPolyfills()` (see `test/setup.ts` for the one-line setup) — consuming apps can use the exact same polyfill for their own tests instead of maintaining a duplicate copy; see `mithril-app/test/setup.ts` for a worked example.
|
|
297
300
|
|
|
298
|
-
**Not provided**: Fast Refresh and a devtools/inspector bundle (project plan, Phase 9 / subsystem 12) are explicitly out of scope — they're deep, compiler-driven DX features in upstream ReactLynx with no zero-compiler equivalent worth building.
|
|
301
|
+
**Not provided**: Fast Refresh and a devtools/inspector bundle (project plan, Phase 9 / subsystem 12) are explicitly out of scope — they're deep, compiler-driven DX features in upstream ReactLynx with no zero-compiler equivalent worth building.
|
|
302
|
+
|
|
303
|
+
In development, `pluginMithrilLynx()` adds a small background-thread client even
|
|
304
|
+
when the app has no `background.ts`. After a successful rebuild it reloads the
|
|
305
|
+
bundle through Lynx Go's `ExplorerModule.openSchema()`, rather than attempting
|
|
306
|
+
module HMR (Mithril views run in the separate main-thread/Lepus bundle). This
|
|
307
|
+
is intentionally a **Lynx Go viewer** convenience, not a portable SDK API: a
|
|
308
|
+
different viewer or a final native host that does not provide `ExplorerModule`
|
|
309
|
+
will log a warning and must be reloaded manually.
|
package/element.js
CHANGED
|
@@ -39,7 +39,11 @@ export function wrapElement(node) {
|
|
|
39
39
|
},
|
|
40
40
|
|
|
41
41
|
setAttribute(name, value) {
|
|
42
|
-
|
|
42
|
+
// "" not undefined when clearing: real hardware's FiberSetClasses
|
|
43
|
+
// rejects a non-string argument ("FiberSetClasses param 1 should be
|
|
44
|
+
// String") — see lynx-mithril-shim.js's own matching fix for the
|
|
45
|
+
// same call.
|
|
46
|
+
if (name === "class") __SetClasses(handle, value == null ? "" : String(value));
|
|
43
47
|
else if (name === "id") __SetID(handle, value == null ? null : String(value));
|
|
44
48
|
else if (name.slice(0, 5) === "data-") __AddDataset(handle, name.slice(5), value);
|
|
45
49
|
else __SetAttribute(handle, name, value == null ? null : value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mithril-lynx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Mithril.js rendered through Lynx's Element PAPI \u2014 a contract-complete port of mithril/render/render.js@2.3.8 to the Lynx main thread, packaged as a reusable framework.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/plugin.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import fs from "node:fs";
|
|
19
19
|
import path from "node:path";
|
|
20
20
|
import { createRequire } from "node:module";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
21
22
|
|
|
22
23
|
import { RuntimeWrapperWebpackPlugin } from "@lynx-js/runtime-wrapper-webpack-plugin";
|
|
23
24
|
import { LynxEncodePlugin, LynxTemplatePlugin } from "@lynx-js/template-webpack-plugin";
|
|
@@ -27,6 +28,55 @@ const PLUGIN_NAME = "mithril-lynx-template-webpack";
|
|
|
27
28
|
const BACKGROUND_CANDIDATES = ["background.ts", "background.js"];
|
|
28
29
|
const STYLE_CANDIDATES = ["style.css"];
|
|
29
30
|
|
|
31
|
+
// Live reload's client only runs where a full JS engine with Native Module
|
|
32
|
+
// access exists: the background/JS thread. The main-thread/Lepus VM cannot
|
|
33
|
+
// use it. An app without background.ts therefore gets a synthetic background
|
|
34
|
+
// entry in development; see `includeBackground` below. Imported by this
|
|
35
|
+
// resolved absolute path directly (see the entry-construction loop below),
|
|
36
|
+
// not through a "mithril-lynx/..." bare specifier — that collides with the
|
|
37
|
+
// package's own broader "mithril-lynx" resolve alias.
|
|
38
|
+
const DEV_RELOAD_CLIENT_PATH = path.join(
|
|
39
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
40
|
+
"src",
|
|
41
|
+
"dev-reload-client.js",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
function createDevReloadClientQuery(api, environment, entryName) {
|
|
45
|
+
const config = environment.config ?? {};
|
|
46
|
+
const dev = config.dev ?? {};
|
|
47
|
+
const server = config.server ?? {};
|
|
48
|
+
const devServer = api.context.devServer ?? {};
|
|
49
|
+
// Rsbuild resolves this to the LAN address it advertises when server.host is
|
|
50
|
+
// 0.0.0.0, which is the address a physical Lynx Go device can actually use.
|
|
51
|
+
const hostname = dev.client?.host || devServer.hostname || server.host || "";
|
|
52
|
+
const port = devServer.port ?? server.port ?? "";
|
|
53
|
+
const protocol = devServer.https ? "https" : "http";
|
|
54
|
+
// At this point in the pipeline Rsbuild hasn't started the dev server yet,
|
|
55
|
+
// so `dev.assetPrefix` (when it's the default, host-derived one — see
|
|
56
|
+
// @lynx-js/rsbuild-plugin) still contains the literal, unsubstituted
|
|
57
|
+
// "<port>" placeholder it's only resolved to a real port number later, in
|
|
58
|
+
// its own `printUrls` callback. `hostname`/`port` above are already the
|
|
59
|
+
// real values, though, so resolve the placeholder the same way that
|
|
60
|
+
// callback does rather than trusting assetPrefix as pre-resolved.
|
|
61
|
+
const assetPrefix = (typeof dev.assetPrefix === "string" ? dev.assetPrefix : "/").replaceAll(
|
|
62
|
+
"<port>",
|
|
63
|
+
String(port),
|
|
64
|
+
);
|
|
65
|
+
const base = /^https?:\/\//.test(assetPrefix)
|
|
66
|
+
? assetPrefix
|
|
67
|
+
: `${protocol}://${hostname}${port ? `:${port}` : ""}${assetPrefix}`;
|
|
68
|
+
const bundleUrl = new URL(`${entryName}.bundle`, base.endsWith("/") ? base : `${base}/`).toString();
|
|
69
|
+
const params = new URLSearchParams({
|
|
70
|
+
hostname,
|
|
71
|
+
port: String(port),
|
|
72
|
+
pathname: "/rsbuild-hmr",
|
|
73
|
+
protocol: devServer.https ? "wss" : "ws",
|
|
74
|
+
"bundle-url": bundleUrl,
|
|
75
|
+
});
|
|
76
|
+
if (environment.webSocketToken) params.set("token", environment.webSocketToken);
|
|
77
|
+
return params.toString();
|
|
78
|
+
}
|
|
79
|
+
|
|
30
80
|
function findSibling(dir, candidates) {
|
|
31
81
|
for (const name of candidates) {
|
|
32
82
|
const candidate = path.join(dir, name);
|
|
@@ -59,13 +109,37 @@ function packageRootOf(resolvedFile) {
|
|
|
59
109
|
|
|
60
110
|
export function pluginMithrilLynx(options = {}) {
|
|
61
111
|
const targetSdkVersion = options.targetSdkVersion ?? "3.5";
|
|
112
|
+
const hmr = options.hmr ?? false;
|
|
62
113
|
|
|
63
114
|
return {
|
|
64
115
|
name: PLUGIN_NAME,
|
|
65
116
|
setup(api) {
|
|
66
117
|
// Keep the template plugin discoverable by Rspeedy's Lynx internals.
|
|
67
118
|
api.expose(Symbol.for("LynxTemplatePlugin"), { LynxTemplatePlugin });
|
|
68
|
-
|
|
119
|
+
|
|
120
|
+
// setupApp()'s render model has no per-module "accept and patch"
|
|
121
|
+
// story (rendering is driven by native __RenderPage/__UpdatePage
|
|
122
|
+
// events, not by re-executing a hot-swapped module) -- module-level
|
|
123
|
+
// HMR's eval'd *.hot-update.js chunks also aren't runtime-wrapped
|
|
124
|
+
// the way the real background.js bundle is, and fail native-side
|
|
125
|
+
// with "ReferenceError: exports is not defined" if hot is left on.
|
|
126
|
+
// Force dev.hmr off (unless the app explicitly set it) so the dev
|
|
127
|
+
// client's ok() handler takes its other branch -- a full native
|
|
128
|
+
// Page.reload -- which is the one live-reload path this framework
|
|
129
|
+
// actually supports cleanly. dev.liveReload stays at its default.
|
|
130
|
+
api.modifyRsbuildConfig({
|
|
131
|
+
// Not a plain default: Rsbuild has already stamped dev.hmr:true onto
|
|
132
|
+
// the config by the time ANY hook sees it (even api.getRsbuildConfig
|
|
133
|
+
// ("original")), so there's no reliable way to tell "the app asked for
|
|
134
|
+
// hot module replacement" apart from "Rsbuild defaulted it" -- this
|
|
135
|
+
// always wins, with an explicit opt-out via pluginMithrilLynx({ hmr })
|
|
136
|
+
// for anyone who's fixed up their own app-level accept() story and the
|
|
137
|
+
// RuntimeWrapperWebpackPlugin gap noted below.
|
|
138
|
+
handler: (config, { mergeRsbuildConfig }) => mergeRsbuildConfig(config, { dev: { hmr } }),
|
|
139
|
+
order: "post",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
api.modifyBundlerChain((chain, { isDev, environment }) => {
|
|
69
143
|
// mithril-lynx's own src/lynx-mithril-shim.js deep-imports mithril's
|
|
70
144
|
// internal render/cachedAttrsIsStaticMap.js (and its emptyAttrs
|
|
71
145
|
// singleton). If the app's own `require("mithril")` resolves to a
|
|
@@ -137,12 +211,30 @@ export function pluginMithrilLynx(options = {}) {
|
|
|
137
211
|
const bgAsset = `.rspeedy/${name}/background.js`;
|
|
138
212
|
const mtAsset = `.rspeedy/${name}/main-thread.js`;
|
|
139
213
|
const hasBackground = bgSource != null;
|
|
214
|
+
// In dev, always materialize a background chunk — even for an app
|
|
215
|
+
// with no background.ts of its own — so live reload's client has
|
|
216
|
+
// somewhere to run. In production, keep the original behavior
|
|
217
|
+
// exactly (no background chunk at all when the app doesn't use one).
|
|
218
|
+
const includeBackground = hasBackground || isDev;
|
|
140
219
|
|
|
141
220
|
// Each entry always has main-thread code and may opt into a
|
|
142
221
|
// background thread by adding a sibling background.ts file.
|
|
143
|
-
if (
|
|
222
|
+
if (includeBackground) {
|
|
223
|
+
// Imported by its resolved absolute path (plus the query string
|
|
224
|
+
// the client reads its config from) rather than through the bare
|
|
225
|
+
// "mithril-lynx/dev-reload-client" specifier + an alias: the
|
|
226
|
+
// package-wide "mithril-lynx" prefix alias set above matches that
|
|
227
|
+
// specifier first regardless of registration order (webpack/
|
|
228
|
+
// rspack's resolve.alias picks the first matching entry, not the
|
|
229
|
+
// most specific one), which broke the import entirely.
|
|
230
|
+
const bgImports = isDev
|
|
231
|
+
? [
|
|
232
|
+
`${DEV_RELOAD_CLIENT_PATH}?${createDevReloadClientQuery(api, environment, name)}`,
|
|
233
|
+
...(hasBackground ? [bgSource] : []),
|
|
234
|
+
]
|
|
235
|
+
: bgSource;
|
|
144
236
|
chain.entry(bgEntry).add({
|
|
145
|
-
import:
|
|
237
|
+
import: bgImports,
|
|
146
238
|
filename: bgAsset,
|
|
147
239
|
});
|
|
148
240
|
}
|
|
@@ -157,14 +249,14 @@ export function pluginMithrilLynx(options = {}) {
|
|
|
157
249
|
...LynxTemplatePlugin.defaultOptions,
|
|
158
250
|
filename: `${name}.bundle`,
|
|
159
251
|
intermediate: `.rspeedy/${name}`,
|
|
160
|
-
chunks:
|
|
252
|
+
chunks: includeBackground ? [bgEntry, mtEntry] : [mtEntry],
|
|
161
253
|
dsl: "react_nodiff",
|
|
162
254
|
targetSdkVersion,
|
|
163
255
|
cssPlugins: [],
|
|
164
256
|
},
|
|
165
257
|
]);
|
|
166
258
|
|
|
167
|
-
if (
|
|
259
|
+
if (includeBackground) {
|
|
168
260
|
// Background chunks run in the JavaScript thread and need the
|
|
169
261
|
// Lynx runtime wrapper; main-thread chunks are encoded as lepus.
|
|
170
262
|
chain.plugin(`runtime-wrapper-${name}`).use(
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Development-only client for Lynx Go.
|
|
2
|
+
//
|
|
3
|
+
// Mithril view code runs in the main-thread/Lepus bundle, while WebSocket and
|
|
4
|
+
// Native Module access only exist in the background JS bundle. Consequently
|
|
5
|
+
// normal module HMR cannot patch the view code. On a successful rebuild this
|
|
6
|
+
// client asks Lynx Go's ExplorerModule to load the same bundle URL again.
|
|
7
|
+
|
|
8
|
+
function parseResourceQuery(query) {
|
|
9
|
+
const values = {};
|
|
10
|
+
if (typeof query !== "string" || !query.startsWith("?")) return values;
|
|
11
|
+
for (const pair of query.slice(1).split("&")) {
|
|
12
|
+
const index = pair.indexOf("=");
|
|
13
|
+
const key = index === -1 ? pair : pair.slice(0, index);
|
|
14
|
+
const value = index === -1 ? "" : pair.slice(index + 1);
|
|
15
|
+
values[key] = decodeURIComponent(value);
|
|
16
|
+
}
|
|
17
|
+
return values;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function socketURL(options) {
|
|
21
|
+
const hostname = options.hostname || "";
|
|
22
|
+
const port = options.port ? `:${options.port}` : "";
|
|
23
|
+
const pathname = options.pathname || "/rsbuild-hmr";
|
|
24
|
+
const token = options.token ? `?token=${encodeURIComponent(options.token)}` : "";
|
|
25
|
+
return `${options.protocol || "ws"}://${hostname}${port}${pathname}${token}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const options = parseResourceQuery(__resourceQuery);
|
|
29
|
+
const bundleUrl = options["bundle-url"];
|
|
30
|
+
let currentHash;
|
|
31
|
+
let initialBuild = true;
|
|
32
|
+
let socket;
|
|
33
|
+
// ExplorerModule.openSchema(url) makes Lynx Go fully reload — including
|
|
34
|
+
// re-executing this very module from scratch. That would naturally replace
|
|
35
|
+
// this stale instance's socket with a fresh one, except the OLD connection
|
|
36
|
+
// is otherwise left open: confirmed on-device that every earlier reload's
|
|
37
|
+
// socket stays alive and keeps receiving server broadcasts, so a build a few
|
|
38
|
+
// reloads in fires openSchema several times at once (one call per still-open
|
|
39
|
+
// stale socket), and the resulting navigations race and clobber each other —
|
|
40
|
+
// only the first reload after a fresh QR/URL load (a single open socket)
|
|
41
|
+
// reliably lands. Closing this socket ourselves right before reloading, and
|
|
42
|
+
// suppressing the reconnect-on-close it would otherwise trigger, keeps
|
|
43
|
+
// exactly one socket alive at a time.
|
|
44
|
+
let reloading = false;
|
|
45
|
+
|
|
46
|
+
console.info(
|
|
47
|
+
"[mithril-lynx] ExplorerModule.openSchema:",
|
|
48
|
+
typeof NativeModules !== "undefined" && typeof NativeModules.ExplorerModule?.openSchema,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
function reload() {
|
|
52
|
+
const openSchema =
|
|
53
|
+
typeof NativeModules !== "undefined" && NativeModules.ExplorerModule?.openSchema;
|
|
54
|
+
if (typeof openSchema !== "function") {
|
|
55
|
+
console.warn("[mithril-lynx] Live reload unavailable: ExplorerModule.openSchema was not found.");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!bundleUrl) {
|
|
59
|
+
console.warn("[mithril-lynx] Live reload unavailable: the bundle URL was not configured.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.info("[mithril-lynx] Reloading updated bundle.");
|
|
63
|
+
reloading = true;
|
|
64
|
+
socket?.close();
|
|
65
|
+
openSchema.call(NativeModules.ExplorerModule, bundleUrl);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleMessage(rawMessage) {
|
|
69
|
+
let message;
|
|
70
|
+
try {
|
|
71
|
+
message = JSON.parse(rawMessage);
|
|
72
|
+
} catch {
|
|
73
|
+
console.warn("[mithril-lynx] Ignoring an invalid dev-server message.");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
switch (message.type) {
|
|
78
|
+
case "hash":
|
|
79
|
+
currentHash = message.data;
|
|
80
|
+
break;
|
|
81
|
+
case "ok":
|
|
82
|
+
// The server sends hash + ok immediately after connecting. Reloading
|
|
83
|
+
// then would loop forever, so only act after a later successful build.
|
|
84
|
+
if (initialBuild) initialBuild = false;
|
|
85
|
+
else if (currentHash) reload();
|
|
86
|
+
break;
|
|
87
|
+
case "still-ok":
|
|
88
|
+
console.info("[mithril-lynx] Nothing changed.");
|
|
89
|
+
break;
|
|
90
|
+
case "warnings":
|
|
91
|
+
console.warn("[mithril-lynx] Build completed with warnings.", message.data);
|
|
92
|
+
if (!message.params?.preventReloading && !initialBuild) reload();
|
|
93
|
+
break;
|
|
94
|
+
case "errors":
|
|
95
|
+
console.warn("[mithril-lynx] Build failed; waiting for the next successful build.", message.data);
|
|
96
|
+
break;
|
|
97
|
+
case "invalid":
|
|
98
|
+
console.info("[mithril-lynx] App updated. Recompiling...");
|
|
99
|
+
break;
|
|
100
|
+
case "error":
|
|
101
|
+
console.error("[mithril-lynx] Dev server error:", message.data);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function connect(retries = 0) {
|
|
107
|
+
socket = new WebSocket(socketURL(options));
|
|
108
|
+
socket.onmessage = (event) => handleMessage(event.data);
|
|
109
|
+
socket.onerror = (error) => console.error("[mithril-lynx] Dev server connection error:", error);
|
|
110
|
+
socket.onclose = () => {
|
|
111
|
+
if (reloading) return; // Deliberately closed by reload() — a fresh socket is on its way already.
|
|
112
|
+
if (retries >= 10) {
|
|
113
|
+
console.error("[mithril-lynx] Unable to reconnect to the dev server.");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const delay = 1000 * 2 ** retries + Math.random() * 100;
|
|
117
|
+
console.info("[mithril-lynx] Dev server disconnected; reconnecting...");
|
|
118
|
+
setTimeout(() => connect(retries + 1), delay);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
connect();
|
package/src/lynx-mithril-shim.js
CHANGED
|
@@ -353,7 +353,15 @@ DIRECT_PROPS.forEach(function (p) {
|
|
|
353
353
|
|
|
354
354
|
LynxNodeWrapper.prototype._setDirectProp = function (key, value) {
|
|
355
355
|
if (key === "className") {
|
|
356
|
-
|
|
356
|
+
// "" (not undefined) when clearing: real hardware's FiberSetClasses
|
|
357
|
+
// rejects a non-string argument outright ("FiberSetClasses param 1
|
|
358
|
+
// should be String"), confirmed on device — a genuinely native-only
|
|
359
|
+
// validation this shim's own jsdom-backed test mock never catches,
|
|
360
|
+
// since ElementPAPI's __SetClasses(e, cls) is just `e.className = cls`
|
|
361
|
+
// and happily accepts undefined. Found via mithril-lynx-ui's FeedList
|
|
362
|
+
// (a native-list cell recycled between content that has a "class"
|
|
363
|
+
// attr and content that doesn't).
|
|
364
|
+
__SetClasses(this._handle, value == null ? "" : String(value))
|
|
357
365
|
} else if (key === "id") {
|
|
358
366
|
__SetID(this._handle, value == null ? null : String(value))
|
|
359
367
|
} else if (key === "checked") {
|
|
@@ -365,7 +373,8 @@ LynxNodeWrapper.prototype._setDirectProp = function (key, value) {
|
|
|
365
373
|
|
|
366
374
|
LynxNodeWrapper.prototype.setAttribute = function (key, value) {
|
|
367
375
|
if (key === "class") {
|
|
368
|
-
|
|
376
|
+
// See _setDirectProp's own comment above: "" not undefined.
|
|
377
|
+
__SetClasses(this._handle, value == null ? "" : String(value))
|
|
369
378
|
} else if (key === "id") {
|
|
370
379
|
__SetID(this._handle, value == null ? null : String(value))
|
|
371
380
|
} else if (key.slice(0, 5) === "data-") {
|
|
@@ -377,7 +386,8 @@ LynxNodeWrapper.prototype.setAttribute = function (key, value) {
|
|
|
377
386
|
|
|
378
387
|
LynxNodeWrapper.prototype.removeAttribute = function (key) {
|
|
379
388
|
if (key === "class") {
|
|
380
|
-
|
|
389
|
+
// See _setDirectProp's own comment above: "" not undefined.
|
|
390
|
+
__SetClasses(this._handle, "")
|
|
381
391
|
} else if (key === "id") {
|
|
382
392
|
__SetID(this._handle, null)
|
|
383
393
|
} else if (key.slice(0, 5) === "data-") {
|