@meekojs/meeko 0.0.1 → 0.0.3
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 +47 -1
- package/dist/cli.js +35 -24
- package/dist/compiler.js +12 -7
- package/dist/devtools-ui/index.d.ts +14 -0
- package/dist/devtools-ui/index.js +351 -0
- package/dist/devtools.d.ts +65 -0
- package/dist/devtools.js +296 -0
- package/dist/dom.d.ts +9 -5
- package/dist/dom.js +76 -14
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/init.d.ts +2 -0
- package/dist/init.js +207 -0
- package/dist/internal.d.ts +13 -0
- package/dist/internal.js +11 -0
- package/dist/reactivity.d.ts +13 -1
- package/dist/reactivity.js +29 -7
- package/package.json +20 -5
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { DomScope } from "./dom.js";
|
|
2
|
+
import type { EffectSource } from "./reactivity.js";
|
|
3
|
+
/** Valores seguros para transportar una captura al panel de desarrollo. */
|
|
4
|
+
export type DevtoolsValue = null | boolean | number | string | DevtoolsValue[] | {
|
|
5
|
+
[key: string]: DevtoolsValue;
|
|
6
|
+
};
|
|
7
|
+
/** Estado serializable de una instancia hidratada. */
|
|
8
|
+
export interface DevtoolsInstance {
|
|
9
|
+
id: string;
|
|
10
|
+
componentId: string;
|
|
11
|
+
name: string;
|
|
12
|
+
file: string;
|
|
13
|
+
props: DevtoolsValue;
|
|
14
|
+
signals: Record<string, DevtoolsValue>;
|
|
15
|
+
parentId: string | null;
|
|
16
|
+
childIds: string[];
|
|
17
|
+
status: "hydrated";
|
|
18
|
+
breakpoint: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Cambio reciente del árbol de componentes o de una señal. */
|
|
21
|
+
export interface DevtoolsEvent {
|
|
22
|
+
id: number;
|
|
23
|
+
at: number;
|
|
24
|
+
kind: "mount" | "unmount" | "signal" | "breakpoint" | "select" | "hmr";
|
|
25
|
+
instanceId: string;
|
|
26
|
+
detail?: string;
|
|
27
|
+
/** Identidad del componente conservada incluso después del desmontaje. */
|
|
28
|
+
componentName?: string;
|
|
29
|
+
/** Archivo fuente `.meeko` proporcionado por la compilación de desarrollo. */
|
|
30
|
+
file?: string;
|
|
31
|
+
/** Valor posterior de una señal, copiado con los mismos límites que una captura. */
|
|
32
|
+
value?: DevtoolsValue;
|
|
33
|
+
/** Expresiones de plantilla que dependían de la señal en esta instancia. No son el origen de la escritura. */
|
|
34
|
+
reads?: EffectSource[];
|
|
35
|
+
}
|
|
36
|
+
/** Captura independiente de referencias DOM y de valores no serializables. */
|
|
37
|
+
export interface DevtoolsSnapshot {
|
|
38
|
+
instances: DevtoolsInstance[];
|
|
39
|
+
events: DevtoolsEvent[];
|
|
40
|
+
selectedId: string | null;
|
|
41
|
+
}
|
|
42
|
+
/** Interfaz pública que consume una extensión o un panel de desarrollo. */
|
|
43
|
+
export interface DevtoolsHook {
|
|
44
|
+
snapshot(): DevtoolsSnapshot;
|
|
45
|
+
subscribe(listener: () => void): () => void;
|
|
46
|
+
select(id: string | null): void;
|
|
47
|
+
highlight(id: string | null): void;
|
|
48
|
+
setBreakpoint(id: string, enabled: boolean): void;
|
|
49
|
+
}
|
|
50
|
+
/** Datos que entrega un componente compilado cuando se hidrata en desarrollo. */
|
|
51
|
+
export interface DevtoolsRegistration {
|
|
52
|
+
componentId: string;
|
|
53
|
+
name: string;
|
|
54
|
+
file: string;
|
|
55
|
+
scope: DomScope;
|
|
56
|
+
props: unknown;
|
|
57
|
+
bindings: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
declare global {
|
|
60
|
+
interface Window {
|
|
61
|
+
__MEEKO_DEVTOOLS__?: DevtoolsHook;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Registra señales y ámbito de una instancia hidratada en Vite desarrollo. */
|
|
65
|
+
export declare function registerDevtoolsInstance(input: DevtoolsRegistration): () => void;
|
package/dist/devtools.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { currentEffectOwner, effect, isWritableSignal, signalReadSites, untrack } from "./reactivity.js";
|
|
2
|
+
const active = new Map();
|
|
3
|
+
const identities = new WeakMap();
|
|
4
|
+
const listeners = new Set();
|
|
5
|
+
const recent = [];
|
|
6
|
+
let nextInstance = 1;
|
|
7
|
+
let nextEvent = 1;
|
|
8
|
+
let selectedId = null;
|
|
9
|
+
let highlightedId = null;
|
|
10
|
+
let overlay;
|
|
11
|
+
let highlightObserver;
|
|
12
|
+
let highlightFrame;
|
|
13
|
+
/** Copia datos arbitrarios con profundidad y tamaño acotados sin invocar getters. */
|
|
14
|
+
function safeValue(value, depth = 0, seen = new WeakSet()) {
|
|
15
|
+
if (value === null || typeof value === "boolean")
|
|
16
|
+
return value;
|
|
17
|
+
if (typeof value === "string")
|
|
18
|
+
return value.length > 500 ? `${value.slice(0, 500)}…` : value;
|
|
19
|
+
if (typeof value === "number")
|
|
20
|
+
return Number.isFinite(value) ? value : String(value);
|
|
21
|
+
if (typeof value === "undefined")
|
|
22
|
+
return "[undefined]";
|
|
23
|
+
if (typeof value === "bigint")
|
|
24
|
+
return `${value}n`;
|
|
25
|
+
if (typeof value === "symbol")
|
|
26
|
+
return "[symbol]";
|
|
27
|
+
if (typeof value === "function")
|
|
28
|
+
return "[function]";
|
|
29
|
+
if (typeof value !== "object")
|
|
30
|
+
return "[unknown]";
|
|
31
|
+
if (typeof Node !== "undefined" && value instanceof Node)
|
|
32
|
+
return "[DOM node]";
|
|
33
|
+
try {
|
|
34
|
+
if (typeof Reflect.get(value, "nodeType") === "number" && typeof Reflect.get(value, "nodeName") === "string")
|
|
35
|
+
return "[DOM node]";
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return "[uninspectable]";
|
|
39
|
+
}
|
|
40
|
+
if (seen.has(value))
|
|
41
|
+
return "[circular]";
|
|
42
|
+
if (depth >= 4)
|
|
43
|
+
return "[max depth]";
|
|
44
|
+
if (value instanceof Date)
|
|
45
|
+
return Number.isNaN(value.getTime()) ? "[invalid date]" : value.toISOString();
|
|
46
|
+
if (value instanceof Error)
|
|
47
|
+
return safeValue(`[${value.name}: ${value.message}]`);
|
|
48
|
+
seen.add(value);
|
|
49
|
+
try {
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
const items = [];
|
|
52
|
+
for (let index = 0; index < Math.min(value.length, 50); index++) {
|
|
53
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
54
|
+
items.push(descriptor && "value" in descriptor ? safeValue(descriptor.value, depth + 1, seen) : descriptor ? "[accessor]" : "[empty]");
|
|
55
|
+
}
|
|
56
|
+
if (value.length > 50)
|
|
57
|
+
items.push(`[${value.length - 50} more]`);
|
|
58
|
+
return items;
|
|
59
|
+
}
|
|
60
|
+
const result = Object.create(null);
|
|
61
|
+
const keys = Object.keys(value);
|
|
62
|
+
for (const key of keys.slice(0, 50)) {
|
|
63
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
64
|
+
result[key] = descriptor && "value" in descriptor ? safeValue(descriptor.value, depth + 1, seen) : "[accessor]";
|
|
65
|
+
}
|
|
66
|
+
if (keys.length > 50)
|
|
67
|
+
result["…"] = `[${keys.length - 50} more]`;
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return "[uninspectable]";
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
seen.delete(value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function notify() {
|
|
78
|
+
for (const listener of [...listeners]) {
|
|
79
|
+
try {
|
|
80
|
+
untrack(listener);
|
|
81
|
+
}
|
|
82
|
+
catch { /* Un panel externo no debe romper la aplicación. */ }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function record(kind, instanceId, detail, value, source, reads) {
|
|
86
|
+
const event = { id: nextEvent++, at: Date.now(), kind, instanceId };
|
|
87
|
+
if (detail !== undefined)
|
|
88
|
+
event.detail = detail;
|
|
89
|
+
const entry = source ?? active.get(instanceId);
|
|
90
|
+
if (entry) {
|
|
91
|
+
event.componentName = entry.name;
|
|
92
|
+
event.file = entry.file;
|
|
93
|
+
}
|
|
94
|
+
if (value !== undefined)
|
|
95
|
+
event.value = value;
|
|
96
|
+
if (reads?.length)
|
|
97
|
+
event.reads = reads;
|
|
98
|
+
recent.push(event);
|
|
99
|
+
if (recent.length > 100)
|
|
100
|
+
recent.shift();
|
|
101
|
+
notify();
|
|
102
|
+
}
|
|
103
|
+
function keyOf(scope) {
|
|
104
|
+
return scope.start ?? scope.root;
|
|
105
|
+
}
|
|
106
|
+
function isInside(parent, child) {
|
|
107
|
+
if (parent.id === child.id || parent.key === child.key)
|
|
108
|
+
return false;
|
|
109
|
+
const node = child.key;
|
|
110
|
+
if (!parent.scope.start || !parent.scope.end)
|
|
111
|
+
return parent.scope.root.contains(node);
|
|
112
|
+
const { start, end } = parent.scope;
|
|
113
|
+
return Boolean(start.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
114
|
+
&& Boolean(end.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_PRECEDING);
|
|
115
|
+
}
|
|
116
|
+
function parentOf(child) {
|
|
117
|
+
let parent;
|
|
118
|
+
for (const candidate of active.values()) {
|
|
119
|
+
if (!isInside(candidate, child))
|
|
120
|
+
continue;
|
|
121
|
+
if (!parent || isInside(parent, candidate))
|
|
122
|
+
parent = candidate;
|
|
123
|
+
}
|
|
124
|
+
return parent;
|
|
125
|
+
}
|
|
126
|
+
function bounds(instance) {
|
|
127
|
+
if (!instance.scope.start || !instance.scope.end)
|
|
128
|
+
return instance.scope.root.getBoundingClientRect();
|
|
129
|
+
const range = instance.scope.start.ownerDocument.createRange();
|
|
130
|
+
range.setStartAfter(instance.scope.start);
|
|
131
|
+
range.setEndBefore(instance.scope.end);
|
|
132
|
+
return range.getBoundingClientRect();
|
|
133
|
+
}
|
|
134
|
+
function positionHighlight() {
|
|
135
|
+
const entry = highlightedId ? active.get(highlightedId) : undefined;
|
|
136
|
+
if (!entry || !overlay)
|
|
137
|
+
return;
|
|
138
|
+
try {
|
|
139
|
+
const rect = bounds(entry);
|
|
140
|
+
Object.assign(overlay.style, {
|
|
141
|
+
left: `${rect.left}px`, top: `${rect.top}px`,
|
|
142
|
+
width: `${rect.width}px`, height: `${rect.height}px`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
removeHighlight();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function scheduleHighlight() {
|
|
150
|
+
if (highlightFrame !== undefined)
|
|
151
|
+
return;
|
|
152
|
+
highlightFrame = window.requestAnimationFrame(() => {
|
|
153
|
+
highlightFrame = undefined;
|
|
154
|
+
positionHighlight();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function removeHighlight() {
|
|
158
|
+
window.removeEventListener("scroll", scheduleHighlight, true);
|
|
159
|
+
window.removeEventListener("resize", scheduleHighlight);
|
|
160
|
+
highlightObserver?.disconnect();
|
|
161
|
+
highlightObserver = undefined;
|
|
162
|
+
if (highlightFrame !== undefined)
|
|
163
|
+
window.cancelAnimationFrame(highlightFrame);
|
|
164
|
+
highlightFrame = undefined;
|
|
165
|
+
overlay?.remove();
|
|
166
|
+
overlay = undefined;
|
|
167
|
+
highlightedId = null;
|
|
168
|
+
}
|
|
169
|
+
const hook = {
|
|
170
|
+
snapshot() {
|
|
171
|
+
return untrack(() => {
|
|
172
|
+
const instances = [];
|
|
173
|
+
for (const entry of active.values()) {
|
|
174
|
+
const signals = Object.create(null);
|
|
175
|
+
for (const [name, value] of Object.entries(entry.bindings)) {
|
|
176
|
+
if (isWritableSignal(value))
|
|
177
|
+
signals[name] = safeValue(value.value);
|
|
178
|
+
}
|
|
179
|
+
instances.push({
|
|
180
|
+
id: entry.id,
|
|
181
|
+
componentId: entry.componentId,
|
|
182
|
+
name: entry.name,
|
|
183
|
+
file: entry.file,
|
|
184
|
+
props: safeValue(entry.props),
|
|
185
|
+
signals,
|
|
186
|
+
parentId: parentOf(entry)?.id ?? null,
|
|
187
|
+
childIds: [],
|
|
188
|
+
status: "hydrated",
|
|
189
|
+
breakpoint: entry.breakpoint,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const byId = new Map(instances.map((instance) => [instance.id, instance]));
|
|
193
|
+
for (const instance of instances) {
|
|
194
|
+
if (instance.parentId)
|
|
195
|
+
byId.get(instance.parentId)?.childIds.push(instance.id);
|
|
196
|
+
}
|
|
197
|
+
return { instances, events: recent.map((event) => ({ ...event })), selectedId };
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
subscribe(listener) {
|
|
201
|
+
listeners.add(listener);
|
|
202
|
+
return () => { listeners.delete(listener); };
|
|
203
|
+
},
|
|
204
|
+
select(id) {
|
|
205
|
+
if (id !== null && !active.has(id))
|
|
206
|
+
return;
|
|
207
|
+
selectedId = id;
|
|
208
|
+
record("select", id ?? "");
|
|
209
|
+
},
|
|
210
|
+
highlight(id) {
|
|
211
|
+
removeHighlight();
|
|
212
|
+
if (id === null)
|
|
213
|
+
return;
|
|
214
|
+
const entry = active.get(id);
|
|
215
|
+
if (!entry)
|
|
216
|
+
return;
|
|
217
|
+
const document = entry.key.ownerDocument ?? window.document;
|
|
218
|
+
const element = document.createElement("div");
|
|
219
|
+
element.setAttribute("data-meeko-devtools-highlight", "");
|
|
220
|
+
Object.assign(element.style, {
|
|
221
|
+
position: "fixed",
|
|
222
|
+
outline: "2px solid #7c3aed", background: "rgba(124, 58, 237, 0.12)",
|
|
223
|
+
pointerEvents: "none", zIndex: "2147483647",
|
|
224
|
+
});
|
|
225
|
+
document.body.appendChild(element);
|
|
226
|
+
overlay = element;
|
|
227
|
+
highlightedId = id;
|
|
228
|
+
positionHighlight();
|
|
229
|
+
window.addEventListener("scroll", scheduleHighlight, true);
|
|
230
|
+
window.addEventListener("resize", scheduleHighlight);
|
|
231
|
+
highlightObserver = new window.MutationObserver((mutations) => {
|
|
232
|
+
if (mutations.some((mutation) => mutation.target !== overlay && !overlay?.contains(mutation.target)))
|
|
233
|
+
scheduleHighlight();
|
|
234
|
+
});
|
|
235
|
+
highlightObserver.observe(entry.scope.root, { attributes: true, characterData: true, childList: true, subtree: true });
|
|
236
|
+
},
|
|
237
|
+
setBreakpoint(id, enabled) {
|
|
238
|
+
const entry = active.get(id);
|
|
239
|
+
if (!entry)
|
|
240
|
+
return;
|
|
241
|
+
entry.breakpoint = enabled;
|
|
242
|
+
const identity = identities.get(entry.key);
|
|
243
|
+
if (identity)
|
|
244
|
+
identity.breakpoint = enabled;
|
|
245
|
+
record("breakpoint", id, enabled ? "enabled" : "disabled");
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
function unregister(entry) {
|
|
249
|
+
if (active.get(entry.id) !== entry)
|
|
250
|
+
return;
|
|
251
|
+
for (const stop of entry.stopSignals)
|
|
252
|
+
stop();
|
|
253
|
+
active.delete(entry.id);
|
|
254
|
+
if (selectedId === entry.id)
|
|
255
|
+
selectedId = null;
|
|
256
|
+
if (highlightedId === entry.id)
|
|
257
|
+
removeHighlight();
|
|
258
|
+
record("unmount", entry.id, undefined, undefined, entry);
|
|
259
|
+
}
|
|
260
|
+
/** Registra señales y ámbito de una instancia hidratada en Vite desarrollo. */
|
|
261
|
+
export function registerDevtoolsInstance(input) {
|
|
262
|
+
if (typeof window === "undefined" || typeof Node === "undefined")
|
|
263
|
+
return () => { };
|
|
264
|
+
if (!window.__MEEKO_DEVTOOLS__) {
|
|
265
|
+
window.__MEEKO_DEVTOOLS__ = hook;
|
|
266
|
+
window.dispatchEvent(new window.Event("meeko:devtools-ready"));
|
|
267
|
+
}
|
|
268
|
+
const key = keyOf(input.scope);
|
|
269
|
+
const previous = identities.get(key);
|
|
270
|
+
const identity = previous ?? { id: `meeko-${nextInstance++}`, breakpoint: false };
|
|
271
|
+
identities.set(key, identity);
|
|
272
|
+
const existing = active.get(identity.id);
|
|
273
|
+
if (existing)
|
|
274
|
+
unregister(existing);
|
|
275
|
+
const entry = { ...input, id: identity.id, key, owner: currentEffectOwner(), breakpoint: identity.breakpoint, stopSignals: [] };
|
|
276
|
+
active.set(entry.id, entry);
|
|
277
|
+
for (const [name, value] of Object.entries(input.bindings)) {
|
|
278
|
+
if (!isWritableSignal(value))
|
|
279
|
+
continue;
|
|
280
|
+
const signal = value;
|
|
281
|
+
let initial = true;
|
|
282
|
+
entry.stopSignals.push(effect(() => {
|
|
283
|
+
const current = signal.value;
|
|
284
|
+
if (initial) {
|
|
285
|
+
initial = false;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
record("signal", entry.id, name, safeValue(current), entry, signalReadSites(signal, entry.owner));
|
|
289
|
+
if (entry.breakpoint) {
|
|
290
|
+
debugger;
|
|
291
|
+
}
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
record(previous ? "hmr" : "mount", entry.id);
|
|
295
|
+
return () => { unregister(entry); };
|
|
296
|
+
}
|
package/dist/dom.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Signal } from "./reactivity.js";
|
|
1
|
+
import type { EffectSource, Signal } from "./reactivity.js";
|
|
2
2
|
/** Fragmento de DOM que pertenece a una instancia o a una región dinámica. */
|
|
3
3
|
export interface DomScope {
|
|
4
4
|
root: Element;
|
|
@@ -26,9 +26,9 @@ export declare function renderComponent<Props extends object>(id: number, compon
|
|
|
26
26
|
/** Hidrata un hijo dentro de su rango y actualiza props reactivas con limpieza previa. */
|
|
27
27
|
export declare function bindComponent<Props extends object>(scope: DomScope, id: number, component: ComponentModule<Props>, read: () => Props, location: string): () => void;
|
|
28
28
|
/** Conecta un texto reactivo al marcador SSR y comprueba su valor inicial. */
|
|
29
|
-
export declare function bindText(scope: DomScope, id: number, read: () => unknown, location: string): () => void;
|
|
29
|
+
export declare function bindText(scope: DomScope, id: number, read: () => unknown, location: string, source?: EffectSource): () => void;
|
|
30
30
|
/** Conecta un atributo dinámico al elemento SSR y comprueba su valor inicial. */
|
|
31
|
-
export declare function bindAttribute(scope: DomScope, id: number, name: string, read: () => unknown, location: string): () => void;
|
|
31
|
+
export declare function bindAttribute(scope: DomScope, id: number, name: string, read: () => unknown, location: string, source?: EffectSource): () => void;
|
|
32
32
|
/** Conecta un evento al elemento SSR y entrega su función de limpieza. */
|
|
33
33
|
export declare function bindEvent(scope: DomScope, id: number, name: string, handler: (event: Event) => void): () => void;
|
|
34
34
|
/** Genera los límites estables de una condición en SSR. */
|
|
@@ -39,5 +39,9 @@ export declare function bindIf(scope: DomScope, id: number, read: () => unknown,
|
|
|
39
39
|
export declare function keyToken(key: unknown): string;
|
|
40
40
|
/** Renderiza una lista con límites identificados por claves únicas. */
|
|
41
41
|
export declare function renderFor<T>(id: number, items: Iterable<T>, keyOf: (item: T, index: number) => string | number, render: (item: T, index: number) => string): string;
|
|
42
|
-
/**
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Hidrata una lista y reordena regiones por clave sin reemplazar nodos conservados.
|
|
44
|
+
* El compilador desactiva `trackIndex` sólo cuando el cuerpo de hidratación no lee
|
|
45
|
+
* la señal de índice; las llamadas directas conservan la señal por defecto.
|
|
46
|
+
*/
|
|
47
|
+
export declare function bindFor<T>(scope: DomScope, id: number, read: () => Iterable<T>, keyOf: (item: T, index: number) => string | number, render: (item: T, index: number) => string, hydrate: (item: Signal<T>, index: Signal<number>, inner: DomScope) => () => void, trackIndex?: boolean): () => void;
|
package/dist/dom.js
CHANGED
|
@@ -133,27 +133,44 @@ export function bindComponent(scope, id, component, read, location) {
|
|
|
133
133
|
return () => { stop(); release(); };
|
|
134
134
|
}
|
|
135
135
|
/** Conecta un texto reactivo al marcador SSR y comprueba su valor inicial. */
|
|
136
|
-
export function bindText(scope, id, read, location) {
|
|
136
|
+
export function bindText(scope, id, read, location, source) {
|
|
137
137
|
const marker = findComment(scope, `meeko:text:${id}`);
|
|
138
138
|
const text = marker.nextSibling;
|
|
139
139
|
if (!text || text.nodeType !== 3)
|
|
140
140
|
throw new Error(`Missing text node in ${location}`);
|
|
141
|
-
|
|
141
|
+
const initial = String(read());
|
|
142
|
+
if (text.textContent !== initial)
|
|
142
143
|
throw new Error(`Hydration mismatch in ${location}`);
|
|
143
|
-
|
|
144
|
+
let first = true;
|
|
145
|
+
return effect(() => {
|
|
146
|
+
const next = String(read());
|
|
147
|
+
if (first) {
|
|
148
|
+
first = false;
|
|
149
|
+
if (text.textContent === next)
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
text.textContent = next;
|
|
153
|
+
}, source);
|
|
144
154
|
}
|
|
145
155
|
/** Conecta un atributo dinámico al elemento SSR y comprueba su valor inicial. */
|
|
146
|
-
export function bindAttribute(scope, id, name, read, location) {
|
|
156
|
+
export function bindAttribute(scope, id, name, read, location, source) {
|
|
147
157
|
const target = findElement(scope, `data-meeko-a-${id}`);
|
|
148
|
-
|
|
158
|
+
const initial = attributeValue(name, read());
|
|
159
|
+
if (target.getAttribute(name) !== initial)
|
|
149
160
|
throw new Error(`Hydration mismatch in ${location}`);
|
|
161
|
+
let first = true;
|
|
150
162
|
return effect(() => {
|
|
151
163
|
const next = attributeValue(name, read());
|
|
164
|
+
if (first) {
|
|
165
|
+
first = false;
|
|
166
|
+
if (target.getAttribute(name) === next)
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
152
169
|
if (next === null)
|
|
153
170
|
target.removeAttribute(name);
|
|
154
171
|
else
|
|
155
172
|
target.setAttribute(name, next);
|
|
156
|
-
});
|
|
173
|
+
}, source);
|
|
157
174
|
}
|
|
158
175
|
/** Conecta un evento al elemento SSR y entrega su función de limpieza. */
|
|
159
176
|
export function bindEvent(scope, id, name, handler) {
|
|
@@ -227,11 +244,42 @@ function moveRange(start, end, before) {
|
|
|
227
244
|
}
|
|
228
245
|
parent.insertBefore(fragment, before);
|
|
229
246
|
}
|
|
230
|
-
/**
|
|
231
|
-
|
|
247
|
+
/** Encuentra las posiciones que ya mantienen el orden relativo de la lista anterior. */
|
|
248
|
+
function stableListPositions(indices) {
|
|
249
|
+
const tails = [];
|
|
250
|
+
const previous = new Int32Array(indices.length).fill(-1);
|
|
251
|
+
for (let index = 0; index < indices.length; index++) {
|
|
252
|
+
const oldIndex = indices[index];
|
|
253
|
+
if (oldIndex < 0)
|
|
254
|
+
continue;
|
|
255
|
+
let low = 0;
|
|
256
|
+
let high = tails.length;
|
|
257
|
+
while (low < high) {
|
|
258
|
+
const middle = (low + high) >>> 1;
|
|
259
|
+
if (indices[tails[middle]] < oldIndex)
|
|
260
|
+
low = middle + 1;
|
|
261
|
+
else
|
|
262
|
+
high = middle;
|
|
263
|
+
}
|
|
264
|
+
if (low > 0)
|
|
265
|
+
previous[index] = tails[low - 1];
|
|
266
|
+
tails[low] = index;
|
|
267
|
+
}
|
|
268
|
+
const stable = new Set();
|
|
269
|
+
for (let index = tails.at(-1) ?? -1; index >= 0; index = previous[index])
|
|
270
|
+
stable.add(index);
|
|
271
|
+
return stable;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Hidrata una lista y reordena regiones por clave sin reemplazar nodos conservados.
|
|
275
|
+
* El compilador desactiva `trackIndex` sólo cuando el cuerpo de hidratación no lee
|
|
276
|
+
* la señal de índice; las llamadas directas conservan la señal por defecto.
|
|
277
|
+
*/
|
|
278
|
+
export function bindFor(scope, id, read, keyOf, render, hydrate, trackIndex = true) {
|
|
232
279
|
const outer = region(scope, "for", id);
|
|
233
280
|
const entries = new Map();
|
|
234
281
|
const initial = [...read()];
|
|
282
|
+
let currentKeys = [];
|
|
235
283
|
let cursor = outer.start.nextSibling;
|
|
236
284
|
for (let index = 0; index < initial.length; index++) {
|
|
237
285
|
const item = initial[index];
|
|
@@ -249,23 +297,35 @@ export function bindFor(scope, id, read, keyOf, render, hydrate) {
|
|
|
249
297
|
if (!end)
|
|
250
298
|
throw new Error(`Missing end marker for list key ${key}`);
|
|
251
299
|
const value = signal(item);
|
|
252
|
-
const position = signal(index);
|
|
300
|
+
const position = trackIndex ? signal(index) : undefined;
|
|
253
301
|
const release = createRoot((dispose) => {
|
|
254
302
|
const cleanup = hydrate(value, position, { root: scope.root, start, end: end });
|
|
255
303
|
return () => { cleanup(); dispose(); };
|
|
256
304
|
});
|
|
257
305
|
entries.set(key, { key, start, end: end, value, index: position, release });
|
|
306
|
+
currentKeys.push(key);
|
|
258
307
|
cursor = end.nextSibling;
|
|
259
308
|
}
|
|
260
309
|
if (cursor !== outer.end)
|
|
261
310
|
throw new Error(`Hydration mismatch in list ${id}: extra nodes`);
|
|
311
|
+
let initialItems = initial;
|
|
262
312
|
const stop = effect(() => {
|
|
263
313
|
const items = [...read()];
|
|
314
|
+
if (initialItems) {
|
|
315
|
+
const previous = initialItems;
|
|
316
|
+
initialItems = undefined;
|
|
317
|
+
if (items.length === previous.length && items.every((item, index) => Object.is(item, previous[index]) && keyToken(keyOf(item, index)) === currentKeys[index]))
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
264
320
|
const nextKeys = items.map((item, index) => keyToken(keyOf(item, index)));
|
|
265
|
-
|
|
321
|
+
const nextKeySet = new Set(nextKeys);
|
|
322
|
+
if (nextKeySet.size !== nextKeys.length)
|
|
266
323
|
throw new Error(`Duplicate Meeko list key in region ${id}`);
|
|
324
|
+
const oldPositions = new Map(currentKeys.map((key, index) => [key, index]));
|
|
325
|
+
const oldIndices = nextKeys.map((key) => oldPositions.get(key) ?? -1);
|
|
326
|
+
const stable = stableListPositions(oldIndices);
|
|
267
327
|
for (const [key, entry] of entries) {
|
|
268
|
-
if (
|
|
328
|
+
if (nextKeySet.has(key))
|
|
269
329
|
continue;
|
|
270
330
|
entry.release();
|
|
271
331
|
let node = entry.start;
|
|
@@ -293,7 +353,7 @@ export function bindFor(scope, id, read, keyOf, render, hydrate) {
|
|
|
293
353
|
if (!start)
|
|
294
354
|
throw new Error(`Missing new list marker for ${key}`);
|
|
295
355
|
const value = signal(item);
|
|
296
|
-
const position = signal(index);
|
|
356
|
+
const position = trackIndex ? signal(index) : undefined;
|
|
297
357
|
const release = createRoot((dispose) => {
|
|
298
358
|
const cleanup = hydrate(value, position, { root: scope.root, start: start, end });
|
|
299
359
|
return () => { cleanup(); dispose(); };
|
|
@@ -303,12 +363,14 @@ export function bindFor(scope, id, read, keyOf, render, hydrate) {
|
|
|
303
363
|
}
|
|
304
364
|
else {
|
|
305
365
|
entry.value.value = item;
|
|
306
|
-
entry.index
|
|
366
|
+
if (entry.index)
|
|
367
|
+
entry.index.value = index;
|
|
307
368
|
}
|
|
308
|
-
if (entry.end.nextSibling !== before)
|
|
369
|
+
if (oldIndices[index] !== -1 && !stable.has(index) && entry.end.nextSibling !== before)
|
|
309
370
|
moveRange(entry.start, entry.end, before);
|
|
310
371
|
before = entry.start;
|
|
311
372
|
}
|
|
373
|
+
currentKeys = nextKeys;
|
|
312
374
|
});
|
|
313
375
|
return () => { stop(); for (const entry of entries.values())
|
|
314
376
|
entry.release(); entries.clear(); };
|
package/dist/index.d.ts
CHANGED
|
@@ -5,3 +5,5 @@ export { bindAttribute, bindComponent, bindEvent, bindFor, bindIf, bindText, key
|
|
|
5
5
|
export type { ComponentModule, ComponentRelease, DomScope } from "./dom.js";
|
|
6
6
|
export { enableHotModule, registerHotInstance, registerHotScope, replaceHotModule, replaceHotStyle, restoreSignals, snapshotSignals } from "./hmr.js";
|
|
7
7
|
export type { HotComponent, HotState } from "./hmr.js";
|
|
8
|
+
export { registerDevtoolsInstance } from "./devtools.js";
|
|
9
|
+
export type { DevtoolsEvent, DevtoolsHook, DevtoolsInstance, DevtoolsRegistration, DevtoolsSnapshot, DevtoolsValue } from "./devtools.js";
|
package/dist/index.js
CHANGED
|
@@ -2,3 +2,4 @@ export { batch, computed, createRoot, effect, signal, untrack } from "./reactivi
|
|
|
2
2
|
export { attributeValue, escapeHtml, renderAttribute, serializeJson } from "./server.js";
|
|
3
3
|
export { bindAttribute, bindComponent, bindEvent, bindFor, bindIf, bindText, keyToken, renderComponent, renderFor, renderIf, withStateSnapshot } from "./dom.js";
|
|
4
4
|
export { enableHotModule, registerHotInstance, registerHotScope, replaceHotModule, replaceHotStyle, restoreSignals, snapshotSignals } from "./hmr.js";
|
|
5
|
+
export { registerDevtoolsInstance } from "./devtools.js";
|
package/dist/init.d.ts
ADDED