@meekojs/meeko 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +101 -2
- package/dist/check.d.ts +3 -0
- package/dist/check.js +130 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +43 -0
- package/dist/compiler/mapping.d.ts +16 -0
- package/dist/compiler/mapping.js +48 -0
- package/dist/compiler/sfc.d.ts +43 -0
- package/dist/compiler/sfc.js +70 -0
- package/dist/compiler/template.d.ts +31 -0
- package/dist/compiler/template.js +44 -0
- package/dist/compiler.d.ts +13 -0
- package/dist/compiler.js +404 -0
- package/dist/dom.d.ts +43 -0
- package/dist/dom.js +315 -0
- package/dist/hmr.d.ts +24 -0
- package/dist/hmr.js +137 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/reactivity.d.ts +22 -0
- package/dist/reactivity.js +151 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +79 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +38 -0
- package/dist/vite.d.ts +3 -0
- package/dist/vite.js +47 -0
- package/package.json +69 -4
package/dist/dom.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { createRoot, effect, signal } from "./reactivity.js";
|
|
2
|
+
import { attributeValue, serializeJson } from "./server.js";
|
|
3
|
+
/** Añade una lectura de estado a la limpieza de un componente compilado. */
|
|
4
|
+
export function withStateSnapshot(release, snapshot) {
|
|
5
|
+
return Object.assign(release, { snapshot });
|
|
6
|
+
}
|
|
7
|
+
const componentStart = /^meeko:component:\d+(?::.*)?$/;
|
|
8
|
+
function componentEnd(start) {
|
|
9
|
+
let depth = 0;
|
|
10
|
+
for (let node = start; node; node = node.nextSibling) {
|
|
11
|
+
if (node.nodeType !== 8)
|
|
12
|
+
continue;
|
|
13
|
+
const data = node.data;
|
|
14
|
+
if (componentStart.test(data))
|
|
15
|
+
depth++;
|
|
16
|
+
else if (/^\/meeko:component:\d+$/.test(data) && --depth === 0)
|
|
17
|
+
return node;
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`Missing end marker for ${start.data}`);
|
|
20
|
+
}
|
|
21
|
+
function visit(scope, callback) {
|
|
22
|
+
function siblings(first, stop = null) {
|
|
23
|
+
for (let node = first; node && node !== stop;) {
|
|
24
|
+
if (callback(node))
|
|
25
|
+
return node;
|
|
26
|
+
if (node.nodeType === 8 && componentStart.test(node.data)) {
|
|
27
|
+
const end = componentEnd(node);
|
|
28
|
+
if (callback(end))
|
|
29
|
+
return end;
|
|
30
|
+
node = end.nextSibling;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const found = siblings(node.firstChild);
|
|
34
|
+
if (found)
|
|
35
|
+
return found;
|
|
36
|
+
node = node.nextSibling;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return siblings(scope.start?.nextSibling ?? scope.root.firstChild, scope.end ?? null);
|
|
41
|
+
}
|
|
42
|
+
function findComment(scope, data) {
|
|
43
|
+
const node = visit(scope, (candidate) => candidate.nodeType === 8 && candidate.data === data);
|
|
44
|
+
if (!node)
|
|
45
|
+
throw new Error(`Missing Meeko marker ${data}`);
|
|
46
|
+
return node;
|
|
47
|
+
}
|
|
48
|
+
function findElement(scope, name) {
|
|
49
|
+
const node = visit(scope, (candidate) => candidate.nodeType === 1 && candidate.hasAttribute(name));
|
|
50
|
+
if (!node)
|
|
51
|
+
throw new Error(`Missing Meeko target ${name}`);
|
|
52
|
+
return node;
|
|
53
|
+
}
|
|
54
|
+
function region(scope, kind, id) {
|
|
55
|
+
const start = findComment(scope, `meeko:${kind}:${id}`);
|
|
56
|
+
const end = findComment(scope, `/meeko:${kind}:${id}`);
|
|
57
|
+
if (start.parentNode !== end.parentNode)
|
|
58
|
+
throw new Error(`Invalid Meeko region ${kind}:${id}`);
|
|
59
|
+
return { root: scope.root, start, end };
|
|
60
|
+
}
|
|
61
|
+
function clearRegion(scope) {
|
|
62
|
+
if (!scope.start || !scope.end)
|
|
63
|
+
throw new Error("Expected a bounded Meeko region");
|
|
64
|
+
for (let node = scope.start.nextSibling; node && node !== scope.end;) {
|
|
65
|
+
const next = node.nextSibling;
|
|
66
|
+
node.parentNode?.removeChild(node);
|
|
67
|
+
node = next;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function insertHtml(end, html) {
|
|
71
|
+
const parent = end.parentNode;
|
|
72
|
+
if (!parent)
|
|
73
|
+
throw new Error("Detached Meeko region");
|
|
74
|
+
const range = end.ownerDocument.createRange();
|
|
75
|
+
range.selectNodeContents(parent);
|
|
76
|
+
parent.insertBefore(range.createContextualFragment(html), end);
|
|
77
|
+
}
|
|
78
|
+
function sameProps(first, second) {
|
|
79
|
+
const firstKeys = Object.keys(first);
|
|
80
|
+
const secondKeys = Object.keys(second);
|
|
81
|
+
return firstKeys.length === secondKeys.length && firstKeys.every((key) => Object.hasOwn(second, key) && Object.is(first[key], second[key]));
|
|
82
|
+
}
|
|
83
|
+
/** Renderiza un hijo entre comentarios de ámbito sin añadir un elemento visual. */
|
|
84
|
+
export function renderComponent(id, component, props, transferState = false) {
|
|
85
|
+
if (transferState) {
|
|
86
|
+
if (!component.renderWithState)
|
|
87
|
+
throw new TypeError(`Component ${id} cannot transfer SSR state`);
|
|
88
|
+
const rendered = component.renderWithState(props);
|
|
89
|
+
return `<!--meeko:component:${id}:${encodeURIComponent(serializeJson(rendered.state))}-->${rendered.html}<!--/meeko:component:${id}-->`;
|
|
90
|
+
}
|
|
91
|
+
return `<!--meeko:component:${id}-->${component.renderToString(props)}<!--/meeko:component:${id}-->`;
|
|
92
|
+
}
|
|
93
|
+
function componentRegion(scope, id) {
|
|
94
|
+
const prefix = `meeko:component:${id}`;
|
|
95
|
+
const start = visit(scope, (candidate) => candidate.nodeType === 8 && (candidate.data === prefix || candidate.data.startsWith(`${prefix}:`)));
|
|
96
|
+
if (!start)
|
|
97
|
+
throw new Error(`Missing Meeko marker ${prefix}`);
|
|
98
|
+
const end = findComment(scope, `/meeko:component:${id}`);
|
|
99
|
+
if (start.parentNode !== end.parentNode)
|
|
100
|
+
throw new Error(`Invalid Meeko region component:${id}`);
|
|
101
|
+
const encoded = start.data.slice(prefix.length);
|
|
102
|
+
if (!encoded)
|
|
103
|
+
return { inner: { root: scope.root, start, end } };
|
|
104
|
+
const parsed = JSON.parse(decodeURIComponent(encoded.slice(1)));
|
|
105
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
106
|
+
throw new TypeError(`Invalid Meeko state for component:${id}`);
|
|
107
|
+
return { inner: { root: scope.root, start, end }, state: parsed };
|
|
108
|
+
}
|
|
109
|
+
/** Hidrata un hijo dentro de su rango y actualiza props reactivas con limpieza previa. */
|
|
110
|
+
export function bindComponent(scope, id, component, read, location) {
|
|
111
|
+
let inner;
|
|
112
|
+
let state;
|
|
113
|
+
try {
|
|
114
|
+
({ inner, state } = componentRegion(scope, id));
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
throw new Error(`Hydration mismatch in ${location}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
118
|
+
}
|
|
119
|
+
let props = read();
|
|
120
|
+
let release = component.hydrateScope(inner, props, state);
|
|
121
|
+
const stop = effect(() => {
|
|
122
|
+
const next = read();
|
|
123
|
+
if (sameProps(props, next))
|
|
124
|
+
return;
|
|
125
|
+
const state = release.snapshot?.();
|
|
126
|
+
const html = component.renderToString(next, state);
|
|
127
|
+
release();
|
|
128
|
+
clearRegion(inner);
|
|
129
|
+
insertHtml(inner.end, html);
|
|
130
|
+
release = component.hydrateScope(inner, next, state);
|
|
131
|
+
props = next;
|
|
132
|
+
});
|
|
133
|
+
return () => { stop(); release(); };
|
|
134
|
+
}
|
|
135
|
+
/** Conecta un texto reactivo al marcador SSR y comprueba su valor inicial. */
|
|
136
|
+
export function bindText(scope, id, read, location) {
|
|
137
|
+
const marker = findComment(scope, `meeko:text:${id}`);
|
|
138
|
+
const text = marker.nextSibling;
|
|
139
|
+
if (!text || text.nodeType !== 3)
|
|
140
|
+
throw new Error(`Missing text node in ${location}`);
|
|
141
|
+
if (text.textContent !== String(read()))
|
|
142
|
+
throw new Error(`Hydration mismatch in ${location}`);
|
|
143
|
+
return effect(() => { text.textContent = String(read()); });
|
|
144
|
+
}
|
|
145
|
+
/** Conecta un atributo dinámico al elemento SSR y comprueba su valor inicial. */
|
|
146
|
+
export function bindAttribute(scope, id, name, read, location) {
|
|
147
|
+
const target = findElement(scope, `data-meeko-a-${id}`);
|
|
148
|
+
if (target.getAttribute(name) !== attributeValue(name, read()))
|
|
149
|
+
throw new Error(`Hydration mismatch in ${location}`);
|
|
150
|
+
return effect(() => {
|
|
151
|
+
const next = attributeValue(name, read());
|
|
152
|
+
if (next === null)
|
|
153
|
+
target.removeAttribute(name);
|
|
154
|
+
else
|
|
155
|
+
target.setAttribute(name, next);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/** Conecta un evento al elemento SSR y entrega su función de limpieza. */
|
|
159
|
+
export function bindEvent(scope, id, name, handler) {
|
|
160
|
+
const target = findElement(scope, `data-meeko-e-${id}`);
|
|
161
|
+
target.addEventListener(name, handler);
|
|
162
|
+
return () => target.removeEventListener(name, handler);
|
|
163
|
+
}
|
|
164
|
+
/** Genera los límites estables de una condición en SSR. */
|
|
165
|
+
export function renderIf(id, condition, render) {
|
|
166
|
+
return `<!--meeko:if:${id}-->${condition ? render() : ""}<!--/meeko:if:${id}-->`;
|
|
167
|
+
}
|
|
168
|
+
/** Hidrata una condición y conserva los nodos mientras sigue siendo verdadera. */
|
|
169
|
+
export function bindIf(scope, id, read, render, hydrate) {
|
|
170
|
+
const inner = region(scope, "if", id);
|
|
171
|
+
let active = Boolean(read());
|
|
172
|
+
if (!active && inner.start?.nextSibling !== inner.end)
|
|
173
|
+
throw new Error(`Hydration mismatch in condition ${id}`);
|
|
174
|
+
let release = active ? hydrate(inner) : undefined;
|
|
175
|
+
const stop = effect(() => {
|
|
176
|
+
const next = Boolean(read());
|
|
177
|
+
if (next === active)
|
|
178
|
+
return;
|
|
179
|
+
release?.();
|
|
180
|
+
release = undefined;
|
|
181
|
+
clearRegion(inner);
|
|
182
|
+
if (next) {
|
|
183
|
+
insertHtml(inner.end, render());
|
|
184
|
+
release = hydrate(inner);
|
|
185
|
+
}
|
|
186
|
+
active = next;
|
|
187
|
+
});
|
|
188
|
+
return () => { stop(); release?.(); };
|
|
189
|
+
}
|
|
190
|
+
/** Convierte una clave de lista en un token seguro para comentarios HTML. */
|
|
191
|
+
export function keyToken(key) {
|
|
192
|
+
if (typeof key === "number" && Number.isFinite(key))
|
|
193
|
+
return `n${encodeURIComponent(String(key)).replaceAll("-", "%2D")}`;
|
|
194
|
+
if (typeof key === "string")
|
|
195
|
+
return `s${encodeURIComponent(key).replaceAll("-", "%2D")}`;
|
|
196
|
+
throw new TypeError("Meeko list keys must be strings or finite numbers");
|
|
197
|
+
}
|
|
198
|
+
/** Renderiza una lista con límites identificados por claves únicas. */
|
|
199
|
+
export function renderFor(id, items, keyOf, render) {
|
|
200
|
+
const seen = new Set();
|
|
201
|
+
let html = `<!--meeko:for:${id}-->`;
|
|
202
|
+
let index = 0;
|
|
203
|
+
for (const item of items) {
|
|
204
|
+
const key = keyToken(keyOf(item, index));
|
|
205
|
+
if (seen.has(key))
|
|
206
|
+
throw new Error(`Duplicate Meeko list key ${key}`);
|
|
207
|
+
seen.add(key);
|
|
208
|
+
html += `<!--meeko:item:${id}:${key}-->${render(item, index)}<!--/meeko:item:${id}:${key}-->`;
|
|
209
|
+
index++;
|
|
210
|
+
}
|
|
211
|
+
return `${html}<!--/meeko:for:${id}-->`;
|
|
212
|
+
}
|
|
213
|
+
function moveRange(start, end, before) {
|
|
214
|
+
const parent = before.parentNode;
|
|
215
|
+
if (!parent)
|
|
216
|
+
throw new Error("Detached Meeko list");
|
|
217
|
+
const document = before.ownerDocument;
|
|
218
|
+
if (!document)
|
|
219
|
+
throw new Error("Missing document for Meeko list");
|
|
220
|
+
const fragment = document.createDocumentFragment();
|
|
221
|
+
for (let node = start; node;) {
|
|
222
|
+
const next = node.nextSibling;
|
|
223
|
+
fragment.appendChild(node);
|
|
224
|
+
if (node === end)
|
|
225
|
+
break;
|
|
226
|
+
node = next;
|
|
227
|
+
}
|
|
228
|
+
parent.insertBefore(fragment, before);
|
|
229
|
+
}
|
|
230
|
+
/** Hidrata una lista y reordena regiones por clave sin reemplazar nodos conservados. */
|
|
231
|
+
export function bindFor(scope, id, read, keyOf, render, hydrate) {
|
|
232
|
+
const outer = region(scope, "for", id);
|
|
233
|
+
const entries = new Map();
|
|
234
|
+
const initial = [...read()];
|
|
235
|
+
let cursor = outer.start.nextSibling;
|
|
236
|
+
for (let index = 0; index < initial.length; index++) {
|
|
237
|
+
const item = initial[index];
|
|
238
|
+
const key = keyToken(keyOf(item, index));
|
|
239
|
+
if (entries.has(key))
|
|
240
|
+
throw new Error(`Duplicate Meeko list key ${key}`);
|
|
241
|
+
const startData = `meeko:item:${id}:${key}`;
|
|
242
|
+
const endData = `/meeko:item:${id}:${key}`;
|
|
243
|
+
if (!cursor || cursor.nodeType !== 8 || cursor.data !== startData)
|
|
244
|
+
throw new Error(`Hydration mismatch in list ${id} at key ${key}`);
|
|
245
|
+
const start = cursor;
|
|
246
|
+
let end = start.nextSibling;
|
|
247
|
+
while (end && (end.nodeType !== 8 || end.data !== endData))
|
|
248
|
+
end = end.nextSibling;
|
|
249
|
+
if (!end)
|
|
250
|
+
throw new Error(`Missing end marker for list key ${key}`);
|
|
251
|
+
const value = signal(item);
|
|
252
|
+
const position = signal(index);
|
|
253
|
+
const release = createRoot((dispose) => {
|
|
254
|
+
const cleanup = hydrate(value, position, { root: scope.root, start, end: end });
|
|
255
|
+
return () => { cleanup(); dispose(); };
|
|
256
|
+
});
|
|
257
|
+
entries.set(key, { key, start, end: end, value, index: position, release });
|
|
258
|
+
cursor = end.nextSibling;
|
|
259
|
+
}
|
|
260
|
+
if (cursor !== outer.end)
|
|
261
|
+
throw new Error(`Hydration mismatch in list ${id}: extra nodes`);
|
|
262
|
+
const stop = effect(() => {
|
|
263
|
+
const items = [...read()];
|
|
264
|
+
const nextKeys = items.map((item, index) => keyToken(keyOf(item, index)));
|
|
265
|
+
if (new Set(nextKeys).size !== nextKeys.length)
|
|
266
|
+
throw new Error(`Duplicate Meeko list key in region ${id}`);
|
|
267
|
+
for (const [key, entry] of entries) {
|
|
268
|
+
if (nextKeys.includes(key))
|
|
269
|
+
continue;
|
|
270
|
+
entry.release();
|
|
271
|
+
let node = entry.start;
|
|
272
|
+
while (node) {
|
|
273
|
+
const next = node.nextSibling;
|
|
274
|
+
node.parentNode?.removeChild(node);
|
|
275
|
+
if (node === entry.end)
|
|
276
|
+
break;
|
|
277
|
+
node = next;
|
|
278
|
+
}
|
|
279
|
+
entries.delete(key);
|
|
280
|
+
}
|
|
281
|
+
let before = outer.end;
|
|
282
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
283
|
+
const item = items[index];
|
|
284
|
+
const key = nextKeys[index];
|
|
285
|
+
let entry = entries.get(key);
|
|
286
|
+
if (!entry) {
|
|
287
|
+
const html = `<!--meeko:item:${id}:${key}-->${render(item, index)}<!--/meeko:item:${id}:${key}-->`;
|
|
288
|
+
insertHtml(before, html);
|
|
289
|
+
const end = before.previousSibling;
|
|
290
|
+
let start = end.previousSibling;
|
|
291
|
+
while (start && (start.nodeType !== 8 || start.data !== `meeko:item:${id}:${key}`))
|
|
292
|
+
start = start.previousSibling;
|
|
293
|
+
if (!start)
|
|
294
|
+
throw new Error(`Missing new list marker for ${key}`);
|
|
295
|
+
const value = signal(item);
|
|
296
|
+
const position = signal(index);
|
|
297
|
+
const release = createRoot((dispose) => {
|
|
298
|
+
const cleanup = hydrate(value, position, { root: scope.root, start: start, end });
|
|
299
|
+
return () => { cleanup(); dispose(); };
|
|
300
|
+
});
|
|
301
|
+
entry = { key, start: start, end, value, index: position, release };
|
|
302
|
+
entries.set(key, entry);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
entry.value.value = item;
|
|
306
|
+
entry.index.value = index;
|
|
307
|
+
}
|
|
308
|
+
if (entry.end.nextSibling !== before)
|
|
309
|
+
moveRange(entry.start, entry.end, before);
|
|
310
|
+
before = entry.start;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
return () => { stop(); for (const entry of entries.values())
|
|
314
|
+
entry.release(); entries.clear(); };
|
|
315
|
+
}
|
package/dist/hmr.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DomScope } from "./dom.js";
|
|
2
|
+
/** Valores de señales transferidos en memoria entre versiones de un componente. */
|
|
3
|
+
export type HotState = Record<string, unknown>;
|
|
4
|
+
/** Contrato mínimo de un módulo Meeko reemplazable en desarrollo. */
|
|
5
|
+
export interface HotComponent<Props = unknown> {
|
|
6
|
+
renderToString(props: Props, state?: HotState): string;
|
|
7
|
+
hydrate(root: Element, props: Props, state?: HotState): () => void;
|
|
8
|
+
hydrateScope?(scope: DomScope, props: Props, state?: HotState): () => void;
|
|
9
|
+
css?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Copia solo las señales editables de un setup, sin serializarlas. */
|
|
12
|
+
export declare function snapshotSignals(bindings: Record<string, unknown>): HotState;
|
|
13
|
+
/** Restaura señales compatibles antes de renderizar o hidratar la nueva versión. */
|
|
14
|
+
export declare function restoreSignals(bindings: Record<string, unknown>, state?: HotState): void;
|
|
15
|
+
/** Activa el seguimiento de instancias solo para módulos servidos por Vite. */
|
|
16
|
+
export declare function enableHotModule(id: string): void;
|
|
17
|
+
/** Registra una raíz hidratada y elimina el registro al liberarla. */
|
|
18
|
+
export declare function registerHotInstance<Props>(id: string, root: Element, props: Props, component: HotComponent<Props>, snapshot: () => HotState, dispose: () => void): () => void;
|
|
19
|
+
/** Registra un componente hijo delimitado por comentarios para actualizarlo sin tocar al padre. */
|
|
20
|
+
export declare function registerHotScope<Props>(id: string, scope: DomScope, props: Props, component: HotComponent<Props>, snapshot: () => HotState, dispose: () => void): () => void;
|
|
21
|
+
/** Aplica CSS nuevo después de la hoja virtual cargada por Vite. */
|
|
22
|
+
export declare function replaceHotStyle(id: string, css: string): void;
|
|
23
|
+
/** Sustituye las instancias activas sin recargar la página y conserva señales editables. */
|
|
24
|
+
export declare function replaceHotModule(id: string, next: unknown): void;
|
package/dist/hmr.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { isWritableSignal } from "./reactivity.js";
|
|
2
|
+
const enabled = new Set();
|
|
3
|
+
const instances = new Map();
|
|
4
|
+
/** Copia solo las señales editables de un setup, sin serializarlas. */
|
|
5
|
+
export function snapshotSignals(bindings) {
|
|
6
|
+
const state = {};
|
|
7
|
+
for (const [name, value] of Object.entries(bindings)) {
|
|
8
|
+
if (isWritableSignal(value))
|
|
9
|
+
state[name] = value.value;
|
|
10
|
+
}
|
|
11
|
+
return state;
|
|
12
|
+
}
|
|
13
|
+
/** Restaura señales compatibles antes de renderizar o hidratar la nueva versión. */
|
|
14
|
+
export function restoreSignals(bindings, state) {
|
|
15
|
+
if (!state)
|
|
16
|
+
return;
|
|
17
|
+
for (const [name, value] of Object.entries(state)) {
|
|
18
|
+
const target = bindings[name];
|
|
19
|
+
if (isWritableSignal(target))
|
|
20
|
+
target.value = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Activa el seguimiento de instancias solo para módulos servidos por Vite. */
|
|
24
|
+
export function enableHotModule(id) {
|
|
25
|
+
enabled.add(id);
|
|
26
|
+
}
|
|
27
|
+
function registerHotTarget(id, key, target, props, component, snapshot, dispose) {
|
|
28
|
+
if (!enabled.has(id))
|
|
29
|
+
return dispose;
|
|
30
|
+
const entry = { target, props, component: component, snapshot, dispose };
|
|
31
|
+
let group = instances.get(id);
|
|
32
|
+
if (!group) {
|
|
33
|
+
group = new Map();
|
|
34
|
+
instances.set(id, group);
|
|
35
|
+
}
|
|
36
|
+
const existing = group.get(key);
|
|
37
|
+
if (existing) {
|
|
38
|
+
existing.current = entry;
|
|
39
|
+
return existing.stop;
|
|
40
|
+
}
|
|
41
|
+
const slot = {
|
|
42
|
+
current: entry,
|
|
43
|
+
stop: () => {
|
|
44
|
+
if (group.get(key) !== slot)
|
|
45
|
+
return;
|
|
46
|
+
group.delete(key);
|
|
47
|
+
if (group.size === 0)
|
|
48
|
+
instances.delete(id);
|
|
49
|
+
slot.current.dispose();
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
group.set(key, slot);
|
|
53
|
+
return slot.stop;
|
|
54
|
+
}
|
|
55
|
+
/** Registra una raíz hidratada y elimina el registro al liberarla. */
|
|
56
|
+
export function registerHotInstance(id, root, props, component, snapshot, dispose) {
|
|
57
|
+
return registerHotTarget(id, root, { kind: "root", root }, props, component, snapshot, dispose);
|
|
58
|
+
}
|
|
59
|
+
/** Registra un componente hijo delimitado por comentarios para actualizarlo sin tocar al padre. */
|
|
60
|
+
export function registerHotScope(id, scope, props, component, snapshot, dispose) {
|
|
61
|
+
if (!scope.start || !scope.end)
|
|
62
|
+
throw new Error("Hot child component requires a bounded scope");
|
|
63
|
+
return registerHotTarget(id, scope.start, { kind: "scope", scope }, props, component, snapshot, dispose);
|
|
64
|
+
}
|
|
65
|
+
function scopeRange(scope) {
|
|
66
|
+
if (!scope.start || !scope.end)
|
|
67
|
+
throw new Error("Hot child component lost its markers");
|
|
68
|
+
const range = scope.start.ownerDocument.createRange();
|
|
69
|
+
range.setStartAfter(scope.start);
|
|
70
|
+
range.setEndBefore(scope.end);
|
|
71
|
+
return range;
|
|
72
|
+
}
|
|
73
|
+
function replaceScope(scope, html) {
|
|
74
|
+
const range = scopeRange(scope);
|
|
75
|
+
const content = range.createContextualFragment(html);
|
|
76
|
+
range.deleteContents();
|
|
77
|
+
range.insertNode(content);
|
|
78
|
+
}
|
|
79
|
+
/** Aplica CSS nuevo después de la hoja virtual cargada por Vite. */
|
|
80
|
+
export function replaceHotStyle(id, css) {
|
|
81
|
+
if (typeof document === "undefined")
|
|
82
|
+
return;
|
|
83
|
+
let style = document.querySelector(`style[data-meeko-hot="${id}"]`);
|
|
84
|
+
if (!style) {
|
|
85
|
+
style = document.createElement("style");
|
|
86
|
+
style.dataset.meekoHot = id;
|
|
87
|
+
document.head.appendChild(style);
|
|
88
|
+
}
|
|
89
|
+
style.textContent = css;
|
|
90
|
+
}
|
|
91
|
+
/** Sustituye las instancias activas sin recargar la página y conserva señales editables. */
|
|
92
|
+
export function replaceHotModule(id, next) {
|
|
93
|
+
if (typeof next !== "object" || next === null || !("renderToString" in next) || !("hydrate" in next)
|
|
94
|
+
|| typeof next.renderToString !== "function" || typeof next.hydrate !== "function") {
|
|
95
|
+
throw new TypeError("Invalid Meeko hot module");
|
|
96
|
+
}
|
|
97
|
+
const component = next;
|
|
98
|
+
const prepared = [...(instances.get(id)?.values() ?? [])].map((slot) => {
|
|
99
|
+
const entry = slot.current;
|
|
100
|
+
const state = entry.snapshot();
|
|
101
|
+
const previous = entry.target.kind === "root" ? entry.target.root.innerHTML : scopeRange(entry.target.scope).cloneContents();
|
|
102
|
+
return { slot, entry, state, html: component.renderToString(entry.props, state), previous };
|
|
103
|
+
});
|
|
104
|
+
for (const { slot, entry, state, html, previous } of prepared) {
|
|
105
|
+
entry.dispose();
|
|
106
|
+
if (entry.target.kind === "root")
|
|
107
|
+
entry.target.root.innerHTML = html;
|
|
108
|
+
else
|
|
109
|
+
replaceScope(entry.target.scope, html);
|
|
110
|
+
try {
|
|
111
|
+
if (entry.target.kind === "root")
|
|
112
|
+
component.hydrate(entry.target.root, entry.props, state);
|
|
113
|
+
else {
|
|
114
|
+
if (!component.hydrateScope)
|
|
115
|
+
throw new Error("Updated Meeko child component has no hydrateScope export");
|
|
116
|
+
component.hydrateScope(entry.target.scope, entry.props, state);
|
|
117
|
+
}
|
|
118
|
+
if (slot.current === entry)
|
|
119
|
+
throw new Error("Updated Meeko component did not register its hydrated instance");
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (entry.target.kind === "root")
|
|
123
|
+
entry.target.root.innerHTML = previous;
|
|
124
|
+
else {
|
|
125
|
+
const range = scopeRange(entry.target.scope);
|
|
126
|
+
range.deleteContents();
|
|
127
|
+
range.insertNode(previous);
|
|
128
|
+
}
|
|
129
|
+
if (entry.target.kind === "root")
|
|
130
|
+
entry.component.hydrate(entry.target.root, entry.props, state);
|
|
131
|
+
else
|
|
132
|
+
entry.component.hydrateScope?.(entry.target.scope, entry.props, state);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
replaceHotStyle(id, component.css ?? "");
|
|
137
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { batch, computed, createRoot, effect, signal, untrack } from "./reactivity.js";
|
|
2
|
+
export type { ReadonlySignal, Signal } from "./reactivity.js";
|
|
3
|
+
export { attributeValue, escapeHtml, renderAttribute, serializeJson } from "./server.js";
|
|
4
|
+
export { bindAttribute, bindComponent, bindEvent, bindFor, bindIf, bindText, keyToken, renderComponent, renderFor, renderIf, withStateSnapshot } from "./dom.js";
|
|
5
|
+
export type { ComponentModule, ComponentRelease, DomScope } from "./dom.js";
|
|
6
|
+
export { enableHotModule, registerHotInstance, registerHotScope, replaceHotModule, replaceHotStyle, restoreSignals, snapshotSignals } from "./hmr.js";
|
|
7
|
+
export type { HotComponent, HotState } from "./hmr.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { batch, computed, createRoot, effect, signal, untrack } from "./reactivity.js";
|
|
2
|
+
export { attributeValue, escapeHtml, renderAttribute, serializeJson } from "./server.js";
|
|
3
|
+
export { bindAttribute, bindComponent, bindEvent, bindFor, bindIf, bindText, keyToken, renderComponent, renderFor, renderIf, withStateSnapshot } from "./dom.js";
|
|
4
|
+
export { enableHotModule, registerHotInstance, registerHotScope, replaceHotModule, replaceHotStyle, restoreSignals, snapshotSignals } from "./hmr.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Señal de solo lectura que registra dependencias al consultar su valor. */
|
|
2
|
+
export interface ReadonlySignal<T> {
|
|
3
|
+
readonly value: T;
|
|
4
|
+
}
|
|
5
|
+
/** Señal cuyo valor puede actualizarse de forma reactiva. */
|
|
6
|
+
export interface Signal<T> extends ReadonlySignal<T> {
|
|
7
|
+
value: T;
|
|
8
|
+
}
|
|
9
|
+
/** Identifica señales editables creadas por Meeko para conservarlas durante HMR. */
|
|
10
|
+
export declare function isWritableSignal(value: unknown): value is Signal<unknown>;
|
|
11
|
+
/** Crea un valor observable. Las escrituras iguales no notifican cambios. */
|
|
12
|
+
export declare function signal<T>(initial: T): Signal<T>;
|
|
13
|
+
/** Ejecuta una función cuando cambian las señales leídas por ella. */
|
|
14
|
+
export declare function effect(fn: () => void): () => void;
|
|
15
|
+
/** Deriva un valor de otras señales y lo actualiza cuando cambian. */
|
|
16
|
+
export declare function computed<T>(fn: () => T): ReadonlySignal<T>;
|
|
17
|
+
/** Agrupa varias escrituras en una sola ronda de notificaciones. */
|
|
18
|
+
export declare function batch<T>(fn: () => T): T;
|
|
19
|
+
/** Lee señales sin registrar dependencias en el observador activo. */
|
|
20
|
+
export declare function untrack<T>(fn: () => T): T;
|
|
21
|
+
/** Asocia efectos a una instancia y entrega una función para liberarlos. */
|
|
22
|
+
export declare function createRoot<T>(fn: (dispose: () => void) => T): T;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const writableSignals = new WeakSet();
|
|
2
|
+
const derivedSignals = new WeakSet();
|
|
3
|
+
/** Identifica señales editables creadas por Meeko para conservarlas durante HMR. */
|
|
4
|
+
export function isWritableSignal(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && writableSignals.has(value) && !derivedSignals.has(value);
|
|
6
|
+
}
|
|
7
|
+
let activeObserver;
|
|
8
|
+
let activeOwner;
|
|
9
|
+
let batchDepth = 0;
|
|
10
|
+
let flushing = false;
|
|
11
|
+
const pending = new Set();
|
|
12
|
+
function detach(observer) {
|
|
13
|
+
for (const dependency of observer.dependencies)
|
|
14
|
+
dependency.subscribers.delete(observer);
|
|
15
|
+
observer.dependencies.clear();
|
|
16
|
+
}
|
|
17
|
+
function flush() {
|
|
18
|
+
if (flushing || batchDepth > 0)
|
|
19
|
+
return;
|
|
20
|
+
flushing = true;
|
|
21
|
+
try {
|
|
22
|
+
let iterations = 0;
|
|
23
|
+
while (pending.size > 0) {
|
|
24
|
+
if (++iterations > 1000)
|
|
25
|
+
throw new Error("Reactive update cycle exceeded 1000 iterations");
|
|
26
|
+
const observers = [...pending];
|
|
27
|
+
pending.clear();
|
|
28
|
+
for (const observer of observers)
|
|
29
|
+
if (!observer.disposed)
|
|
30
|
+
observer.run();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
flushing = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Crea un valor observable. Las escrituras iguales no notifican cambios. */
|
|
38
|
+
export function signal(initial) {
|
|
39
|
+
let current = initial;
|
|
40
|
+
const subscribers = new Set();
|
|
41
|
+
const result = {
|
|
42
|
+
subscribers,
|
|
43
|
+
get value() {
|
|
44
|
+
if (activeObserver) {
|
|
45
|
+
subscribers.add(activeObserver);
|
|
46
|
+
activeObserver.dependencies.add(result);
|
|
47
|
+
}
|
|
48
|
+
return current;
|
|
49
|
+
},
|
|
50
|
+
set value(next) {
|
|
51
|
+
if (Object.is(current, next))
|
|
52
|
+
return;
|
|
53
|
+
current = next;
|
|
54
|
+
for (const observer of subscribers)
|
|
55
|
+
pending.add(observer);
|
|
56
|
+
flush();
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
writableSignals.add(result);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
/** Ejecuta una función cuando cambian las señales leídas por ella. */
|
|
63
|
+
export function effect(fn) {
|
|
64
|
+
const owner = activeOwner;
|
|
65
|
+
const observer = {
|
|
66
|
+
dependencies: new Set(),
|
|
67
|
+
disposed: false,
|
|
68
|
+
run() {
|
|
69
|
+
detach(observer);
|
|
70
|
+
const previous = activeObserver;
|
|
71
|
+
const previousOwner = activeOwner;
|
|
72
|
+
activeObserver = observer;
|
|
73
|
+
activeOwner = owner;
|
|
74
|
+
try {
|
|
75
|
+
fn();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
activeObserver = previous;
|
|
79
|
+
activeOwner = previousOwner;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
owner?.observers.add(observer);
|
|
84
|
+
observer.run();
|
|
85
|
+
return () => {
|
|
86
|
+
observer.disposed = true;
|
|
87
|
+
pending.delete(observer);
|
|
88
|
+
detach(observer);
|
|
89
|
+
owner?.observers.delete(observer);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Deriva un valor de otras señales y lo actualiza cuando cambian. */
|
|
93
|
+
export function computed(fn) {
|
|
94
|
+
const result = signal(undefined);
|
|
95
|
+
derivedSignals.add(result);
|
|
96
|
+
effect(() => {
|
|
97
|
+
result.value = fn();
|
|
98
|
+
});
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
/** Agrupa varias escrituras en una sola ronda de notificaciones. */
|
|
102
|
+
export function batch(fn) {
|
|
103
|
+
batchDepth++;
|
|
104
|
+
try {
|
|
105
|
+
return fn();
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
batchDepth--;
|
|
109
|
+
flush();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Lee señales sin registrar dependencias en el observador activo. */
|
|
113
|
+
export function untrack(fn) {
|
|
114
|
+
const previous = activeObserver;
|
|
115
|
+
activeObserver = undefined;
|
|
116
|
+
try {
|
|
117
|
+
return fn();
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
activeObserver = previous;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Asocia efectos a una instancia y entrega una función para liberarlos. */
|
|
124
|
+
export function createRoot(fn) {
|
|
125
|
+
const owner = { observers: new Set(), children: new Set(), parent: activeOwner };
|
|
126
|
+
const previous = activeOwner;
|
|
127
|
+
activeOwner = owner;
|
|
128
|
+
const dispose = () => {
|
|
129
|
+
for (const child of [...owner.children])
|
|
130
|
+
child();
|
|
131
|
+
owner.children.clear();
|
|
132
|
+
for (const observer of owner.observers) {
|
|
133
|
+
observer.disposed = true;
|
|
134
|
+
pending.delete(observer);
|
|
135
|
+
detach(observer);
|
|
136
|
+
}
|
|
137
|
+
owner.observers.clear();
|
|
138
|
+
owner.parent?.children.delete(dispose);
|
|
139
|
+
};
|
|
140
|
+
owner.parent?.children.add(dispose);
|
|
141
|
+
try {
|
|
142
|
+
return fn(dispose);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
dispose();
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
activeOwner = previous;
|
|
150
|
+
}
|
|
151
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Escapa texto dinámico antes de insertarlo en HTML generado por SSR. */
|
|
2
|
+
export declare function escapeHtml(value: unknown): string;
|
|
3
|
+
/** Convierte un valor de atributo dinámico en su forma DOM; null lo elimina. */
|
|
4
|
+
export declare function attributeValue(name: string, value: unknown): string | null;
|
|
5
|
+
/** Genera un atributo HTML dinámico con escape seguro para SSR. */
|
|
6
|
+
export declare function renderAttribute(name: string, value: unknown): string;
|
|
7
|
+
/** Serializa estado y props transferibles al navegador, rechazando valores ambiguos. */
|
|
8
|
+
export declare function serializeJson(value: unknown): string;
|