mithril-lynx 0.0.4 → 0.0.6
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 +6 -3
- package/element.js +5 -1
- package/package.json +1 -1
- package/src/lynx-mithril-shim.js +43 -15
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
|
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.6",
|
|
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/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-") {
|
|
@@ -696,23 +706,41 @@ function factory() {
|
|
|
696
706
|
|
|
697
707
|
function initComponent(vnode, hooks) {
|
|
698
708
|
var sentinel
|
|
699
|
-
|
|
709
|
+
var classStyle = typeof vnode.tag.view !== "function"
|
|
710
|
+
if (!classStyle) {
|
|
700
711
|
vnode.state = Object.create(vnode.tag)
|
|
701
712
|
sentinel = vnode.state.view
|
|
702
|
-
if (sentinel.$$reentrantLock$$ != null) return
|
|
703
|
-
sentinel.$$reentrantLock$$ = true
|
|
704
713
|
} else {
|
|
705
|
-
vnode.state = void 0
|
|
706
714
|
sentinel = vnode.tag
|
|
707
|
-
if (sentinel.$$reentrantLock$$ != null) return
|
|
708
|
-
sentinel.$$reentrantLock$$ = true
|
|
709
|
-
vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode)
|
|
710
715
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
+
if (sentinel.$$reentrantLock$$ != null) return
|
|
717
|
+
sentinel.$$reentrantLock$$ = true
|
|
718
|
+
// The lock above is stored on `sentinel` — the component's OWN shared
|
|
719
|
+
// view function (or, for class-style components, the tag itself), NOT
|
|
720
|
+
// on this vnode instance — so it's shared by EVERY use of this
|
|
721
|
+
// component anywhere in the app. Without the try/finally below, a
|
|
722
|
+
// view() (or, for a class-style component, its CONSTRUCTOR) that
|
|
723
|
+
// throws on a component's first render (e.g. a fail-loudly validation
|
|
724
|
+
// error) skips the `sentinel.$$reentrantLock$$ = null` reset entirely,
|
|
725
|
+
// leaving that lock stuck at `true` forever. Every later attempt to
|
|
726
|
+
// create that SAME component type — anywhere, not just the one
|
|
727
|
+
// instance that threw — then silently no-ops right here
|
|
728
|
+
// (`if (sentinel.$$reentrantLock$$ != null) return`): no render, no
|
|
729
|
+
// error, nothing. Found via mithril-lynx-ui's own test suite: a
|
|
730
|
+
// component whose view() deliberately throws to validate a prop,
|
|
731
|
+
// exercised in one test, made every SUBSEQUENT test mounting that
|
|
732
|
+
// same component silently render nothing, no matter how unrelated.
|
|
733
|
+
try {
|
|
734
|
+
if (classStyle) {
|
|
735
|
+
vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode)
|
|
736
|
+
}
|
|
737
|
+
initLifecycle(vnode.state, vnode, hooks)
|
|
738
|
+
if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks)
|
|
739
|
+
vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode))
|
|
740
|
+
if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument")
|
|
741
|
+
} finally {
|
|
742
|
+
sentinel.$$reentrantLock$$ = null
|
|
743
|
+
}
|
|
716
744
|
}
|
|
717
745
|
|
|
718
746
|
function createComponent(parent, vnode, hooks, ns, nextSibling) {
|