@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.
@@ -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;
@@ -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
- /** Hidrata una lista y reordena regiones por clave sin reemplazar nodos conservados. */
43
- 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): () => void;
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
- if (text.textContent !== String(read()))
141
+ const initial = String(read());
142
+ if (text.textContent !== initial)
142
143
  throw new Error(`Hydration mismatch in ${location}`);
143
- return effect(() => { text.textContent = String(read()); });
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
- if (target.getAttribute(name) !== attributeValue(name, read()))
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
- /** Hidrata una lista y reordena regiones por clave sin reemplazar nodos conservados. */
231
- export function bindFor(scope, id, read, keyOf, render, hydrate) {
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
- if (new Set(nextKeys).size !== nextKeys.length)
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 (nextKeys.includes(key))
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.value = 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
@@ -0,0 +1,2 @@
1
+ /** Crea un proyecto SSR independiente sin sobrescribir archivos del usuario. */
2
+ export declare function initProject(directory: string): Promise<string>;