mithril-lynx 0.0.9 → 2.0.0
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/.omo/plans/m-request-fetch-lynx.md +306 -0
- package/.omo/plans/m-route-en-memoria.md +397 -0
- package/.omo/plans/mithril-lynx-v2-desde-cero.md +548 -0
- package/FETCH_INVESTIGATION.md +307 -0
- package/README.md +32 -302
- package/REQUEST.md +71 -0
- package/ROUTE.md +71 -0
- package/package.json +24 -80
- package/plugin.d.ts +4 -33
- package/plugin.js +108 -438
- package/rstest.config.ts +27 -0
- package/src/apply-patch.js +179 -0
- package/src/backends/virtual-backend.js +80 -0
- package/src/background.d.ts +11 -0
- package/src/background.js +79 -0
- package/src/channel.js +41 -0
- package/src/commit.js +67 -0
- package/src/dev-reload-client.js +171 -187
- package/src/dev-transport-noop.js +10 -0
- package/src/fake-dom.js +374 -0
- package/src/main-thread.d.ts +1 -0
- package/src/main-thread.js +68 -0
- package/src/mount-redraw.js +67 -0
- package/src/patch-protocol.js +40 -0
- package/src/reload/version.js +28 -0
- package/src/request.d.ts +37 -0
- package/src/request.js +181 -0
- package/src/route.d.ts +33 -0
- package/src/route.js +207 -0
- package/test/end-to-end.test.ts +86 -0
- package/test/reload-version.test.ts +17 -0
- package/test/request.test.ts +182 -0
- package/test/route-hot-reload.test.ts +40 -0
- package/test/route.test.ts +152 -0
- package/test/setup.ts +25 -0
- package/test/structural-reload.test.ts +95 -0
- package/CONTRACT.md +0 -151
- package/LICENSE +0 -21
- package/background.d.ts +0 -54
- package/background.js +0 -169
- package/element.d.ts +0 -34
- package/element.js +0 -83
- package/gesture.d.ts +0 -40
- package/gesture.js +0 -117
- package/internal/constants.js +0 -26
- package/internal/virtual-node.js +0 -388
- package/list.d.ts +0 -31
- package/list.js +0 -185
- package/main-thread.d.ts +0 -43
- package/main-thread.js +0 -165
- package/navigation.d.ts +0 -35
- package/navigation.js +0 -76
- package/renderer/background.d.ts +0 -21
- package/renderer/background.js +0 -84
- package/renderer/main-thread.d.ts +0 -12
- package/renderer/main-thread.js +0 -175
- package/src/lynx-mithril-shim.d.ts +0 -16
- package/src/lynx-mithril-shim.js +0 -1505
- package/src/worklet-runtime.js +0 -82
- package/testing.d.ts +0 -10
- package/testing.js +0 -91
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# Plan — `m.route` para mithril-lynx-v2: navegación en memoria, sin APIs de navegador
|
|
2
|
+
|
|
3
|
+
> Estado (2026-09-17): **Plan completo — F0–F6 hechos y verificados.**
|
|
4
|
+
> F1–F3 con `rstest` (PAPI real, sin mocks, 9/9 tests). F4 y F6 en device
|
|
5
|
+
> real con una demo de 2 pantallas (`mithril-lynx-v2-app`), verificados
|
|
6
|
+
> mirando los ops del patch real (no el `nodeId` de CDP — ver la nota
|
|
7
|
+
> metodológica en §10, un hallazgo real de esta sesión). F5 (botón atrás
|
|
8
|
+
> nativo) queda con la respuesta de F0 (no hay evento expuesto al JS) sin
|
|
9
|
+
> una forma de confirmarlo/descartarlo más a fondo en este device.
|
|
10
|
+
> Pendiente no bloqueante: `mithril-runtime@1.1.0` (con
|
|
11
|
+
> `pathname/`/`querystring/` vendorizados) está pusheado a GitHub pero el
|
|
12
|
+
> `npm publish` pide un OTP que solo el usuario puede aprobar — mientras
|
|
13
|
+
> tanto `mithril-lynx-v2` y `mithril-lynx-v2-app` dependen de
|
|
14
|
+
> `file:/home/sweb/mithril-runtime` en vez de `^1.1.0`. Ver §10 al final
|
|
15
|
+
> para el detalle exacto.
|
|
16
|
+
> Idioma: español (consistente con el resto de planes de este proyecto).
|
|
17
|
+
> Insumo de las 3 tareas solicitadas: (1) cómo navega React en Lynx, (2)
|
|
18
|
+
> cómo navega Vue en Lynx, (3) diseño de nuestro propio `m.route`
|
|
19
|
+
> respetando la API de Mithril. Investigación vía WebSearch/WebFetch contra
|
|
20
|
+
> `lynxjs.org`, `vue.lynxjs.org`, y el repo `lynx-family/lynx` en GitHub —
|
|
21
|
+
> más lectura directa del `m.route` real en
|
|
22
|
+
> `node_modules/mithril/{route.js,api/router.js,pathname/}` (mithril 2.3.8,
|
|
23
|
+
> el mismo que `mithril-runtime` ya usa como base).
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 0. Por qué esto es un plan aparte (no parte del core ya construido)
|
|
28
|
+
|
|
29
|
+
`mithril-lynx-v2` (F0–F6, ya hecho y verificado en device) deliberadamente
|
|
30
|
+
no incluye navegación — es un no-objetivo explícito del plan original
|
|
31
|
+
(`mithril-lynx-v2-desde-cero.md` §2). Este documento cubre exactamente eso,
|
|
32
|
+
como una pieza **opcional y separada** (igual que en v1, `navigation.js`
|
|
33
|
+
era un export aparte, no parte del core) — nunca se mezcla con el
|
|
34
|
+
commit/patch/reload que ya está resuelto.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 1. Tarea 1 — Cómo navega ReactLynx
|
|
39
|
+
|
|
40
|
+
**Mecanismo: React Router v6, con `MemoryRouter` (no `BrowserRouter`).**
|
|
41
|
+
|
|
42
|
+
```jsx
|
|
43
|
+
<MemoryRouter>
|
|
44
|
+
<Routes>
|
|
45
|
+
<Route path="/" element={<App />} />
|
|
46
|
+
<Route path="/home" element={<Home />} />
|
|
47
|
+
</Routes>
|
|
48
|
+
</MemoryRouter>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **Por qué `MemoryRouter`**: Lynx no tiene `window.location` ni History
|
|
52
|
+
API real — no hay URL bar, no hay "atrás" de navegador. `MemoryRouter`
|
|
53
|
+
mantiene el historial como un array en memoria, sin ninguna dependencia
|
|
54
|
+
de API de browser.
|
|
55
|
+
- **Navegación**: no hay `<Link>`/`<a>` (Lynx no tiene esos tags) — se usa
|
|
56
|
+
el hook `useNavigate()` disparado desde un `ontap`/`bindtap`:
|
|
57
|
+
```jsx
|
|
58
|
+
const nav = useNavigate();
|
|
59
|
+
<text bindtap={() => nav('/home')}>Ir a Home</text>
|
|
60
|
+
```
|
|
61
|
+
- **Params/ubicación**: `useParams()` (segmentos dinámicos `:id`),
|
|
62
|
+
`useLocation()` (pathname actual).
|
|
63
|
+
- **Alternativa moderna documentada**: TanStack Router, mismo motivo
|
|
64
|
+
explícito: *"Memory Routing is required due to browser History API
|
|
65
|
+
limitations in Lynx"*. File-based routing vía plugin de rspack
|
|
66
|
+
(`@tanstack/router-plugin/rspack`) es un añadido posterior, no cambia el
|
|
67
|
+
fondo (memoria, no browser).
|
|
68
|
+
- **Dato relevante**: hay un issue abierto y sin resolver en
|
|
69
|
+
`lynx-family/lynx` (#93, *"How to navigate between pages in native
|
|
70
|
+
apps?"*) preguntando específicamente por navegación "nativa" (¿bundles
|
|
71
|
+
separados? ¿Activities/ViewControllers nuevos?) — **sigue sin respuesta
|
|
72
|
+
oficial**. Confirma que el modelo "un solo bundle, router en memoria" es
|
|
73
|
+
el único camino documentado/soportado hoy — no hay alternativa nativa
|
|
74
|
+
resuelta que estemos dejando pasar.
|
|
75
|
+
|
|
76
|
+
## 2. Tarea 2 — Cómo navega Vue Lynx
|
|
77
|
+
|
|
78
|
+
**Mecanismo: Vue Router estándar, con `createMemoryHistory()`.**
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const router = createRouter({
|
|
82
|
+
history: createMemoryHistory(),
|
|
83
|
+
routes: [
|
|
84
|
+
{ path: '/', name: 'home', component: Home },
|
|
85
|
+
{ path: '/about', name: 'about', component: About },
|
|
86
|
+
{ path: '/users/:id', name: 'user-detail', component: UserDetail },
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
app.use(router);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- **Misma razón explícita**: *"since Lynx has no browser `window.location`
|
|
93
|
+
or History API, you must use `createMemoryHistory()` instead of
|
|
94
|
+
`createWebHistory()"`.* Historial como array en proceso, cero
|
|
95
|
+
dependencia de browser.
|
|
96
|
+
- **Navegación**: sin `<a>` — dos caminos: `RouterLink` con `custom` +
|
|
97
|
+
scoped slot (renderiza `<view>`/`<text>` propios de Lynx, expone
|
|
98
|
+
`isActive` + `navigate`), o programático vía `useRouter()` +
|
|
99
|
+
`router.push()`/`.back()`/`.replace()` en un handler de tap.
|
|
100
|
+
- **Display**: `<RouterView>` renderiza el componente matcheado;
|
|
101
|
+
`useRoute()` para params (`route.params.id`).
|
|
102
|
+
|
|
103
|
+
## 3. Patrón común extraído (la conclusión que importa para la Tarea 3)
|
|
104
|
+
|
|
105
|
+
React y Vue en Lynx **convergen exactamente en la misma arquitectura**,
|
|
106
|
+
independientemente del framework:
|
|
107
|
+
|
|
108
|
+
| Pieza | React en Lynx | Vue en Lynx | Conclusión para nosotros |
|
|
109
|
+
|---|---|---|---|
|
|
110
|
+
| Historial | `MemoryRouter` (array en memoria) | `createMemoryHistory()` (array en memoria) | Nuestro `m.route` NO debe tocar `window.history`/`popstate` — ninguno de los dos existe en Lynx |
|
|
111
|
+
| Modelo de página | Un solo bundle/página, "pantallas" = subárboles condicionales | Igual | Confirma que mithril-lynx-v2 tampoco necesita bundles separados por pantalla — un solo `renderApp()` de toda la vida de la app |
|
|
112
|
+
| Link/navegación | Sin `<a>`; hook + `ontap`/`bindtap` | Sin `<a>`; slot custom o hook + tap | `m.route.Link` debe re-diseñarse para emitir un elemento Lynx-nativo con `ontap`, no un `<a>` con `href`/`onclick` |
|
|
113
|
+
| Navegación "nativa" (multi-bundle) | Sin resolver, issue abierto sin respuesta | No mencionada | No hay un patrón oficial que estemos ignorando — el camino en-memoria es *el* camino |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 4. El contrato real de `m.route` (lo que hay que igualar para que "se sienta Mithril")
|
|
118
|
+
|
|
119
|
+
Leído directo de `mithril/route.js` + `mithril/api/router.js` (2.3.8, el
|
|
120
|
+
mismo mithril del que sale `mithril-runtime`):
|
|
121
|
+
|
|
122
|
+
| API | Firma | Uso interno real |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `m.route(root, defaultRoute, routes)` | monta `RouterRoot` en `root` vía `mountRedraw.mount()`, matchea rutas | `root` es un elemento DOM real — ver §5.2, esto cambia en v2 |
|
|
125
|
+
| `m.route.set(path, data, options)` | `pushState`/`replaceState` + `resolveRoute()` async | el `pushState` es lo único 100% browser-atado |
|
|
126
|
+
| `m.route.get()` | devuelve `currentPath` | puro, sin cambios |
|
|
127
|
+
| `m.route.prefix` | default `"#!"` | solo tiene sentido con URL real — ver §5.5 |
|
|
128
|
+
| `m.route.param(key)` | devuelve params de la ruta actual | puro, sin cambios |
|
|
129
|
+
| `m.route.Link` | componente que renderiza `<a href onclick>` | tiene que re-emitirse para Lynx — ver §5.4 |
|
|
130
|
+
| `m.route.SKIP` | sentinel para que `onmatch` pase a la siguiente ruta que matchee | puro, sin cambios |
|
|
131
|
+
| `onmatch(params, path, route)` | resolver async opcional por ruta (lazy-loading) | puro (Promise), sin cambios |
|
|
132
|
+
|
|
133
|
+
## 5. Decisiones de diseño para v2
|
|
134
|
+
|
|
135
|
+
### 5.1 Historial: array en memoria, no `window.history`/`popstate`
|
|
136
|
+
|
|
137
|
+
Un stack simple (`string[]` + índice actual) mantenido en un módulo interno
|
|
138
|
+
del router — exactamente lo que `MemoryRouter`/`createMemoryHistory()`
|
|
139
|
+
hacen por dentro. `route.set(path, data, options)` empuja o reemplaza en
|
|
140
|
+
ese array según `options.replace`, en vez de llamar
|
|
141
|
+
`history.pushState/replaceState`.
|
|
142
|
+
|
|
143
|
+
### 5.2 Mount: NO un segundo punto de montaje — `m.route` alimenta el ÚNICO `renderApp()`
|
|
144
|
+
|
|
145
|
+
Real Mithril permite (y de hecho normalmente lo hace) que `m.route()` haga
|
|
146
|
+
su propio `mountRedraw.mount(dom, RouterRoot)`, independiente de cualquier
|
|
147
|
+
otro `m.mount()` que la app tenga. **v2 no puede replicar esto tal cual**:
|
|
148
|
+
la arquitectura entera (plan original §3.1) es *un solo* `renderApp()` para
|
|
149
|
+
toda la vida de la app — dos puntos de montaje independientes romperían el
|
|
150
|
+
modelo de un solo commit-controller/un solo patch stream.
|
|
151
|
+
|
|
152
|
+
**Decisión**: `m.route` para v2 pierde el parámetro `root` (no hay un DOM
|
|
153
|
+
real al que apuntar) y en su lugar **es quien llama `renderApp()` por
|
|
154
|
+
vos**:
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
// en vez de: m.route(document.body, "/", routes) (mithril real)
|
|
158
|
+
import route from "mithril-lynx-v2/route";
|
|
159
|
+
const app = route("/", routes); // llama renderApp() adentro, devuelve el mismo handle {redraw, document}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Esto es una desviación real y documentada de la firma de mithril (3
|
|
163
|
+
argumentos → 2), justificada porque el primer argumento nunca tuvo sentido
|
|
164
|
+
en Lynx (no hay nodo al que montar) — no un intento de ocultarla. Todo lo
|
|
165
|
+
demás (`route.set/get/param/Link/SKIP`, sintaxis de rutas con `:param`)
|
|
166
|
+
queda idéntico.
|
|
167
|
+
|
|
168
|
+
### 5.3 Redraw: se reusa `performRender`, no un redraw propio del router
|
|
169
|
+
|
|
170
|
+
`resolveRoute()` real llama `mountRedraw.redraw()` tras el primer mount.
|
|
171
|
+
Para v2, el router recibe (internamente, al llamar `renderApp()` por
|
|
172
|
+
dentro) la MISMA función `performRender` que ya es el único camino de
|
|
173
|
+
redraw de toda la app (commit.js) — no hay un segundo mecanismo de redraw
|
|
174
|
+
compitiendo.
|
|
175
|
+
|
|
176
|
+
### 5.4 `route.Link`: rediseño obligatorio, sin `<a>`/`onclick`
|
|
177
|
+
|
|
178
|
+
Lynx no tiene `<a>` ni `onclick`. Mirroreando el patrón de React
|
|
179
|
+
(`ontap={() => nav(path)}`) y Vue (scoped slot con `navigate`):
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
route.Link = {
|
|
183
|
+
view(vnode) {
|
|
184
|
+
var selector = vnode.attrs.selector || "view";
|
|
185
|
+
var { selector: _s, options, params, href, ...rest } = vnode.attrs;
|
|
186
|
+
return hyperscript(selector, {
|
|
187
|
+
...rest,
|
|
188
|
+
ontap: function (e) {
|
|
189
|
+
if (rest.disabled) return;
|
|
190
|
+
route.set(buildPathname(href, params), null, options);
|
|
191
|
+
},
|
|
192
|
+
}, vnode.children);
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`disabled` se respeta (igual que la versión real), pero sin
|
|
198
|
+
`aria-disabled`/`href=null` (conceptos de accesibilidad web sin
|
|
199
|
+
equivalente directo documentado en Lynx todavía — marcar como gap
|
|
200
|
+
conocido, no bloqueante).
|
|
201
|
+
|
|
202
|
+
### 5.5 `route.prefix`: no-op de compatibilidad
|
|
203
|
+
|
|
204
|
+
No tiene sentido sin URL bar. Se deja como una propiedad asignable que no
|
|
205
|
+
hace nada (para no romper código portado de mithril real que hace
|
|
206
|
+
`m.route.prefix = ""` defensivamente), documentado explícitamente como
|
|
207
|
+
ignorado.
|
|
208
|
+
|
|
209
|
+
### 5.6 `route.SKIP` / `onmatch` async: se preserva sin cambios
|
|
210
|
+
|
|
211
|
+
Es JS puro (Promises), sin acoplamiento a browser — mismo comportamiento
|
|
212
|
+
que mithril real, incluido lazy-loading de componentes de ruta.
|
|
213
|
+
|
|
214
|
+
### 5.7 Botón "atrás" — pregunta abierta, no asumir una respuesta
|
|
215
|
+
|
|
216
|
+
Ni React ni Vue en Lynx documentan qué pasa con un botón/gesto de "atrás"
|
|
217
|
+
nativo del shell (Android back, gesto iOS). Esto es un spike (F0), no una
|
|
218
|
+
decisión ya tomada — ver §7.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 6. Qué se reutiliza sin tocar (validado, no es la parte riesgosa)
|
|
223
|
+
|
|
224
|
+
`mithril/pathname/{build.js,parse.js,compileTemplate.js}` (108 líneas
|
|
225
|
+
totales) — el compilador de templates de ruta (`:id`, `:file...`, etc.).
|
|
226
|
+
**Verificado por grep: cero referencias a `window`/`document`/`location`/
|
|
227
|
+
`history` en los tres archivos** — es exactamente el mismo tipo de pieza
|
|
228
|
+
"pura, no-browser" que ya justificó reusar `render/render.js` tal cual en
|
|
229
|
+
el core de v2. Se vendoriza sin modificar (mismo criterio que
|
|
230
|
+
`mithril-runtime`: no forkeamos algo que no está roto).
|
|
231
|
+
|
|
232
|
+
Importante: estos archivos **no están en `mithril-runtime`** — el script
|
|
233
|
+
de extracción nunca los copió (no estaban en el allowlist de
|
|
234
|
+
`update-from-mithril.sh`). Hay que agregarlos ahí (a `mithril-runtime`,
|
|
235
|
+
como una pieza más del "runtime puro sin browser") o vendorizarlos
|
|
236
|
+
directo en `mithril-lynx-v2/route.js` — decisión de F1.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 7. Fases
|
|
241
|
+
|
|
242
|
+
| Fase | Contenido | Criterio de salida | Device |
|
|
243
|
+
|---|---|---|---|
|
|
244
|
+
| **F0** | Spike: ¿Lynx expone un evento nativo de "back" (hardware/gesto) al hilo background? Revisar `lynx.getEngine()`/`lynx.getCoreContext()` en busca de un evento de lifecycle equivalente a `popstate`, probar en el device ya conectado. | Respuesta documentada con evidencia (logcat), no supuesta | Sí |
|
|
245
|
+
| **F1** | Vendorizar `pathname/*` (¿en `mithril-runtime` o directo en `mithril-lynx-v2`? decidir acá) + historial en memoria (array + índice) + `route.set/get/param/SKIP` sobre ese historial | Tests `rstest`: matching de `:param`, push/replace, `route.get()` correcto — sin device | No |
|
|
246
|
+
| **F2** | Integrar con `renderApp()`: `m.route(defaultRoute, routes)` como único punto de entrada, sin segundo mount | Test: navegar entre 2 rutas produce los ops de patch esperados (crear árbol nuevo, no acumular el viejo) | No |
|
|
247
|
+
| **F3** | `route.Link` rediseñado (`ontap`, sin `<a>`) | Test: tap en un Link navega, `disabled` bloquea el tap | No |
|
|
248
|
+
| **F4** | Verificación en device: navegar 2+ pantallas reales, confirmar con `uiautomator`/DevTool que la pantalla vieja se desmonta (sin huérfanos) | uiautomator/CDP tree limpio en cada paso | Sí |
|
|
249
|
+
| **F5** | Atrás nativo (según lo que F0 haya encontrado) — si existe evento, engancharlo a `route.set` con `replace`; si no existe, documentar que no hay atrás nativo y `route.Link`/botón explícito es el único camino | Depende de F0 | Sí |
|
|
250
|
+
| **F6** | Interacción con reload (A/B ya construidos): ¿cambiar de ruta durante un hot-update de datos/estructural sigue funcionando sin recrear todo el árbol? | Test + verificación en device combinando ambos | Sí |
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 8. No-objetivos
|
|
255
|
+
|
|
256
|
+
- Historial persistente entre reinicios de la app (un `Page.reload`/full
|
|
257
|
+
reload ya resetea todo — no se intenta preservar la ruta a través de
|
|
258
|
+
eso en esta fase).
|
|
259
|
+
- `route.prefix` funcional (hash o pathname reales) — no tiene sentido sin
|
|
260
|
+
URL bar; queda como no-op documentado (§5.5).
|
|
261
|
+
- Deep-linking (abrir la app directo en una ruta interna desde afuera) —
|
|
262
|
+
fuera de alcance, es un tema de intents/schemes nativos, no del router
|
|
263
|
+
en sí.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 9. Referencias
|
|
268
|
+
|
|
269
|
+
- ReactLynx routing: <https://lynxjs.org/react/routing/react-router>,
|
|
270
|
+
<https://lynxjs.org/4.0/react/routing/tanstack-router>
|
|
271
|
+
- Vue Lynx routing: <https://vue.lynxjs.org/guide/routing>
|
|
272
|
+
- Issue sin resolver sobre navegación nativa:
|
|
273
|
+
<https://github.com/lynx-family/lynx/issues/93>
|
|
274
|
+
- Contrato real de `m.route`: `node_modules/mithril/route.js`,
|
|
275
|
+
`node_modules/mithril/api/router.js`,
|
|
276
|
+
`node_modules/mithril/pathname/{build,parse,compileTemplate}.js`
|
|
277
|
+
(mithril 2.3.8 — mismo checkout que ya usa `mithril-runtime`).
|
|
278
|
+
- Arquitectura base sobre la que esto se integra:
|
|
279
|
+
`mithril-lynx-v2-desde-cero.md` (especialmente §3.1, un solo
|
|
280
|
+
`renderApp()`, y §3.4, el único punto de commit/redraw).
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## 10. Estado de ejecución
|
|
285
|
+
|
|
286
|
+
### F0 — respuesta parcial (estático, no en device)
|
|
287
|
+
|
|
288
|
+
`grep` sobre `lynx_core.js` instalado (indicadores-android) y el runtime
|
|
289
|
+
de ReactLynx (`@lynx-js/react`) no encontró **ningún** evento de "back"
|
|
290
|
+
nativo/hardware expuesto al hilo JS — solo `onAppEnterBackground`
|
|
291
|
+
(ciclo de vida de la app, no navegación). Consistente con que ni
|
|
292
|
+
`MemoryRouter` ni `createMemoryHistory()` escuchan algo así. **No
|
|
293
|
+
confirmado en vivo** — el device no estaba conectado en esta sesión.
|
|
294
|
+
`route.back()`/`route.forward()` ya están implementados sobre el stack en
|
|
295
|
+
memoria (§5.1); si F4 en device confirma que SÍ existe un evento nativo,
|
|
296
|
+
conectarlo a `route.back()` es trivial.
|
|
297
|
+
|
|
298
|
+
### F1–F3 — cerrados, verificados con `rstest` (5 tests nuevos, PAPI real)
|
|
299
|
+
|
|
300
|
+
- **`mithril-runtime@1.1.0`**: se agregaron `pathname/{build,parse,
|
|
301
|
+
compileTemplate}.js` + `querystring/{build,parse}.js`, copiados sin
|
|
302
|
+
modificar desde mithril 2.3.8 (verificado: cero referencias a
|
|
303
|
+
`window`/`document`/`location`/`history`). `update-from-mithril.sh`
|
|
304
|
+
actualizado para seguir sincronizándolos. Commiteado y pusheado a
|
|
305
|
+
GitHub (`5e35bd2`); **`npm publish` pendiente del OTP del usuario**.
|
|
306
|
+
- **`src/route.js`** (mithril-lynx-v2): implementado siguiendo §5
|
|
307
|
+
completo — historial en memoria, `route(defaultRoute, routes)` sin
|
|
308
|
+
parámetro `root` (llama `renderApp()` adentro, una sola vez, mismo
|
|
309
|
+
patrón `hasBeenResolved` que el `api/router.js` real), `route.set/get/
|
|
310
|
+
param/SKIP` idénticos en comportamiento a mithril real, `onmatch`
|
|
311
|
+
async preservado, `route.Link` reescrito con `ontap` (sin `<a>`/
|
|
312
|
+
`onclick`), `route.prefix` no-op documentado, y `route.back()/
|
|
313
|
+
forward()` nuevos (no existen en mithril real — necesarios porque acá
|
|
314
|
+
no hay botón de navegador que dispare `popstate`).
|
|
315
|
+
- **`test/route.test.ts`** (5 tests, todos contra PAPI real vía
|
|
316
|
+
`@lynx-js/testing-environment`, mismo patrón que
|
|
317
|
+
`test/end-to-end.test.ts`): ruta por defecto + `get()`/`param()`;
|
|
318
|
+
navegación sin nodos huérfanos (ops de `RemoveChild`+`CreateElement`
|
|
319
|
+
confirmados); `back()`/`forward()` sobre el stack; `onmatch`+`SKIP`
|
|
320
|
+
cayendo a la siguiente ruta; `Link` navegando por tap y respetando
|
|
321
|
+
`disabled`. **Los 8 tests de la suite completa pasan** (los 3 de antes
|
|
322
|
+
+ estos 5).
|
|
323
|
+
- **Deviación respecto al plan original**: el plan (§5.2) proponía que
|
|
324
|
+
`m.route(...)` devolviera el handle `{redraw, document}` de
|
|
325
|
+
`renderApp()`. Al implementarlo se decidió NO devolver nada (igual que
|
|
326
|
+
el `m.route()` real, que tampoco devuelve algo útil) — el auto-redraw
|
|
327
|
+
de Mithril ya cubre todos los redraws necesarios sin que el código de
|
|
328
|
+
la app necesite tocar `app.redraw()`/`app.document` directamente. Más
|
|
329
|
+
fiel a "se siente Mithril" que la propuesta original.
|
|
330
|
+
- **Dependencia temporal**: mientras `mithril-runtime@1.1.0` no esté en
|
|
331
|
+
npm, `mithril-lynx-v2/package.json` apunta a
|
|
332
|
+
`file:/home/sweb/mithril-runtime` en vez de `^1.1.0` — cambiar en
|
|
333
|
+
cuanto el publish se complete (mismo procedimiento que la vez pasada
|
|
334
|
+
con 1.0.0).
|
|
335
|
+
|
|
336
|
+
### F4 y F6 — cerrados, verificados en device real (2026-09-17, sesión siguiente)
|
|
337
|
+
|
|
338
|
+
Se armó una demo real de 2 pantallas en `mithril-lynx-v2-app`:
|
|
339
|
+
`src/screens/home.ts` (título + input + `route.Link` a `/detail/:id`) y
|
|
340
|
+
`src/screens/detail.ts` (título + `route.param("id")` + `route.Link` de
|
|
341
|
+
vuelta a `/`), con `background.ts` reescrito para usar `route(...)` +
|
|
342
|
+
un stable-host **por pantalla** (`HomeHost`/`DetailHost`, cada uno
|
|
343
|
+
`{view: () => currentX.view()}`) y `module.hot.accept` por archivo de
|
|
344
|
+
pantalla, llamando `route.set(route.get(), null, {replace:true})` para
|
|
345
|
+
forzar el re-render tras un hot-swap — la generalización directa del
|
|
346
|
+
patrón stable-host de F1/F3 del plan de reload, ahora una instancia por
|
|
347
|
+
ruta en vez de una sola global.
|
|
348
|
+
|
|
349
|
+
**F4 (navegación limpia)**: tap en "Ir a Detail →" en device real → título
|
|
350
|
+
cambia a "Detail" en rojo, `id: 42` se ve correctamente interpolado
|
|
351
|
+
(`buildPathname("/detail/:id", {id:42})` funcionando). Vuelta con
|
|
352
|
+
"← Volver a Home" funciona. **Hallazgo metodológico importante**: la
|
|
353
|
+
primera verificación usó `DOM.getDocument()` de Lynx DevTool para comparar
|
|
354
|
+
`nodeId` antes/después, y el `nodeId` cambió por completo entre pantallas
|
|
355
|
+
— parecía un huérfano. **Es una falsa alarma**: `nodeId` de CDP se
|
|
356
|
+
reasigna en cada llamada a `DOM.getDocument()`, no es un handle estable
|
|
357
|
+
del elemento nativo — comparar `nodeId` entre dos llamadas *distintas* no
|
|
358
|
+
prueba nada sobre si el elemento físico se recreó. La verificación
|
|
359
|
+
correcta (y la que de verdad importa) es mirar los **ops del patch real**:
|
|
360
|
+
navegar Home→Detail generó `[Op.RemoveChild, 0, 1, Op.CreateElement,
|
|
361
|
+
"view", 8, ...]` — un solo `RemoveChild` que tira toda la subrama vieja de
|
|
362
|
+
Home (id interno 1, el propio backend/virtual id, no el `nodeId` de CDP)
|
|
363
|
+
antes de crear Detail desde cero. Cero huérfanos, confirmado por el
|
|
364
|
+
mecanismo correcto. **Corrección para F5 del plan de reload
|
|
365
|
+
(`mithril-lynx-v2-desde-cero.md`)**: esa sesión también usó
|
|
366
|
+
`DOM.getDocument()` antes/después y reportó ids estables — en ese caso
|
|
367
|
+
coincidió con la realidad (validado independientemente por
|
|
368
|
+
`uiautomator` en la misma sesión), pero fue suerte de que no cambiara de
|
|
369
|
+
método entre llamadas, no una propiedad garantizada de la herramienta.
|
|
370
|
+
Anotado acá para que futuras sesiones no repitan la comparación de
|
|
371
|
+
`nodeId` entre llamadas separadas de `DOM.getDocument()` como prueba de
|
|
372
|
+
identidad.
|
|
373
|
+
|
|
374
|
+
**F6 (reload + ruta activa)**: parado en `/detail/42`, se editó
|
|
375
|
+
`screens/detail.ts` en vivo (cambio de texto). Logcat:
|
|
376
|
+
`:4`→`:5`→`:6 hmr-check-resolved updatedModulesLength:1`, **sin**
|
|
377
|
+
`:9` (full reload). Captura directa de los ops reales enviados
|
|
378
|
+
(instrumentación temporal en `channel.js`, removida después de
|
|
379
|
+
confirmar): **`[Op.SetText, id, "texto nuevo"]` — nada más.** Cero
|
|
380
|
+
`CreateElement`, cero `RemoveChild`. El stable-host por pantalla funciona
|
|
381
|
+
exactamente igual que el stable-host global del plan de reload — routing
|
|
382
|
+
no rompió nada del mecanismo ya construido. Confirmado además con un test
|
|
383
|
+
de regresión permanente (`test/route-hot-reload.test.ts`) que fija este
|
|
384
|
+
comportamiento con las mismas aserciones sobre los ops.
|
|
385
|
+
|
|
386
|
+
**F5 (botón atrás nativo)**: no se encontró forma de disparar un back
|
|
387
|
+
nativo real en esta sesión para confirmar/descartar el hallazgo estático
|
|
388
|
+
de F0 (el device no tiene un botón físico de "atrás" mapeado a la app —
|
|
389
|
+
solo el botón de navegación de Android, que sale de la app en vez de
|
|
390
|
+
navegar dentro de ella). El hallazgo de F0 (no hay evento expuesto al JS)
|
|
391
|
+
queda como la respuesta operativa: `route.back()`/`route.Link` explícito
|
|
392
|
+
son el único camino, tal como React/Vue en Lynx tampoco ofrecen otra
|
|
393
|
+
cosa.
|
|
394
|
+
|
|
395
|
+
**Suite completa: 9/9 tests pasan** (`npx rstest run`, sin device) — F1-F3
|
|
396
|
+
originales (5) + F6 (1, `route-hot-reload.test.ts`) + los 3 previos del
|
|
397
|
+
plan de reload.
|