mithril-lynx 0.0.2 → 0.0.4
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 +35 -1
- package/package.json +2 -2
- package/plugin.js +50 -0
package/README.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Mithril.js rendered through [Lynx](https://lynxjs.org)'s Element PAPI — the core runtime layer of a Mithril-based alternative to [`@lynx-js/react`](https://lynxjs.org/react/).
|
|
4
4
|
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm create mithril-lynx@latest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Scaffolds a new app via [`create-mithril-lynx`](https://github.com/carlos-sweb/create-mithril-lynx) — pick **Hello World** (a tap-to-animate logo demo, the Mithril analog of Lynx's own React hello-world), **Blank** (a single line of text, nothing else), or **Basic Activity** (two screens wired with `mithril-lynx/navigation`, confirmed working end to end on a real device), in TypeScript or JavaScript. That's the fastest way to a running app; the rest of this README documents the framework itself for once you're inside one.
|
|
12
|
+
|
|
5
13
|
## What this package is
|
|
6
14
|
|
|
7
15
|
`src/lynx-mithril-shim.js` is a contract-complete, line-by-line port of `mithril/render/render.js@2.3.8`: the exact same diff algorithm (`createNode`/`updateNodes`/`updateNode`/keyed-diff-with-LIS/etc.) as upstream Mithril, with every DOM call it makes redirected onto Lynx's Element PAPI (`__CreateView`, `__AppendElement`, `__SetAttribute`, `__SetInlineStyles`, `__AddEventListener`, ...) instead of the browser DOM. See `CONTRACT.md` for the exhaustive, reverse-engineered spec of exactly which DOM surface Mithril's renderer touches — that document is the reference this shim is built and validated against.
|
|
@@ -35,7 +43,7 @@ engine.addEventListener("__RenderPage", () => {
|
|
|
35
43
|
});
|
|
36
44
|
```
|
|
37
45
|
|
|
38
|
-
Subsequent UI updates flow through `shim.redraw()` (
|
|
46
|
+
Subsequent UI updates flow through `shim.redraw()` — **never plain `m.redraw()`**, which is a no-op in this shim-based architecture, and **never automatically after an `on*` handler either**: every event the shim hands to a handler is normalized with `redraw: false` on purpose (confirmed on real hardware 2026-09-10 — an `ontap` handler that only mutates local main-thread state, with no round trip through a data-channel/renderer-mode push, silently doesn't repaint until `shim.redraw()` is called from inside the handler itself; no error is thrown, since Mithril's own `EventDict.handleEvent` just skips the auto-redraw when `ev.redraw === false`). This is deliberate — a real DOM redraws cheaply enough that auto-redraw-per-event is a reasonable default there; a full Lynx PAPI diff pass on every touch event is not something to default to. `mithril-lynx/navigation`'s `push`/`pop`/`replace` already do this internally (see its source), which is why call sites using only that module never need to think about it.
|
|
39
47
|
|
|
40
48
|
## Usage — data-channel mode
|
|
41
49
|
|
|
@@ -235,6 +243,32 @@ Built on `shim.redraw()` alone, so it works unmodified in all three rendering mo
|
|
|
235
243
|
|
|
236
244
|
**Deliberately out of scope for v1**: screen transition animations (left entirely to the app's own CSS/styling on whatever wraps `nav.Navigator`), and hardware back-button integration (no documented Lynx PAPI hook for it was found — wire a screen's own back-affordance to `nav.pop()` instead, as in the example above).
|
|
237
245
|
|
|
246
|
+
## Custom fonts
|
|
247
|
+
|
|
248
|
+
Use a plain CSS `@font-face` rule — not `lynx.addFont()`. That JS API (background-thread-only) is for loading a font dynamically *after* mount, matching [`lynx-family/lynx-examples`](https://github.com/lynx-family/lynx-examples)'s own `examples/text/src/custom_font`, which calls it from `componentDidMount` and re-renders via `setState` once its callback fires. For a font known at build time (the common case), `examples/text/src/font_face` is the pattern to copy — a declarative `@font-face`, no JS:
|
|
249
|
+
|
|
250
|
+
```css
|
|
251
|
+
@font-face {
|
|
252
|
+
font-family: "Ubuntu Mono";
|
|
253
|
+
src: url("./assets/fonts/ubuntu-mono-400.ttf");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@font-face {
|
|
257
|
+
font-family: "Ubuntu Mono";
|
|
258
|
+
font-weight: 700;
|
|
259
|
+
src: url("./assets/fonts/ubuntu-mono-700.ttf");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
:root {
|
|
263
|
+
font-family: "Ubuntu Mono"; /* needs enableCSSInheritance, see below */
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Two gotchas, both confirmed on real hardware 2026-09-10:
|
|
268
|
+
|
|
269
|
+
- **The font file must be `.ttf`, not `.woff2`.** 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.** `@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
|
+
|
|
238
272
|
## Known permanent gaps
|
|
239
273
|
|
|
240
274
|
- `m.trust` / innerHTML vnodes — no Lynx PAPI equivalent to raw innerHTML injection.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mithril-lynx",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Mithril.js rendered through Lynx's Element PAPI
|
|
3
|
+
"version": "0.0.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",
|
|
7
7
|
"repository": {
|
package/plugin.js
CHANGED
|
@@ -35,6 +35,28 @@ function findSibling(dir, candidates) {
|
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Walks up from a resolved file to the root of the package that owns it,
|
|
40
|
+
* verified by name rather than assumed from the layout — this package's
|
|
41
|
+
* exports map deliberately doesn't expose ./package.json, so the usual
|
|
42
|
+
* require.resolve("<pkg>/package.json") trick isn't available here.
|
|
43
|
+
*/
|
|
44
|
+
function packageRootOf(resolvedFile) {
|
|
45
|
+
let dir = path.dirname(resolvedFile);
|
|
46
|
+
for (let i = 0; i < 10; i++) {
|
|
47
|
+
try {
|
|
48
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
49
|
+
if (pkg.name === "mithril-lynx") return dir;
|
|
50
|
+
} catch {
|
|
51
|
+
// keep walking
|
|
52
|
+
}
|
|
53
|
+
const parent = path.dirname(dir);
|
|
54
|
+
if (parent === dir) break;
|
|
55
|
+
dir = parent;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
38
60
|
export function pluginMithrilLynx(options = {}) {
|
|
39
61
|
const targetSdkVersion = options.targetSdkVersion ?? "3.5";
|
|
40
62
|
|
|
@@ -69,6 +91,34 @@ export function pluginMithrilLynx(options = {}) {
|
|
|
69
91
|
// leave resolution as-is rather than guessing.
|
|
70
92
|
}
|
|
71
93
|
|
|
94
|
+
// Same class of problem, worse symptom: mithril-lynx itself keeps
|
|
95
|
+
// per-app state in module-level variables — the shim's rootWrapper/
|
|
96
|
+
// redraw/runRender, main-thread.js's latestData and its cross-thread
|
|
97
|
+
// handler maps, background.js's mirror of those. Two physical copies
|
|
98
|
+
// means two disconnected renderers: the app renders through one, and
|
|
99
|
+
// any LIBRARY that depends on mithril-lynx (a component library, say,
|
|
100
|
+
// resolving its own nested copy once linked) calls shim.redraw() on
|
|
101
|
+
// the other — whose `redraw` is still null. That's a silent no-op:
|
|
102
|
+
// no error, nothing logged, components simply never update. Confirmed
|
|
103
|
+
// on real hardware 2026-09-11 while building mithril-lynx-ui, where
|
|
104
|
+
// it read as "the animation just doesn't run".
|
|
105
|
+
try {
|
|
106
|
+
const appRequire = createRequire(path.join(process.cwd(), "package.json"));
|
|
107
|
+
const selfDir = packageRootOf(appRequire.resolve("mithril-lynx"));
|
|
108
|
+
if (selfDir != null) {
|
|
109
|
+
// The bare specifier has to point at the entry FILE: aliasing it
|
|
110
|
+
// to the directory would bypass this package's own exports map
|
|
111
|
+
// (which has no "main" to fall back on) and fail to resolve.
|
|
112
|
+
// The prefix alias then keeps subpaths — "mithril-lynx/main-thread"
|
|
113
|
+
// and friends, which hold state of their own — on that same copy.
|
|
114
|
+
chain.resolve.alias.set("mithril-lynx$", path.join(selfDir, "src", "lynx-mithril-shim.js"));
|
|
115
|
+
chain.resolve.alias.set("mithril-lynx", selfDir);
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// App doesn't resolve mithril-lynx from its own root (it's being
|
|
119
|
+
// consumed some other way) — leave resolution alone.
|
|
120
|
+
}
|
|
121
|
+
|
|
72
122
|
const rawEntries = Object.entries(chain.entryPoints.entries() ?? {});
|
|
73
123
|
chain.entryPoints.clear();
|
|
74
124
|
|