mithril-lynx 0.0.8 → 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 -284
- package/REQUEST.md +71 -0
- package/ROUTE.md +71 -0
- package/package.json +24 -80
- package/plugin.d.ts +4 -27
- package/plugin.js +142 -359
- 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 +245 -0
- 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,548 @@
|
|
|
1
|
+
# Plan — mithril-lynx v2: reescritura completa, redraw automático, 3 modos de reload
|
|
2
|
+
|
|
3
|
+
> Estado (2026-09-17): **Plan completo, F0–F6 hechos y verificados.**
|
|
4
|
+
> F0–F2 con `rstest` (PAPI real vía `@lynx-js/testing-environment`, sin
|
|
5
|
+
> mocks). F3–F5 en un device Android real conectado (`adb R8YYC0VV0PV`),
|
|
6
|
+
> con una app de prueba (`mithril-lynx-v2-app`) — los 3 modos de reload y
|
|
7
|
+
> el auto-redraw de F1 confirmados con trazas de logcat, un dump de
|
|
8
|
+
> `uiautomator`, Y una inspección de árbol por CDP vía Lynx DevTool
|
|
9
|
+
> (`agent-lynx`) — tres instrumentos independientes, mismo resultado. F6
|
|
10
|
+
> (`create-mithril-lynx-v2`) probado generando un proyecto nuevo desde cero
|
|
11
|
+
> y compilándolo. Ver §8 al final de este documento para el detalle exacto
|
|
12
|
+
> y la evidencia cruda de cada fase. Idioma: español (consistente con el
|
|
13
|
+
> resto de planes de este proyecto).
|
|
14
|
+
>
|
|
15
|
+
> Decisión del usuario (2026-09-17): v2 se escribe en un directorio hermano
|
|
16
|
+
> nuevo, `mithril-lynx-v2/`, **no** dentro de `mithril-lynx/`. Motivo textual:
|
|
17
|
+
> *"no quiero hacerlo en el directorio actual, por que no quiero que queden
|
|
18
|
+
> cabos sueltos"*. `mithril-lynx/` (v1) queda intacto, de solo lectura, como
|
|
19
|
+
> referencia — no se borra, no se toca, no se le agregan más parches.
|
|
20
|
+
>
|
|
21
|
+
> Insumo principal: `../../rspeedy-react-analysis/LYNX_PAPI_SPEC.md` —
|
|
22
|
+
> investigación de cómo ReactLynx realmente logra redraw automático y reload
|
|
23
|
+
> ligero, leyendo su código fuente real (no el REPORT.md anterior, que era
|
|
24
|
+
> superficial). Este plan traduce esos hallazgos en decisiones de arquitectura
|
|
25
|
+
> para v2. Todas las citas `§N` de ese documento se refieren a él.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 0. Por qué una v2 desde cero y no un fix de v1
|
|
30
|
+
|
|
31
|
+
v1 (`mithril-lynx/`) llegó a un punto donde cada solución generó un cabo
|
|
32
|
+
suelto nuevo:
|
|
33
|
+
|
|
34
|
+
- El reload ligero (F0–F2 del plan viejo, `mithril-lynx/.omo/plans/arquitectura-dual-reload.md`)
|
|
35
|
+
quedó funcionando para el caso feliz, pero la forma de lograrlo — monkey-
|
|
36
|
+
patch de `lynx.requireModuleAsync` en runtime, un `if (typeof
|
|
37
|
+
globalThis.__FlushElementTree !== "function")` condicional para decidir si
|
|
38
|
+
el hook de flush existe — son parches sobre síntomas, no un diseño.
|
|
39
|
+
- La regresión activa ahora mismo (auto-redraw tras un evento no llega al
|
|
40
|
+
main-thread; test `renderer-integration.test.ts` en rojo) es consecuencia
|
|
41
|
+
directa de eso: el punto de "flush" del framework es opcional/condicional
|
|
42
|
+
en vez de ser un contrato fijo instalado una sola vez.
|
|
43
|
+
- `LYNX_PAPI_SPEC.md` mostró que varias de las decisiones de v1 no son las
|
|
44
|
+
que usa el propio ReactLynx (canal de patch, guard de race, cómo se evita
|
|
45
|
+
el error de `exports`, cómo se reconcilia un reload) — no son ajustes
|
|
46
|
+
incrementales, son otra arquitectura. Mezclarlas a mitad de v1 hubiera
|
|
47
|
+
significado reescribir el core de todos modos.
|
|
48
|
+
|
|
49
|
+
Conclusión: reescribir desde cero, con el contrato correcto desde el primer
|
|
50
|
+
commit, es más barato que seguir apilando parches sobre v1.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 1. Objetivo
|
|
55
|
+
|
|
56
|
+
Un framework mithril-sobre-Lynx que iguale el contrato de desarrollo de
|
|
57
|
+
ReactLynx en dos frentes concretos:
|
|
58
|
+
|
|
59
|
+
1. **Redraw automático genuino.** Un handler de evento (`ontap`, etc.) que
|
|
60
|
+
muta estado actualiza la pantalla sin que el código de la app llame nada
|
|
61
|
+
— nada de `shim.redraw()`/`m.redraw()` a mano como paso obligatorio (hoy
|
|
62
|
+
sigue existiendo como API, igual que en React con `setState`, pero
|
|
63
|
+
**nunca** debe ser el único camino para que algo se pinte).
|
|
64
|
+
2. **Tres modos de reload**, no dos — el usuario ya lo señaló y
|
|
65
|
+
`LYNX_PAPI_SPEC.md` §4.4 lo confirmó con evidencia de código real:
|
|
66
|
+
- **A — Light reload de datos**: texto/props/CSS. Ya validado en v1,
|
|
67
|
+
se rediseña sobre el core nuevo.
|
|
68
|
+
- **B — Light reload estructural**: agregar/quitar/reordenar nodos del
|
|
69
|
+
árbol (lo que v1 asumía que *tenía* que ser full reload). ReactLynx lo
|
|
70
|
+
resuelve mandando el nuevo template compilado por el MISMO canal de
|
|
71
|
+
patch (`DEV_ONLY_AddSnapshot`); v2 lo resuelve dejando que el diff
|
|
72
|
+
normal de Mithril (que corre en background contra un árbol real)
|
|
73
|
+
produzca los ops de Create/Insert/Remove que hagan falta — no necesita
|
|
74
|
+
un "modo" de wire protocol aparte, necesita que la reconciliación no
|
|
75
|
+
tire elementos que no cambiaron (ver §3.6).
|
|
76
|
+
- **C — Full reload**: fallback para lo que A/B no pueden resolver
|
|
77
|
+
(imports nuevos, dependencias cambiadas, error irrecuperable). Ya
|
|
78
|
+
estable en v1 desde 0.0.9 (commit `74fcf9f`, vía CDP `Page.reload`) —
|
|
79
|
+
se reutiliza el mecanismo, reescrito sobre el core nuevo.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 2. No-objetivos (explícitos, para no repetir el scope creep de v1)
|
|
84
|
+
|
|
85
|
+
- **No portar código de v1.** Está permitido releer v1 y reimplementar
|
|
86
|
+
conceptos ya validados en device (el wrapper de Element PAPI del
|
|
87
|
+
main-thread, gestos, listas — ver `mithril-lynx/CONTRACT.md` y
|
|
88
|
+
`DEVICE_VERIFICATION.md`), pero como código nuevo escrito para el
|
|
89
|
+
contrato v2, no copy-paste.
|
|
90
|
+
- **No HMR de "worklets"/funciones main-thread-script.** mithril-lynx no
|
|
91
|
+
tiene ese concepto todavía; ReactLynx tampoco lo resolvió (`hmr.js`:
|
|
92
|
+
*"disable hmr until bugs are fixed"*, sin arreglar). No se persigue acá.
|
|
93
|
+
- **No compilador de templates (SWC) propio.** v2 sigue interpretando
|
|
94
|
+
hyperscript de Mithril en runtime, como v1 — no precompila `create()`/
|
|
95
|
+
`update[]` por posición como hace ReactLynx. Es una limitación de
|
|
96
|
+
performance aceptada conscientemente, no un bloqueo para el reload.
|
|
97
|
+
- **No se mantienen los 3 modos de render de v1** (main-thread-owned,
|
|
98
|
+
data-channel, renderer). v2 nace con **un solo modo**: vista real en
|
|
99
|
+
background contra árbol virtual, patch al main-thread — es el único que
|
|
100
|
+
puede dar reload ligero, que es el objetivo declarado. Si en el futuro
|
|
101
|
+
hace falta un modo "sin background" por alguna razón de performance, es
|
|
102
|
+
una v3, no parte de este plan.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 3. Arquitectura (decisiones tomadas — no todas están abiertas a discusión, ver F0 para las que sí)
|
|
107
|
+
|
|
108
|
+
### 3.1 Modelo de hilos: uno solo
|
|
109
|
+
|
|
110
|
+
Toda la vista (Mithril real, diff real) corre en el hilo **background**
|
|
111
|
+
contra un árbol virtual (equivalente al `VirtualNodeWrapper` que v1 ya tiene
|
|
112
|
+
en `internal/virtual-node.js` — reimplementado, no copiado). El
|
|
113
|
+
**main-thread** solo aplica un patch contra elementos PAPI reales — nunca
|
|
114
|
+
ejecuta lógica de vista. Esto es lo que v1 llamó "renderer mode" y ya
|
|
115
|
+
verificó on-device (F1/F2 del plan viejo) — la decisión acá es que en v2
|
|
116
|
+
**no hay alternativa**, no que se descubre de nuevo.
|
|
117
|
+
|
|
118
|
+
### 3.2 Canal cruzado background → main: a decidir con evidencia (F0)
|
|
119
|
+
|
|
120
|
+
`LYNX_PAPI_SPEC.md` §3 documentó que ReactLynx usa
|
|
121
|
+
`lynx.getNativeApp().callLepusMethod(name, payload, callback)` — una
|
|
122
|
+
llamada nativa directa con callback de confirmación — en vez de un evento
|
|
123
|
+
genérico (`lynx.getCoreContext().dispatchEvent()`, que es lo que usa v1
|
|
124
|
+
hoy). No sabemos todavía si `callLepusMethod`/el mecanismo de "calledByNative
|
|
125
|
+
globals" es parte de la PAPI pública general o es infraestructura interna
|
|
126
|
+
exclusiva de `@lynx-js/react`. **Regla de decisión (F0.1):**
|
|
127
|
+
|
|
128
|
+
- Si `callLepusMethod` (o equivalente) funciona desde un bundle sin
|
|
129
|
+
`@lynx-js/react` → v2 lo adopta. Ventaja: callback de confirmación nativo,
|
|
130
|
+
sin depender de que el main-thread ya esté escuchando un evento.
|
|
131
|
+
- Si no está expuesto a terceros → v2 se queda con el canal de evento
|
|
132
|
+
custom que v1 ya tiene funcionando (`lynx.getCoreContext().dispatchEvent`
|
|
133
|
+
con nombre de evento propio) — es una degradación aceptable, no
|
|
134
|
+
bloqueante, porque ya está validado en device.
|
|
135
|
+
|
|
136
|
+
### 3.3 Formato del patch: array plano de enteros, no objetos
|
|
137
|
+
|
|
138
|
+
v1 manda ops como `{op: "createElement", vid, tag}`. v2 adopta el patrón de
|
|
139
|
+
`SnapshotOperation` (`LYNX_PAPI_SPEC.md` §4.3): un array plano
|
|
140
|
+
`[opcode, ...args, opcode, ...args, ...]`, con un enum de opcodes propio
|
|
141
|
+
para el vocabulario mínimo de mithril-lynx (CreateElement, InsertBefore,
|
|
142
|
+
RemoveChild, SetAttribute, SetAttributes-batch, AddEvent, RemoveEvent).
|
|
143
|
+
Motivo: menos overhead de `JSON.stringify`/parseo, patrón ya probado en
|
|
144
|
+
producción por ReactLynx a escala. Este cambio es de bajo riesgo (es
|
|
145
|
+
serialización interna, no afecta la superficie pública del framework) y no
|
|
146
|
+
necesita spike — se implementa directo en F2.
|
|
147
|
+
|
|
148
|
+
### 3.4 Redraw automático — el rediseño central que pidió el usuario
|
|
149
|
+
|
|
150
|
+
Este es el punto que v1 nunca resolvió bien y es la razón de fondo de la
|
|
151
|
+
regresión activa. Diagnóstico de v1 (ya documentado en
|
|
152
|
+
`mithril-lynx/AGENTS.md`): el hook de fin-de-render de Mithril
|
|
153
|
+
(`flushTree()`) llama a un global (`__FlushElementTree`) que **a veces
|
|
154
|
+
existe y a veces no**, dependiendo de si `renderApp()` ya corrió, en qué
|
|
155
|
+
hilo, y en qué orden — un `if (typeof globalThis.X !== "function")` es una
|
|
156
|
+
condición de carrera de diseño, no un detalle de implementación.
|
|
157
|
+
|
|
158
|
+
**Diseño v2:** el shim de Mithril expone un **punto de extensión explícito
|
|
159
|
+
y obligatorio** para "algo terminó de re-renderizar, hay que empujarlo" —
|
|
160
|
+
no un global condicional. Ejemplo de forma (a afinar en F1, la idea es el
|
|
161
|
+
contrato, no la firma exacta):
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
// v2/src/shim.js — análogo a Preact's options.__c (commit hook),
|
|
165
|
+
// pero como API de primera clase del shim, no un hack sobre `options`.
|
|
166
|
+
export function onCommit(callback) {
|
|
167
|
+
// registra `callback` como EL único punto de salida de cualquier
|
|
168
|
+
// pase de render/redraw — eventos, m.redraw(), aplicación de HMR.
|
|
169
|
+
// Lanza si ya hay uno registrado: un shim solo tiene UN consumidor
|
|
170
|
+
// (quien monta la app), nunca "el que llegue primero define el global".
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`renderApp()` (el entry point de background, equivalente al `renderApp` de
|
|
175
|
+
v1) llama `onCommit(flushToMainThread)` **una vez, de forma explícita, en
|
|
176
|
+
su propio código** — no hay detección implícita de "si ya existe un
|
|
177
|
+
flush". Si `renderApp()` nunca corrió, cualquier intento de redraw debe
|
|
178
|
+
fallar ruidosamente (throw), no fallar en silencio como pasa hoy en v1
|
|
179
|
+
(pantalla congelada sin ningún error).
|
|
180
|
+
|
|
181
|
+
**Criterio de diseño no negociable:** todo redraw —por evento, por
|
|
182
|
+
`m.redraw()` manual, o por HMR— pasa por el mismo único callback. Si en
|
|
183
|
+
algún punto del código hace falta preguntar "¿existe la función de
|
|
184
|
+
flush?", el diseño está mal — la pregunta correcta es "¿ya se montó la
|
|
185
|
+
app?", que se responde con una excepción clara en desarrollo, no con un
|
|
186
|
+
`typeof` chequeado en cada llamada.
|
|
187
|
+
|
|
188
|
+
### 3.5 Los 3 modos de reload, en detalle
|
|
189
|
+
|
|
190
|
+
**A — Datos (texto/props/CSS).** Idéntico en espíritu a v1 F1: HMR normal
|
|
191
|
+
de webpack (`module.hot.accept("./view.js", cb)`), el callback reapunta un
|
|
192
|
+
binding vivo al módulo recargado y dispara un commit (§3.4). Ya validado
|
|
193
|
+
on-device en v1 — se re-implementa sobre el core nuevo, no se re-descubre.
|
|
194
|
+
|
|
195
|
+
**B — Estructural.** La diferencia real con A es solo "cuánto cambia el
|
|
196
|
+
árbol", no el mecanismo de transporte. Como en v2 la vista SIEMPRE corre en
|
|
197
|
+
background contra un árbol real (§3.1), un cambio estructural (nuevo nodo,
|
|
198
|
+
nodo eliminado, reordenado) produce naturalmente ops
|
|
199
|
+
`CreateElement`/`InsertBefore`/`RemoveChild` en el mismo patch — Mithril ya
|
|
200
|
+
sabe diffear eso, es su trabajo normal. **No hace falta un modo de wire
|
|
201
|
+
protocol separado como el `DEV_ONLY_AddSnapshot` de ReactLynx** (ese existe
|
|
202
|
+
porque ReactLynx precompila templates a nivel de función y necesita mandar
|
|
203
|
+
la función nueva; mithril-lynx interpreta hyperscript en runtime, así que
|
|
204
|
+
el módulo recargado YA contiene la nueva estructura sin nada que serializar
|
|
205
|
+
aparte). El problema real de B no es de protocolo — es de **reconciliación**
|
|
206
|
+
(§3.6): si insertás un nodo hermano de un `<input>` enfocado, ¿el input
|
|
207
|
+
sobrevive?
|
|
208
|
+
|
|
209
|
+
**C — Full reload.** Mecanismo de v1 sin cambios de fondo (CDP
|
|
210
|
+
`Page.reload`, cache-busted) — se dispara cuando `module.hot.check()` es
|
|
211
|
+
rechazado, `module.hot.decline()` fue llamado, o hay un error irrecuperable
|
|
212
|
+
aplicando un patch. Se reescribe sobre el core nuevo por prolijidad, no
|
|
213
|
+
porque el mecanismo esté roto.
|
|
214
|
+
|
|
215
|
+
### 3.6 Reconciliación en reload estructural — la pieza que v1 no tiene y v2 sí necesita
|
|
216
|
+
|
|
217
|
+
v1 resolvió esto para UN caso (el patrón "stable-host": un componente fijo,
|
|
218
|
+
delegando a un binding vivo) que preserva el `<input>` **solo si la
|
|
219
|
+
estructura no cambia** entre el módulo viejo y el nuevo. Eso no cubre B.
|
|
220
|
+
|
|
221
|
+
ReactLynx lo resuelve con `hydrate(oldRoot, newRoot, {skipUnRef: true})`
|
|
222
|
+
(`LYNX_PAPI_SPEC.md` §5.1): renderiza el árbol nuevo completo desde cero en
|
|
223
|
+
un root nuevo, y después reconcilia por posición/tipo contra el root viejo,
|
|
224
|
+
reutilizando el elemento físico donde el nodo nuevo calza estructuralmente
|
|
225
|
+
y creando/borrando solo donde de verdad cambió — el mismo algoritmo que se
|
|
226
|
+
usa para hidratar SSR, aplicado a un reload.
|
|
227
|
+
|
|
228
|
+
**Decisión para v2:** no reimplementar un hydrate genérico tipo React desde
|
|
229
|
+
cero (es una pieza grande y arriesgada, ver riesgos §6) — en cambio,
|
|
230
|
+
aprovechar que Mithril YA hace esto cuando renderiza dos veces **contra el
|
|
231
|
+
mismo root/vnode tree**: si el hot-update re-ejecuta `render(rootWrapper,
|
|
232
|
+
newVnode)` sobre el **mismo `rootWrapper`** que ya tenía el árbol viejo
|
|
233
|
+
montado (en vez de crear un root nuevo), el diff normal de Mithril (por
|
|
234
|
+
tag + posición + `key`) hace exactamente el trabajo de "reusar donde
|
|
235
|
+
calza, recrear donde no" — sin escribir un hydrate aparte. Esto generaliza
|
|
236
|
+
el patrón stable-host de v1 (que ya hacía esto para un solo componente) a
|
|
237
|
+
cualquier árbol, con una regla de disciplina para el autor de la app: usar
|
|
238
|
+
`key` en listas/nodos que puedan reordenarse, igual que en React. Se marca
|
|
239
|
+
como **hipótesis a verificar en F4**, no como hecho — si el diff normal de
|
|
240
|
+
Mithril no alcanza a preservar el foco en casos no triviales, ahí sí hace
|
|
241
|
+
falta diseñar algo más parecido a `hydrate()`.
|
|
242
|
+
|
|
243
|
+
### 3.7 Evitar el error "exports is not defined" por diseño, no por monkey-patch
|
|
244
|
+
|
|
245
|
+
v1 resolvió esto parcheando `lynx.requireModuleAsync` en runtime para
|
|
246
|
+
inyectar `module`/`exports` antes de evaluar un chunk `.hot-update.js` de
|
|
247
|
+
webpack. `LYNX_PAPI_SPEC.md` §6 mostró que ReactLynx nunca tiene este
|
|
248
|
+
problema porque **todo chunk se envuelve en build-time** en un module
|
|
249
|
+
system propio (`tt.define(id, function(require, module, exports, ...) {})`)
|
|
250
|
+
vía `RuntimeWrapperWebpackPlugin` — `module`/`exports` son siempre
|
|
251
|
+
parámetros de función reales, nunca variables libres que el motor deba
|
|
252
|
+
proveer.
|
|
253
|
+
|
|
254
|
+
**Decisión para v2 (a validar en F0.2):** escribir un plugin de build
|
|
255
|
+
equivalente para mithril-lynx-v2 que envuelva TODO chunk (incluidos los
|
|
256
|
+
`.hot-update.js`) de la misma forma, en vez de mantener el monkey-patch de
|
|
257
|
+
`requireModuleAsync` en el cliente de dev-reload. Si envolver hot-update
|
|
258
|
+
chunks specifically resulta impracticable con Rspeedy/rspack tal como está
|
|
259
|
+
expuesto hoy, el monkey-patch de v1 queda como plan B documentado (ya
|
|
260
|
+
funciona, es solo menos elegante).
|
|
261
|
+
|
|
262
|
+
### 3.8 Guard de race entre builds: contador de versión, no un flag de estado
|
|
263
|
+
|
|
264
|
+
v1 tuvo la "race de doble-build" (F1 del plan viejo): un guard
|
|
265
|
+
`hotStatus === "idle"` que, si dos rebuilds llegan casi juntos, degrada a
|
|
266
|
+
full reload aunque cada uno individualmente fuera ligero. ReactLynx evita
|
|
267
|
+
esto con un contador global `reloadVersion` que se incrementa en cada
|
|
268
|
+
reload y descarta silenciosamente cualquier patch en vuelo con una versión
|
|
269
|
+
vieja (`LYNX_PAPI_SPEC.md` §5.1). v2 adopta el contador de versión
|
|
270
|
+
directamente — no es un spike, es una decisión de bajo riesgo con un patrón
|
|
271
|
+
de referencia claro.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 4. Fases
|
|
276
|
+
|
|
277
|
+
| Fase | Contenido | Criterio de salida | Ayuda del usuario |
|
|
278
|
+
|---|---|---|---|
|
|
279
|
+
| **F0** | Spikes de viabilidad (antes de escribir una línea del framework) | Ver F0.1–F0.3 abajo | Sí, dos de tres necesitan device |
|
|
280
|
+
| **F1** | Shim/core reescrito: diff de Mithril + punto de extensión de commit único (§3.4) | Test unitario equivalente al que hoy está roto en v1 (`renderer-integration.test.ts`, reescrito) **pasa desde el primer commit** | No — automatizable con `rstest` |
|
|
281
|
+
| **F2** | Canal de patch (según F0.1) + formato de ops plano (§3.3) + apply en main-thread | Snapshot/replay test: un render produce el mismo árbol físico que hoy, ops verificados por conteo/tipo | No |
|
|
282
|
+
| **F3** | Reload A (datos) + C (full) sobre el core nuevo | Device: editar texto → sin `Page.reload`, foco/texto de `<input>` intactos (mismo criterio que v1 ya alcanzó, ahora con arquitectura limpia) | Sí — device + logcat |
|
|
283
|
+
| **F4** | Reload B (estructural) + verificación de la hipótesis de §3.6 | Device: insertar/quitar un nodo HERMANO de un `<input>` enfocado → foco y texto sobreviven. **v1 nunca llegó a plantear este criterio** | Sí — device + logcat |
|
|
284
|
+
| **F5** | Verificación cruzada con Lynx DevTool | Inspeccionar el árbol de elementos en vivo durante cada uno de los 3 modos; confirmar 0 elementos huérfanos/duplicados tras reload estructural | Sí — DevTool conectado al device |
|
|
285
|
+
| **F6** | Empaquetado: `create-mithril-lynx-v2` (o adaptar `mithril-app-final` como banco de pruebas de v2) | Scaffold fresco compila y corre los 3 modos de reload | No (verificación automatizable una vez F0–F5 cierran) |
|
|
286
|
+
|
|
287
|
+
### F0 — Detalle de los spikes
|
|
288
|
+
|
|
289
|
+
- **F0.1 — ¿`callLepusMethod`/canal nativo directo disponible sin `@lynx-js/react`?**
|
|
290
|
+
Método: app mínima en `mithril-lynx-v2` (sin ninguna dependencia de
|
|
291
|
+
`@lynx-js/react`) que intente `lynx.getNativeApp().callLepusMethod(...)`
|
|
292
|
+
desde el background y capturar en logcat si el main-thread recibe algo.
|
|
293
|
+
Regla de decisión: ver §3.2.
|
|
294
|
+
- **F0.2 — ¿se puede envolver `.hot-update.js` en build-time con Rspeedy/rspack?**
|
|
295
|
+
Método: plugin mínimo de rspack que intente aplicar el mismo wrapper
|
|
296
|
+
`tt.define` a un chunk de hot-update generado por `HotModuleReplacementPlugin`,
|
|
297
|
+
verificar en el `dist/` compilado si el wrapper quedó aplicado también ahí
|
|
298
|
+
(no solo en los chunks iniciales). Regla de decisión: ver §3.7.
|
|
299
|
+
- **F0.3 — confirmar que el guard de `reloadVersion` alcanza sin el flag `hotStatus`.**
|
|
300
|
+
Método: reproducir la race de doble-build que v1 documentó (dos rebuilds
|
|
301
|
+
en ráfaga, ver `arquitectura-dual-reload.md` "race de doble-build") contra
|
|
302
|
+
una implementación mínima del contador, confirmar que ambos patches se
|
|
303
|
+
aplican en orden correcto (o el viejo se descarta) sin caer a full reload.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## 5. Herramientas de verificación ya disponibles en este entorno
|
|
308
|
+
|
|
309
|
+
- **Device Android real** (visto en sesiones anteriores: Galaxy A07 / otro
|
|
310
|
+
dispositivo con Lynx Go instalado) vía `adb` — USB o Wi-Fi.
|
|
311
|
+
- **Lynx DevTool** (skill `lynx-devtool` + bridge ya operativo con el
|
|
312
|
+
device) — inspección de árbol DOM/CSS, screenshots, logs de runtime,
|
|
313
|
+
evaluación de JS en vivo.
|
|
314
|
+
- **`adb logcat`** — script ya existente en v1 (`mithril-app-final/scripts/adb-log.sh`),
|
|
315
|
+
se replica igual en el banco de pruebas de v2.
|
|
316
|
+
- **`rstest`** como test runner — **no `vitest`**, ya confirmado que falla
|
|
317
|
+
con "Rstest API 'describe' is not registered" si se usa por error.
|
|
318
|
+
- **Convención de traza `[mrl-trace]`** (o el nombre que se le dé en v2) —
|
|
319
|
+
logging estructurado y numerado para diagnósticos cruzados
|
|
320
|
+
background/main, ya probado como herramienta de debugging efectiva en v1.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## 6. Riesgos
|
|
325
|
+
|
|
326
|
+
| Riesgo | Mitigación |
|
|
327
|
+
|---|---|
|
|
328
|
+
| F0.1 cierra en "no disponible para terceros" | Cae a canal de evento genérico (§3.2) — no bloqueante, ya validado en v1 |
|
|
329
|
+
| F0.2 impracticable con Rspeedy tal como está expuesto | Monkey-patch de v1 queda como plan B documentado, ya funciona |
|
|
330
|
+
| §3.6 (reusar el diff normal de Mithril como reconciliador) no alcanza en casos no triviales | F4 lo marca como hipótesis a verificar, no como hecho — si falla, hace falta una segunda iteración de diseño (hydrate real) antes de cerrar F4 |
|
|
331
|
+
| Sin compilador de templates, el árbol virtual es más caro en runtime que el de ReactLynx | Aceptado como no-objetivo (§2) — no se ataca en esta v2 |
|
|
332
|
+
| Repetir el error de v1 de dejar `console.log("[dbg]...")` de debugging mezclado con código de producción | Regla de higiene explícita: instrumentación de diagnóstico vive detrás de un flag (`__DEV__`-style), nunca como `console.log` suelto en el core |
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## 7. Referencias
|
|
337
|
+
|
|
338
|
+
- `../../rspeedy-react-analysis/LYNX_PAPI_SPEC.md` — spec de la PAPI y
|
|
339
|
+
mecanismo real de ReactLynx (fase 1 de investigación, ya escrita).
|
|
340
|
+
- `../../mithril-lynx/AGENTS.md` — estado y regresión activa de v1, para no
|
|
341
|
+
repetir los mismos errores de diseño.
|
|
342
|
+
- `../../mithril-lynx/.omo/plans/arquitectura-dual-reload.md` — lo que v1
|
|
343
|
+
intentó, con evidencia on-device real; insumo útil aunque v2 no siga su
|
|
344
|
+
arquitectura exacta en varios puntos (§3.2–§3.8 documentan exactamente
|
|
345
|
+
dónde difiere y por qué).
|
|
346
|
+
- `../../mithril-lynx/CONTRACT.md`, `../../mithril-lynx/DEVICE_VERIFICATION.md` —
|
|
347
|
+
conceptos de Element PAPI wrapper, gestos y listas ya validados en device,
|
|
348
|
+
candidatos a reimplementar (no copiar) en v2 — no forman parte del
|
|
349
|
+
problema de reload, son infraestructura ya resuelta.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## 8. Estado de ejecución (actualizado en vivo, no re-escribir el plan de arriba)
|
|
354
|
+
|
|
355
|
+
### F0 — cerrado, con evidencia real (no experimento en device todavía, pero no hacía falta)
|
|
356
|
+
|
|
357
|
+
- **F0.1** (canal nativo sin `@lynx-js/react`): `callLepusMethod` **no existe**
|
|
358
|
+
en el `lynx_core.js` real instalado (`indicadores-android/.../lynx_core.js`,
|
|
359
|
+
0 ocurrencias). Sí existe `lynx.triggerLepusGlobalEvent(name, params)` —
|
|
360
|
+
genérico, público, parte del motor base, no de `@lynx-js/react`. Queda
|
|
361
|
+
como candidato de canal para F3; el canal de evento genérico de v1
|
|
362
|
+
(`getCoreContext`/`getJSContext`, también presentes en ese mismo
|
|
363
|
+
`lynx_core.js`) sigue como fallback validado si `triggerLepusGlobalEvent`
|
|
364
|
+
no calza con lo que necesita v2 en la práctica.
|
|
365
|
+
- **F0.2** (wrapping de chunks hot-update): **causa raíz encontrada, no es
|
|
366
|
+
una limitación arquitectónica.** v1 ya depende de
|
|
367
|
+
`@lynx-js/runtime-wrapper-webpack-plugin` (el mismo plugin oficial que usa
|
|
368
|
+
ReactLynx) pero lo configura con
|
|
369
|
+
`test: new RegExp(`${name}/background\\.js$`)` (`mithril-lynx/plugin.js:613`)
|
|
370
|
+
— matchea el asset inicial `main-thread/background.js` (ruta con slash,
|
|
371
|
+
el nombre de ASSET) pero nunca los chunks `.hot-update.js` (que se emiten
|
|
372
|
+
planos, nombrados por el nombre del CHUNK: `main-thread__background.<hash>.hot-update.js`,
|
|
373
|
+
con doble guión bajo, sin slash). Confirmado con un test de regex directo,
|
|
374
|
+
no con una build completa. **F6 solo necesita ampliar ese regex** —
|
|
375
|
+
no hace falta un plugin nuevo ni el monkey-patch de
|
|
376
|
+
`lynx.requireModuleAsync` que v1 tuvo que escribir.
|
|
377
|
+
- **F0.3** (guard de versión): implementado y testeado directamente
|
|
378
|
+
(`src/reload/version.js` + `test/reload-version.test.ts`), sin
|
|
379
|
+
necesidad de reproducir la race real todavía — la lógica es la misma que
|
|
380
|
+
usa ReactLynx, de bajo riesgo.
|
|
381
|
+
|
|
382
|
+
### F1 — cerrado y verificado
|
|
383
|
+
|
|
384
|
+
Reescrito desde cero (`src/fake-dom.js`, `src/commit.js`, `src/background.js`),
|
|
385
|
+
corriendo el `mithril@2.3.8` real (`render/render.js`, sin fork) contra un
|
|
386
|
+
DOM falso construido para este propósito — no contra una copia modificada
|
|
387
|
+
del shim de v1. El contrato exacto de esa reimplementación está en
|
|
388
|
+
`mithril-lynx/CONTRACT.md` (ya escrito por una sesión anterior, verificado
|
|
389
|
+
por grep contra el `render.js` real) — se usó como checklist, no como
|
|
390
|
+
código a copiar.
|
|
391
|
+
|
|
392
|
+
**Test decisivo, verde desde el primer commit** (`test/end-to-end.test.ts`):
|
|
393
|
+
un `ontap` que muta estado SIN llamar `redraw()`/`m.redraw()` en ningún
|
|
394
|
+
lado produce un segundo patch automáticamente — el mismo escenario que
|
|
395
|
+
estaba en rojo en v1 (`mithril-lynx/test/renderer-integration.test.ts`,
|
|
396
|
+
"device regression"). La diferencia de diseño que lo logra: el callback de
|
|
397
|
+
redraw que Mithril llama solo (`EventDict.handleEvent`, contrato ya
|
|
398
|
+
documentado en CONTRACT.md §e) es una clausura capturada una vez
|
|
399
|
+
(`performRender` en `src/background.js`), nunca un global condicional.
|
|
400
|
+
|
|
401
|
+
### F2 — núcleo cerrado y verificado; aplicación en main-thread con un TODO explícito
|
|
402
|
+
|
|
403
|
+
`src/patch-protocol.js` (array plano de opcodes, patrón `SnapshotOperation`
|
|
404
|
+
de ReactLynx) + `src/backends/virtual-backend.js` (background, genera ops) +
|
|
405
|
+
`src/apply-patch.js` (main-thread, aplica ops con PAPI real —
|
|
406
|
+
`__CreateView`/`__CreateText`/`__CreateElement`/`__CreateRawText`/
|
|
407
|
+
`__AppendElement`/`__InsertElementBefore`/`__RemoveElement`/`__SetAttribute`/
|
|
408
|
+
`__SetClasses`/`__AddInlineStyle`/`__AddEventListener`/`__FlushElementTree`,
|
|
409
|
+
todas validadas ya en device por v1 — ver `mithril-lynx/src/lynx-mithril-shim.js`
|
|
410
|
+
líneas 503-519 y `CONTRACT.md`).
|
|
411
|
+
|
|
412
|
+
**Test decisivo** (`test/end-to-end.test.ts`, mismo archivo que F1): el
|
|
413
|
+
patch inicial y el patch del auto-redraw se aplican con el PAPI real de
|
|
414
|
+
`@lynx-js/testing-environment` (no un mock) y el handler del tap corre
|
|
415
|
+
sobre el nodo real correcto.
|
|
416
|
+
|
|
417
|
+
**Evidencia adicional para F4** (`test/structural-reload.test.ts`, no
|
|
418
|
+
sustituye la verificación en device pero da una señal fuerte antes de
|
|
419
|
+
llegar ahí): insertar un nodo hermano NUEVO junto a un nodo `input`-like
|
|
420
|
+
existente, ambos con `key`, **no** produce ningún op de
|
|
421
|
+
`CreateElement`/`RemoveChild` sobre el id del input — el diff normal de
|
|
422
|
+
Mithril lo reusa in-place. Confirma la hipótesis del §3.6 en el caso
|
|
423
|
+
keyed; el caso sin `key` NO se probó a propósito (es sabido que ahí
|
|
424
|
+
Mithril recrea desde el punto de la diferencia — disciplina de `key`
|
|
425
|
+
documentada, no bug).
|
|
426
|
+
|
|
427
|
+
**TODO explícito dejado en el código** (`apply-patch.js`, caso
|
|
428
|
+
`RemoveStyleProperty` con nombre `"*"`, o sea `element.style = ""`): lanza
|
|
429
|
+
un error en vez de fallar en silencio, porque no hay todavía una llamada
|
|
430
|
+
PAPI de "limpiar todos los estilos de una vez" validada. Bloqueante solo
|
|
431
|
+
si una app usa ese patrón exacto; no bloquea F3.
|
|
432
|
+
|
|
433
|
+
### F3 y F4 — cerrados, verificados en device real (2026-09-17, misma sesión)
|
|
434
|
+
|
|
435
|
+
Se construyó `mithril-lynx-v2-app` (repo git propio, hermano de éste),
|
|
436
|
+
esqueleto igual a `mithril-app-final` pero apuntando a las APIs de v2
|
|
437
|
+
(`mithril-lynx-v2/background`, `/main-thread`, `/plugin`). `npm run build`
|
|
438
|
+
compiló sin errores; `npx rspeedy dev` levantó el dev server en
|
|
439
|
+
`10.49.37.154:3000` (misma subred Wi-Fi que el device, `10.49.37.79`,
|
|
440
|
+
confirmado con `adb shell ip -f inet addr show`). Se abrió el bundle en
|
|
441
|
+
Lynx Go vía `adb shell am start -a android.intent.action.VIEW -d
|
|
442
|
+
"lynx://open?url=<bundle-url-encoded>" com.funcs.io.lynx.go` (deep link
|
|
443
|
+
del propio Lynx Go, sin necesidad de escanear el QR a mano).
|
|
444
|
+
|
|
445
|
+
**F1 en device real** (no solo en `rstest`): `adb shell input tap <título>`
|
|
446
|
+
→ el título cambió de azul a rojo. `index.ts`'s `ontap` no llama
|
|
447
|
+
`redraw()`/`m.redraw()` en ningún lado — la app entera confía en el
|
|
448
|
+
auto-redraw de Mithril, exactamente el mecanismo que estaba roto en v1.
|
|
449
|
+
|
|
450
|
+
**F3 (reload A — datos), 3 ediciones en vivo de `src/index.ts` (texto del
|
|
451
|
+
título)**: cada guardado produjo en logcat
|
|
452
|
+
`:4 hmr-check hotStatus:"idle"` → `:5 hmr-check-calling` → `:6
|
|
453
|
+
hmr-check-resolved updatedModulesLength:1` — **sin** `:7`/`:8`/`:9`
|
|
454
|
+
(ningún full reload). El input, previamente enfocado con `adb shell input
|
|
455
|
+
tap` + `input text "abc123"`, mantuvo el texto y el teclado abierto en las
|
|
456
|
+
3 ediciones (confirmado por captura de pantalla en cada paso).
|
|
457
|
+
|
|
458
|
+
**F4 (reload B — estructural), la prueba que v1 nunca llegó a plantear**:
|
|
459
|
+
con el input todavía enfocado y con `abc123` escrito, se agregó un nodo
|
|
460
|
+
`m("text", {key:"extra"}, "NUEVO NODO F4")` HERMANO del input (entre el
|
|
461
|
+
título y el input) y se guardó. Logcat: mismo camino ligero
|
|
462
|
+
(`:4`→`:5`→`:6 updatedModulesLength:1`), sin full reload. **Verificación
|
|
463
|
+
dura, no solo visual**: `adb shell uiautomator dump` después del cambio
|
|
464
|
+
muestra el `EditText` real con
|
|
465
|
+
`text="abc123" ... focused="true"` — el nodo nuevo se insertó, el título
|
|
466
|
+
cambió, y el input ni perdió el foco ni el texto. Esto confirma en
|
|
467
|
+
device real la hipótesis del §3.6 (reusar el diff normal de Mithril con
|
|
468
|
+
`key` estables alcanza para esto), no solo en el test automatizado
|
|
469
|
+
(`test/structural-reload.test.ts`).
|
|
470
|
+
|
|
471
|
+
**F3 (fallback full reload — método C)**: se editó `src/background.ts`
|
|
472
|
+
(un módulo que NINGÚN `module.hot.accept` cubre — ni siquiera un
|
|
473
|
+
self-accept). Logcat: `:4`→`:5`→**`:7 hmr-check-rejected` "Aborted because
|
|
474
|
+
./src/background.ts is not accepted"** → `:8 reload-called` → `:9
|
|
475
|
+
cdp-page-reload` → reboot limpio (`:0`→`:1`→`:3a`), un solo ciclo, sin
|
|
476
|
+
loop, estado de la app reseteado (el input volvió al placeholder vacío —
|
|
477
|
+
comportamiento correcto y esperado de un full reload).
|
|
478
|
+
|
|
479
|
+
**Los 3 métodos de reload y el auto-redraw de F1 quedan así confirmados
|
|
480
|
+
end-to-end en device real**, con trazas de logcat y un dump de
|
|
481
|
+
`uiautomator` como evidencia dura — no solo con capturas de pantalla ni
|
|
482
|
+
con tests automatizados. Ver el commit `c788572` de
|
|
483
|
+
`mithril-lynx-v2-app` (mensaje del commit incluye el resumen completo de
|
|
484
|
+
esta corrida) y el commit `f449aa6` de este repo (dev-reload-client.js,
|
|
485
|
+
channel.js, main-thread.js, plugin.js con el fix de F0.2 ya aplicado y
|
|
486
|
+
validado — el hot-update chunk se sirvió y evaluó sin el error
|
|
487
|
+
`ReferenceError: exports is not defined` de v1, en ningún momento de la
|
|
488
|
+
sesión, sin ningún monkey-patch de `lynx.requireModuleAsync`).
|
|
489
|
+
|
|
490
|
+
### F5 — cerrado, verificado con Lynx DevTool (2026-09-17, misma sesión)
|
|
491
|
+
|
|
492
|
+
Usando `agent-lynx` (skill `lynx-devtool`) contra el mismo device/sesión de
|
|
493
|
+
F3/F4. Requirió activar switches que estaban OFF por defecto y necesitan un
|
|
494
|
+
relanzamiento de la app para tomar efecto:
|
|
495
|
+
`enable_dom_tree`, `enable_cdp_domain_dom`, `enable_cdp_domain_css`,
|
|
496
|
+
`enable_cdp_domain_page` (`agent-lynx global-switch set --key <k> --status on`).
|
|
497
|
+
Con eso, `DOM.enable {"useCompression":false}` + `DOM.getDocument
|
|
498
|
+
{"depth":-1}` da el árbol real completo, con `nodeId` estables por
|
|
499
|
+
elemento — la fuente de verdad para "¿se reusó el nodo o se recreó?",
|
|
500
|
+
independiente de `uiautomator` (F4) y de las capturas de pantalla.
|
|
501
|
+
|
|
502
|
+
**Baseline**: 6 nodos — `#document(11) > page(10) > view.Page(13) >
|
|
503
|
+
[text.TitleBlue(14) > raw-text(15), input(16)]`.
|
|
504
|
+
|
|
505
|
+
**Método A (datos)**: tras editar el texto del título, el árbol tiene
|
|
506
|
+
**exactamente los mismos 6 `nodeId`** (10,11,13,14,15,16); el `nodeId 15`
|
|
507
|
+
(el raw-text) pasó a tener el atributo `text` con el string nuevo — mismo
|
|
508
|
+
nodo, atributo actualizado in-place, confirmado por CDP, no por
|
|
509
|
+
inferencia visual.
|
|
510
|
+
|
|
511
|
+
**Método B (estructural)**: tras insertar un nodo hermano nuevo entre el
|
|
512
|
+
título y el input, el árbol pasó a 8 nodos: **los 6 originales
|
|
513
|
+
(10,11,13,14,15,16) sin tocar** — el `input` sigue siendo el `nodeId 16`
|
|
514
|
+
exacto — más dos nodos nuevos (`17`=el `text` nuevo, `18`=su `raw-text`),
|
|
515
|
+
insertados en la posición correcta del árbol. Cero huérfanos, cero
|
|
516
|
+
duplicados — el criterio de aceptación del plan original para F5, cumplido
|
|
517
|
+
con una fuente de verdad distinta de `uiautomator` (que ya lo había
|
|
518
|
+
confirmado por otro lado en F4: mismo resultado, dos instrumentos
|
|
519
|
+
distintos).
|
|
520
|
+
|
|
521
|
+
**Método C (full reload)**: tras editar `background.ts` (dispara el
|
|
522
|
+
fallback, igual que en F3), el árbol se reconstruyó desde cero — mismo
|
|
523
|
+
conteo de nodos que el estado B (8, porque el source todavía tenía el
|
|
524
|
+
nodo "extra"), pero **la numeración de `nodeId` se reinició** (el segundo
|
|
525
|
+
`text`/`raw-text` pasó de 17/18 a 16/17, el `input` de 16 a 18) — es
|
|
526
|
+
decir, la sesión vieja se descartó por completo y se creó una página
|
|
527
|
+
nueva de cero, sin ningún id viejo conviviendo con los nuevos. Exactamente
|
|
528
|
+
el comportamiento esperado de un full reload — no es un caso de
|
|
529
|
+
"huérfanos", es una página nueva reemplazando a la anterior.
|
|
530
|
+
|
|
531
|
+
### F6 — hecho: `create-mithril-lynx-v2`
|
|
532
|
+
|
|
533
|
+
Ver `create-mithril-lynx-v2/` (repo hermano nuevo). CLI mínimo que copia
|
|
534
|
+
la plantilla (`templates/ts/`, calcada de `mithril-lynx-v2-app` una vez
|
|
535
|
+
limpio de los edits de prueba de F3-F5) a un directorio nuevo, sustituye
|
|
536
|
+
el nombre del paquete, y deja instrucciones de `npm install && npm run
|
|
537
|
+
dev`. Uso: `npx create-mithril-lynx-v2 <nombre-app>`. No incluye un
|
|
538
|
+
prompt interactivo (JS vs TS, blank vs demo) como tiene
|
|
539
|
+
`create-mithril-lynx` (v1) — la plantilla única de F6 es intencionalmente
|
|
540
|
+
mínima; ampliar el CLI queda fuera de este plan si hace falta más
|
|
541
|
+
adelante.
|
|
542
|
+
|
|
543
|
+
**Pendiente, no bloqueante**: `module.hot.decline()` explícito para
|
|
544
|
+
cambios estructurales "duros" no está implementado — el fallback a full
|
|
545
|
+
reload ya sale solo cuando `module.hot.check()` no puede propagar la
|
|
546
|
+
actualización (confirmado en la prueba de método C de F3/F5). Un
|
|
547
|
+
`decline()` explícito solo agregaría un mensaje de log más claro para el
|
|
548
|
+
autor de la app, no cambia el comportamiento observable.
|