@meekojs/meeko 0.0.0-stage → 0.0.2
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 +126 -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/devtools-ui/index.d.ts +14 -0
- package/dist/devtools-ui/index.js +302 -0
- package/dist/devtools.d.ts +56 -0
- package/dist/devtools.js +287 -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 +9 -0
- package/dist/index.js +5 -0
- package/dist/internal.d.ts +13 -0
- package/dist/internal.js +11 -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 +79 -4
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
const styles = `
|
|
2
|
+
:host { all: initial; position: fixed; z-index: 2147483647; right: 16px; bottom: 16px; width: min(680px, calc(100vw - 32px)); color: #e8ecf5; font: 13px/1.45 system-ui, sans-serif; color-scheme: dark; }
|
|
3
|
+
* { box-sizing: border-box; }
|
|
4
|
+
.panel { display: flex; flex-direction: column; max-height: min(560px, calc(100vh - 32px)); overflow: hidden; border: 1px solid #475569; border-radius: 12px; background: #101827; box-shadow: 0 16px 48px #0009; }
|
|
5
|
+
header { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid #334155; }
|
|
6
|
+
header strong { font-size: 14px; margin-right: auto; }
|
|
7
|
+
.mobile-switch { display: none; gap: 6px; padding: 8px 12px; border-bottom: 1px solid #334155; }
|
|
8
|
+
.mobile-switch button { flex: 1; }
|
|
9
|
+
.content { display: grid; grid-template-columns: minmax(170px, 0.9fr) minmax(0, 1.6fr); min-height: 220px; overflow: auto; }
|
|
10
|
+
section { min-width: 0; padding: 12px; }
|
|
11
|
+
section + section { border-left: 1px solid #334155; }
|
|
12
|
+
h2, h3 { margin: 0 0 8px; font-size: 13px; }
|
|
13
|
+
h3 { margin-top: 12px; }
|
|
14
|
+
p { margin: 0 0 8px; color: #aab7ca; }
|
|
15
|
+
ul, ol { margin: 0; padding: 0; list-style: none; }
|
|
16
|
+
li + li { margin-top: 6px; }
|
|
17
|
+
button { appearance: none; border: 1px solid #52657d; border-radius: 6px; background: #202d40; color: inherit; padding: 5px 8px; font: inherit; cursor: pointer; }
|
|
18
|
+
button:hover { background: #30425d; }
|
|
19
|
+
button:focus-visible { outline: 2px solid #79b8ff; outline-offset: 2px; }
|
|
20
|
+
button[aria-pressed="true"] { border-color: #79b8ff; background: #25476c; }
|
|
21
|
+
.tree-node { margin: 3px 0; }
|
|
22
|
+
.tree-node > button { width: 100%; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
23
|
+
.tree-children { margin: 4px 0 6px 8px; padding-left: 12px; border-left: 1px solid #52657d; }
|
|
24
|
+
.actions { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0; }
|
|
25
|
+
.muted { color: #aab7ca; }
|
|
26
|
+
.path { overflow-wrap: anywhere; }
|
|
27
|
+
pre { margin: 4px 0 0; padding: 8px; max-height: 130px; overflow: auto; border-radius: 6px; background: #0a111e; color: #dbeafe; font: 12px/1.45 ui-monospace, SFMono-Regular, Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
28
|
+
.events { border-top: 1px solid #334155; padding: 10px 12px; max-height: 130px; overflow: auto; }
|
|
29
|
+
.events li { overflow-wrap: anywhere; }
|
|
30
|
+
.status { padding: 7px 12px; border-top: 1px solid #334155; color: #aab7ca; }
|
|
31
|
+
@media (max-width: 540px) {
|
|
32
|
+
.mobile-switch { display: flex; }
|
|
33
|
+
.content { display: block; min-height: 0; }
|
|
34
|
+
.content[data-view="instances"] .detail { display: none; }
|
|
35
|
+
.content[data-view="detail"] .instances { display: none; }
|
|
36
|
+
section + section { border-left: 0; }
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
/** Formatea datos arbitrarios sin insertar HTML procedente de la aplicación. */
|
|
40
|
+
function formatValue(value) {
|
|
41
|
+
const seen = new WeakSet();
|
|
42
|
+
try {
|
|
43
|
+
const result = JSON.stringify(value, (_key, current) => {
|
|
44
|
+
if (typeof current === "bigint")
|
|
45
|
+
return `${current}n`;
|
|
46
|
+
if (typeof current === "function")
|
|
47
|
+
return "[función]";
|
|
48
|
+
if (typeof current === "symbol")
|
|
49
|
+
return String(current);
|
|
50
|
+
if (typeof current === "object" && current !== null) {
|
|
51
|
+
if (seen.has(current))
|
|
52
|
+
return "[circular]";
|
|
53
|
+
seen.add(current);
|
|
54
|
+
}
|
|
55
|
+
return current;
|
|
56
|
+
}, 2);
|
|
57
|
+
return (result ?? String(value)).slice(0, 8000);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return "[valor no representable]";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Crea un elemento con texto seguro y una clase opcional. */
|
|
64
|
+
function element(document, tag, text, className) {
|
|
65
|
+
const node = document.createElement(tag);
|
|
66
|
+
if (text !== undefined)
|
|
67
|
+
node.textContent = text;
|
|
68
|
+
if (className)
|
|
69
|
+
node.className = className;
|
|
70
|
+
return node;
|
|
71
|
+
}
|
|
72
|
+
/** Muestra el momento relativo comunicado por el runtime. */
|
|
73
|
+
function eventLabel(event) {
|
|
74
|
+
const kinds = {
|
|
75
|
+
mount: "Montaje",
|
|
76
|
+
unmount: "Desmontaje",
|
|
77
|
+
signal: "Señal",
|
|
78
|
+
breakpoint: "Punto de parada",
|
|
79
|
+
select: "Selección",
|
|
80
|
+
hmr: "HMR",
|
|
81
|
+
};
|
|
82
|
+
const time = Number.isFinite(event.at) ? new Date(event.at).toLocaleTimeString("es", { hour12: false }) : "hora desconocida";
|
|
83
|
+
const detail = event.detail ? ` · ${event.detail}` : "";
|
|
84
|
+
return `${time} · ${kinds[event.kind]} · ${event.instanceId}${detail}`;
|
|
85
|
+
}
|
|
86
|
+
/** Exige un documento conectado a una ventana para registrar eventos del hook. */
|
|
87
|
+
function requireView(document) {
|
|
88
|
+
const view = document.defaultView;
|
|
89
|
+
if (!view)
|
|
90
|
+
throw new Error("Meeko DevTools requires a browser document");
|
|
91
|
+
return view;
|
|
92
|
+
}
|
|
93
|
+
/** Monta una interfaz aislada en Shadow DOM para inspeccionar instancias Meeko. */
|
|
94
|
+
export function mountDevtoolsPanel(options = {}) {
|
|
95
|
+
const container = options.container ?? document.body;
|
|
96
|
+
const ownerDocument = container.ownerDocument;
|
|
97
|
+
const view = requireView(ownerDocument);
|
|
98
|
+
const host = element(ownerDocument, "div");
|
|
99
|
+
host.setAttribute("data-meeko-devtools-panel", "");
|
|
100
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
101
|
+
const style = element(ownerDocument, "style", styles);
|
|
102
|
+
const panel = element(ownerDocument, "div", undefined, "panel");
|
|
103
|
+
const header = element(ownerDocument, "header");
|
|
104
|
+
const title = element(ownerDocument, "strong", "Meeko DevTools");
|
|
105
|
+
const refreshButton = element(ownerDocument, "button", "Actualizar");
|
|
106
|
+
refreshButton.type = "button";
|
|
107
|
+
refreshButton.setAttribute("aria-label", "Actualizar instancias de Meeko");
|
|
108
|
+
const closeButton = element(ownerDocument, "button", "Cerrar");
|
|
109
|
+
closeButton.type = "button";
|
|
110
|
+
closeButton.setAttribute("aria-label", "Cerrar Meeko DevTools");
|
|
111
|
+
header.append(title, refreshButton, closeButton);
|
|
112
|
+
const mobileSwitch = element(ownerDocument, "nav", undefined, "mobile-switch");
|
|
113
|
+
mobileSwitch.setAttribute("aria-label", "Vistas de Meeko DevTools");
|
|
114
|
+
const instancesButton = element(ownerDocument, "button", "Instancias");
|
|
115
|
+
instancesButton.type = "button";
|
|
116
|
+
instancesButton.setAttribute("aria-controls", "meeko-devtools-instances");
|
|
117
|
+
const detailButton = element(ownerDocument, "button", "Detalle");
|
|
118
|
+
detailButton.type = "button";
|
|
119
|
+
detailButton.setAttribute("aria-controls", "meeko-devtools-detail");
|
|
120
|
+
mobileSwitch.append(instancesButton, detailButton);
|
|
121
|
+
const main = element(ownerDocument, "div", undefined, "content");
|
|
122
|
+
const instancesSection = element(ownerDocument, "section", undefined, "instances");
|
|
123
|
+
instancesSection.id = "meeko-devtools-instances";
|
|
124
|
+
const instancesTitle = element(ownerDocument, "h2", "Instancias");
|
|
125
|
+
const instancesList = element(ownerDocument, "ul");
|
|
126
|
+
instancesList.setAttribute("aria-label", "Instancias de Meeko");
|
|
127
|
+
instancesSection.append(instancesTitle, instancesList);
|
|
128
|
+
const detailSection = element(ownerDocument, "section", undefined, "detail");
|
|
129
|
+
detailSection.id = "meeko-devtools-detail";
|
|
130
|
+
detailSection.setAttribute("aria-label", "Detalle de instancia");
|
|
131
|
+
main.append(instancesSection, detailSection);
|
|
132
|
+
const eventsSection = element(ownerDocument, "section", undefined, "events");
|
|
133
|
+
const eventsTitle = element(ownerDocument, "h2", "Eventos recientes");
|
|
134
|
+
const eventsList = element(ownerDocument, "ol");
|
|
135
|
+
eventsList.setAttribute("aria-label", "Eventos recientes de Meeko");
|
|
136
|
+
eventsSection.append(eventsTitle, eventsList);
|
|
137
|
+
const status = element(ownerDocument, "div", undefined, "status");
|
|
138
|
+
status.setAttribute("role", "status");
|
|
139
|
+
status.setAttribute("aria-live", "polite");
|
|
140
|
+
panel.append(header, mobileSwitch, main, eventsSection, status);
|
|
141
|
+
shadow.append(style, panel);
|
|
142
|
+
container.appendChild(host);
|
|
143
|
+
let hook;
|
|
144
|
+
let unsubscribe;
|
|
145
|
+
let highlightedId = null;
|
|
146
|
+
let mounted = true;
|
|
147
|
+
let mobileView = "instances";
|
|
148
|
+
/** Cambia la vista móvil y comunica el botón activo a tecnologías de asistencia. */
|
|
149
|
+
function showMobileView(next) {
|
|
150
|
+
mobileView = next;
|
|
151
|
+
main.dataset.view = mobileView;
|
|
152
|
+
instancesButton.setAttribute("aria-pressed", String(mobileView === "instances"));
|
|
153
|
+
detailButton.setAttribute("aria-pressed", String(mobileView === "detail"));
|
|
154
|
+
}
|
|
155
|
+
showMobileView(mobileView);
|
|
156
|
+
instancesButton.addEventListener("click", () => showMobileView("instances"));
|
|
157
|
+
detailButton.addEventListener("click", () => showMobileView("detail"));
|
|
158
|
+
/** Conserva el foco al reconstruir una fila después de recibir una notificación. */
|
|
159
|
+
function restoreFocus(id, action) {
|
|
160
|
+
if (!id || !action)
|
|
161
|
+
return;
|
|
162
|
+
for (const button of shadow.querySelectorAll("button[data-instance-id]")) {
|
|
163
|
+
if (button.dataset.instanceId === id && button.dataset.action === action) {
|
|
164
|
+
button.focus();
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Reconstruye sólo el contenido variable usando nodos de texto seguros. */
|
|
170
|
+
function render() {
|
|
171
|
+
if (!mounted)
|
|
172
|
+
return;
|
|
173
|
+
const active = shadow.activeElement;
|
|
174
|
+
const focusId = active?.dataset.instanceId;
|
|
175
|
+
const focusAction = active?.dataset.action;
|
|
176
|
+
instancesList.replaceChildren();
|
|
177
|
+
detailSection.replaceChildren();
|
|
178
|
+
eventsList.replaceChildren();
|
|
179
|
+
if (!hook) {
|
|
180
|
+
status.textContent = "Hook no disponible. Abre una página Meeko con Vite en desarrollo y pulsa Actualizar.";
|
|
181
|
+
instancesList.append(element(ownerDocument, "li", "No hay instancias disponibles.", "muted"));
|
|
182
|
+
detailSection.append(element(ownerDocument, "p", "El runtime de desarrollo aún no está conectado."));
|
|
183
|
+
eventsList.append(element(ownerDocument, "li", "Sin eventos.", "muted"));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
let snapshot;
|
|
187
|
+
try {
|
|
188
|
+
snapshot = hook.snapshot();
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
status.textContent = `No se pudo leer el hook: ${error instanceof Error ? error.message : String(error)}`;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
status.textContent = `${snapshot.instances.length} instancia(s) · ${snapshot.events.length} evento(s)`;
|
|
195
|
+
if (snapshot.instances.length === 0)
|
|
196
|
+
instancesList.append(element(ownerDocument, "li", "No hay componentes hidratados.", "muted"));
|
|
197
|
+
const byId = new Map(snapshot.instances.map((instance) => [instance.id, instance]));
|
|
198
|
+
const rendered = new Set();
|
|
199
|
+
function appendInstance(instance, list) {
|
|
200
|
+
if (rendered.has(instance.id))
|
|
201
|
+
return;
|
|
202
|
+
rendered.add(instance.id);
|
|
203
|
+
const row = element(ownerDocument, "li", undefined, "tree-node");
|
|
204
|
+
const selectButton = element(ownerDocument, "button", instance.name || instance.componentId);
|
|
205
|
+
selectButton.type = "button";
|
|
206
|
+
selectButton.title = `${instance.name} · ${instance.file}`;
|
|
207
|
+
selectButton.setAttribute("aria-label", `Seleccionar ${instance.name || instance.componentId}`);
|
|
208
|
+
selectButton.dataset.instanceId = instance.id;
|
|
209
|
+
selectButton.dataset.action = "select";
|
|
210
|
+
selectButton.setAttribute("aria-pressed", String(snapshot.selectedId === instance.id));
|
|
211
|
+
selectButton.addEventListener("click", () => {
|
|
212
|
+
hook?.select(instance.id);
|
|
213
|
+
showMobileView("detail");
|
|
214
|
+
render();
|
|
215
|
+
if (view.matchMedia("(max-width: 540px)").matches)
|
|
216
|
+
detailButton.focus();
|
|
217
|
+
});
|
|
218
|
+
row.append(selectButton);
|
|
219
|
+
list.append(row);
|
|
220
|
+
const children = instance.childIds.map((id) => byId.get(id)).filter((child) => child !== undefined);
|
|
221
|
+
if (children.length > 0) {
|
|
222
|
+
const nested = element(ownerDocument, "ul", undefined, "tree-children");
|
|
223
|
+
for (const child of children)
|
|
224
|
+
appendInstance(child, nested);
|
|
225
|
+
if (nested.childElementCount > 0)
|
|
226
|
+
row.append(nested);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const instance of snapshot.instances)
|
|
230
|
+
if (!instance.parentId || !byId.has(instance.parentId))
|
|
231
|
+
appendInstance(instance, instancesList);
|
|
232
|
+
for (const instance of snapshot.instances)
|
|
233
|
+
appendInstance(instance, instancesList);
|
|
234
|
+
const selected = snapshot.instances.find((instance) => instance.id === snapshot.selectedId);
|
|
235
|
+
if (selected) {
|
|
236
|
+
detailSection.append(element(ownerDocument, "h2", selected.name || selected.componentId));
|
|
237
|
+
detailSection.append(element(ownerDocument, "p", selected.file, "path"));
|
|
238
|
+
detailSection.append(element(ownerDocument, "p", `ID: ${selected.id} · Estado: ${selected.status === "hydrated" ? "Hidratado" : selected.status} · Padre: ${selected.parentId ?? "ninguno"} · Hijos: ${selected.childIds.length}`));
|
|
239
|
+
const actions = element(ownerDocument, "div", undefined, "actions");
|
|
240
|
+
const highlightButton = element(ownerDocument, "button", highlightedId === selected.id ? "Quitar resaltado" : "Resaltar en página");
|
|
241
|
+
highlightButton.type = "button";
|
|
242
|
+
highlightButton.setAttribute("aria-pressed", String(highlightedId === selected.id));
|
|
243
|
+
highlightButton.dataset.instanceId = selected.id;
|
|
244
|
+
highlightButton.dataset.action = "highlight";
|
|
245
|
+
highlightButton.addEventListener("click", () => {
|
|
246
|
+
highlightedId = highlightedId === selected.id ? null : selected.id;
|
|
247
|
+
hook?.highlight(highlightedId);
|
|
248
|
+
render();
|
|
249
|
+
});
|
|
250
|
+
const breakpointButton = element(ownerDocument, "button", selected.breakpoint ? "Desactivar punto de parada" : "Activar punto de parada");
|
|
251
|
+
breakpointButton.type = "button";
|
|
252
|
+
breakpointButton.setAttribute("aria-pressed", String(selected.breakpoint));
|
|
253
|
+
breakpointButton.dataset.instanceId = selected.id;
|
|
254
|
+
breakpointButton.dataset.action = "breakpoint";
|
|
255
|
+
breakpointButton.addEventListener("click", () => {
|
|
256
|
+
hook?.setBreakpoint(selected.id, !selected.breakpoint);
|
|
257
|
+
render();
|
|
258
|
+
});
|
|
259
|
+
actions.append(highlightButton, breakpointButton);
|
|
260
|
+
detailSection.append(actions, element(ownerDocument, "h3", "Props"), element(ownerDocument, "pre", formatValue(selected.props)), element(ownerDocument, "h3", "Señales"), element(ownerDocument, "pre", formatValue(selected.signals)));
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
detailSection.append(element(ownerDocument, "p", "Selecciona una instancia para ver props y señales."));
|
|
264
|
+
}
|
|
265
|
+
const recent = snapshot.events.slice(-30).reverse();
|
|
266
|
+
if (recent.length === 0)
|
|
267
|
+
eventsList.append(element(ownerDocument, "li", "Sin eventos.", "muted"));
|
|
268
|
+
for (const event of recent)
|
|
269
|
+
eventsList.append(element(ownerDocument, "li", eventLabel(event)));
|
|
270
|
+
restoreFocus(focusId, focusAction);
|
|
271
|
+
}
|
|
272
|
+
/** Descubre el hook al inicio, al recibir el evento de disponibilidad o al pulsar Actualizar. */
|
|
273
|
+
function connect() {
|
|
274
|
+
if (!mounted)
|
|
275
|
+
return;
|
|
276
|
+
const next = view.__MEEKO_DEVTOOLS__;
|
|
277
|
+
if (next !== hook) {
|
|
278
|
+
unsubscribe?.();
|
|
279
|
+
hook = next;
|
|
280
|
+
unsubscribe = hook?.subscribe(render);
|
|
281
|
+
}
|
|
282
|
+
render();
|
|
283
|
+
}
|
|
284
|
+
function unmount() {
|
|
285
|
+
if (!mounted)
|
|
286
|
+
return;
|
|
287
|
+
mounted = false;
|
|
288
|
+
unsubscribe?.();
|
|
289
|
+
view.removeEventListener("meeko:devtools-ready", connect);
|
|
290
|
+
refreshButton.removeEventListener("click", connect);
|
|
291
|
+
closeButton.removeEventListener("click", unmount);
|
|
292
|
+
if (highlightedId)
|
|
293
|
+
hook?.highlight(null);
|
|
294
|
+
host.remove();
|
|
295
|
+
hook = undefined;
|
|
296
|
+
}
|
|
297
|
+
view.addEventListener("meeko:devtools-ready", connect);
|
|
298
|
+
refreshButton.addEventListener("click", connect);
|
|
299
|
+
closeButton.addEventListener("click", unmount);
|
|
300
|
+
connect();
|
|
301
|
+
return { refresh: connect, unmount };
|
|
302
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { DomScope } from "./dom.js";
|
|
2
|
+
/** Valores seguros para transportar una captura al panel de desarrollo. */
|
|
3
|
+
export type DevtoolsValue = null | boolean | number | string | DevtoolsValue[] | {
|
|
4
|
+
[key: string]: DevtoolsValue;
|
|
5
|
+
};
|
|
6
|
+
/** Estado serializable de una instancia hidratada. */
|
|
7
|
+
export interface DevtoolsInstance {
|
|
8
|
+
id: string;
|
|
9
|
+
componentId: string;
|
|
10
|
+
name: string;
|
|
11
|
+
file: string;
|
|
12
|
+
props: DevtoolsValue;
|
|
13
|
+
signals: Record<string, DevtoolsValue>;
|
|
14
|
+
parentId: string | null;
|
|
15
|
+
childIds: string[];
|
|
16
|
+
status: "hydrated";
|
|
17
|
+
breakpoint: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** Cambio reciente del árbol de componentes o de una señal. */
|
|
20
|
+
export interface DevtoolsEvent {
|
|
21
|
+
id: number;
|
|
22
|
+
at: number;
|
|
23
|
+
kind: "mount" | "unmount" | "signal" | "breakpoint" | "select" | "hmr";
|
|
24
|
+
instanceId: string;
|
|
25
|
+
detail?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Captura independiente de referencias DOM y de valores no serializables. */
|
|
28
|
+
export interface DevtoolsSnapshot {
|
|
29
|
+
instances: DevtoolsInstance[];
|
|
30
|
+
events: DevtoolsEvent[];
|
|
31
|
+
selectedId: string | null;
|
|
32
|
+
}
|
|
33
|
+
/** Interfaz pública que consume una extensión o un panel de desarrollo. */
|
|
34
|
+
export interface DevtoolsHook {
|
|
35
|
+
snapshot(): DevtoolsSnapshot;
|
|
36
|
+
subscribe(listener: () => void): () => void;
|
|
37
|
+
select(id: string | null): void;
|
|
38
|
+
highlight(id: string | null): void;
|
|
39
|
+
setBreakpoint(id: string, enabled: boolean): void;
|
|
40
|
+
}
|
|
41
|
+
/** Datos que entrega un componente compilado cuando se hidrata en desarrollo. */
|
|
42
|
+
export interface DevtoolsRegistration {
|
|
43
|
+
componentId: string;
|
|
44
|
+
name: string;
|
|
45
|
+
file: string;
|
|
46
|
+
scope: DomScope;
|
|
47
|
+
props: unknown;
|
|
48
|
+
bindings: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
declare global {
|
|
51
|
+
interface Window {
|
|
52
|
+
__MEEKO_DEVTOOLS__?: DevtoolsHook;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Registra señales y ámbito de una instancia hidratada en Vite desarrollo. */
|
|
56
|
+
export declare function registerDevtoolsInstance(input: DevtoolsRegistration): () => void;
|
package/dist/devtools.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { effect, isWritableSignal, 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) {
|
|
86
|
+
const event = { id: nextEvent++, at: Date.now(), kind, instanceId };
|
|
87
|
+
if (detail !== undefined)
|
|
88
|
+
event.detail = detail;
|
|
89
|
+
recent.push(event);
|
|
90
|
+
if (recent.length > 100)
|
|
91
|
+
recent.shift();
|
|
92
|
+
notify();
|
|
93
|
+
}
|
|
94
|
+
function keyOf(scope) {
|
|
95
|
+
return scope.start ?? scope.root;
|
|
96
|
+
}
|
|
97
|
+
function isInside(parent, child) {
|
|
98
|
+
if (parent.id === child.id || parent.key === child.key)
|
|
99
|
+
return false;
|
|
100
|
+
const node = child.key;
|
|
101
|
+
if (!parent.scope.start || !parent.scope.end)
|
|
102
|
+
return parent.scope.root.contains(node);
|
|
103
|
+
const { start, end } = parent.scope;
|
|
104
|
+
return Boolean(start.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
105
|
+
&& Boolean(end.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_PRECEDING);
|
|
106
|
+
}
|
|
107
|
+
function parentOf(child) {
|
|
108
|
+
let parent;
|
|
109
|
+
for (const candidate of active.values()) {
|
|
110
|
+
if (!isInside(candidate, child))
|
|
111
|
+
continue;
|
|
112
|
+
if (!parent || isInside(parent, candidate))
|
|
113
|
+
parent = candidate;
|
|
114
|
+
}
|
|
115
|
+
return parent;
|
|
116
|
+
}
|
|
117
|
+
function bounds(instance) {
|
|
118
|
+
if (!instance.scope.start || !instance.scope.end)
|
|
119
|
+
return instance.scope.root.getBoundingClientRect();
|
|
120
|
+
const range = instance.scope.start.ownerDocument.createRange();
|
|
121
|
+
range.setStartAfter(instance.scope.start);
|
|
122
|
+
range.setEndBefore(instance.scope.end);
|
|
123
|
+
return range.getBoundingClientRect();
|
|
124
|
+
}
|
|
125
|
+
function positionHighlight() {
|
|
126
|
+
const entry = highlightedId ? active.get(highlightedId) : undefined;
|
|
127
|
+
if (!entry || !overlay)
|
|
128
|
+
return;
|
|
129
|
+
try {
|
|
130
|
+
const rect = bounds(entry);
|
|
131
|
+
Object.assign(overlay.style, {
|
|
132
|
+
left: `${rect.left}px`, top: `${rect.top}px`,
|
|
133
|
+
width: `${rect.width}px`, height: `${rect.height}px`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
removeHighlight();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function scheduleHighlight() {
|
|
141
|
+
if (highlightFrame !== undefined)
|
|
142
|
+
return;
|
|
143
|
+
highlightFrame = window.requestAnimationFrame(() => {
|
|
144
|
+
highlightFrame = undefined;
|
|
145
|
+
positionHighlight();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function removeHighlight() {
|
|
149
|
+
window.removeEventListener("scroll", scheduleHighlight, true);
|
|
150
|
+
window.removeEventListener("resize", scheduleHighlight);
|
|
151
|
+
highlightObserver?.disconnect();
|
|
152
|
+
highlightObserver = undefined;
|
|
153
|
+
if (highlightFrame !== undefined)
|
|
154
|
+
window.cancelAnimationFrame(highlightFrame);
|
|
155
|
+
highlightFrame = undefined;
|
|
156
|
+
overlay?.remove();
|
|
157
|
+
overlay = undefined;
|
|
158
|
+
highlightedId = null;
|
|
159
|
+
}
|
|
160
|
+
const hook = {
|
|
161
|
+
snapshot() {
|
|
162
|
+
return untrack(() => {
|
|
163
|
+
const instances = [];
|
|
164
|
+
for (const entry of active.values()) {
|
|
165
|
+
const signals = Object.create(null);
|
|
166
|
+
for (const [name, value] of Object.entries(entry.bindings)) {
|
|
167
|
+
if (isWritableSignal(value))
|
|
168
|
+
signals[name] = safeValue(value.value);
|
|
169
|
+
}
|
|
170
|
+
instances.push({
|
|
171
|
+
id: entry.id,
|
|
172
|
+
componentId: entry.componentId,
|
|
173
|
+
name: entry.name,
|
|
174
|
+
file: entry.file,
|
|
175
|
+
props: safeValue(entry.props),
|
|
176
|
+
signals,
|
|
177
|
+
parentId: parentOf(entry)?.id ?? null,
|
|
178
|
+
childIds: [],
|
|
179
|
+
status: "hydrated",
|
|
180
|
+
breakpoint: entry.breakpoint,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const byId = new Map(instances.map((instance) => [instance.id, instance]));
|
|
184
|
+
for (const instance of instances) {
|
|
185
|
+
if (instance.parentId)
|
|
186
|
+
byId.get(instance.parentId)?.childIds.push(instance.id);
|
|
187
|
+
}
|
|
188
|
+
return { instances, events: recent.map((event) => ({ ...event })), selectedId };
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
subscribe(listener) {
|
|
192
|
+
listeners.add(listener);
|
|
193
|
+
return () => { listeners.delete(listener); };
|
|
194
|
+
},
|
|
195
|
+
select(id) {
|
|
196
|
+
if (id !== null && !active.has(id))
|
|
197
|
+
return;
|
|
198
|
+
selectedId = id;
|
|
199
|
+
record("select", id ?? "");
|
|
200
|
+
},
|
|
201
|
+
highlight(id) {
|
|
202
|
+
removeHighlight();
|
|
203
|
+
if (id === null)
|
|
204
|
+
return;
|
|
205
|
+
const entry = active.get(id);
|
|
206
|
+
if (!entry)
|
|
207
|
+
return;
|
|
208
|
+
const document = entry.key.ownerDocument ?? window.document;
|
|
209
|
+
const element = document.createElement("div");
|
|
210
|
+
element.setAttribute("data-meeko-devtools-highlight", "");
|
|
211
|
+
Object.assign(element.style, {
|
|
212
|
+
position: "fixed",
|
|
213
|
+
outline: "2px solid #7c3aed", background: "rgba(124, 58, 237, 0.12)",
|
|
214
|
+
pointerEvents: "none", zIndex: "2147483647",
|
|
215
|
+
});
|
|
216
|
+
document.body.appendChild(element);
|
|
217
|
+
overlay = element;
|
|
218
|
+
highlightedId = id;
|
|
219
|
+
positionHighlight();
|
|
220
|
+
window.addEventListener("scroll", scheduleHighlight, true);
|
|
221
|
+
window.addEventListener("resize", scheduleHighlight);
|
|
222
|
+
highlightObserver = new window.MutationObserver((mutations) => {
|
|
223
|
+
if (mutations.some((mutation) => mutation.target !== overlay && !overlay?.contains(mutation.target)))
|
|
224
|
+
scheduleHighlight();
|
|
225
|
+
});
|
|
226
|
+
highlightObserver.observe(entry.scope.root, { attributes: true, characterData: true, childList: true, subtree: true });
|
|
227
|
+
},
|
|
228
|
+
setBreakpoint(id, enabled) {
|
|
229
|
+
const entry = active.get(id);
|
|
230
|
+
if (!entry)
|
|
231
|
+
return;
|
|
232
|
+
entry.breakpoint = enabled;
|
|
233
|
+
const identity = identities.get(entry.key);
|
|
234
|
+
if (identity)
|
|
235
|
+
identity.breakpoint = enabled;
|
|
236
|
+
record("breakpoint", id, enabled ? "enabled" : "disabled");
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
function unregister(entry) {
|
|
240
|
+
if (active.get(entry.id) !== entry)
|
|
241
|
+
return;
|
|
242
|
+
for (const stop of entry.stopSignals)
|
|
243
|
+
stop();
|
|
244
|
+
active.delete(entry.id);
|
|
245
|
+
if (selectedId === entry.id)
|
|
246
|
+
selectedId = null;
|
|
247
|
+
if (highlightedId === entry.id)
|
|
248
|
+
removeHighlight();
|
|
249
|
+
record("unmount", entry.id);
|
|
250
|
+
}
|
|
251
|
+
/** Registra señales y ámbito de una instancia hidratada en Vite desarrollo. */
|
|
252
|
+
export function registerDevtoolsInstance(input) {
|
|
253
|
+
if (typeof window === "undefined" || typeof Node === "undefined")
|
|
254
|
+
return () => { };
|
|
255
|
+
if (!window.__MEEKO_DEVTOOLS__) {
|
|
256
|
+
window.__MEEKO_DEVTOOLS__ = hook;
|
|
257
|
+
window.dispatchEvent(new window.Event("meeko:devtools-ready"));
|
|
258
|
+
}
|
|
259
|
+
const key = keyOf(input.scope);
|
|
260
|
+
const previous = identities.get(key);
|
|
261
|
+
const identity = previous ?? { id: `meeko-${nextInstance++}`, breakpoint: false };
|
|
262
|
+
identities.set(key, identity);
|
|
263
|
+
const existing = active.get(identity.id);
|
|
264
|
+
if (existing)
|
|
265
|
+
unregister(existing);
|
|
266
|
+
const entry = { ...input, id: identity.id, key, breakpoint: identity.breakpoint, stopSignals: [] };
|
|
267
|
+
active.set(entry.id, entry);
|
|
268
|
+
for (const [name, value] of Object.entries(input.bindings)) {
|
|
269
|
+
if (!isWritableSignal(value))
|
|
270
|
+
continue;
|
|
271
|
+
const signal = value;
|
|
272
|
+
let initial = true;
|
|
273
|
+
entry.stopSignals.push(effect(() => {
|
|
274
|
+
void signal.value;
|
|
275
|
+
if (initial) {
|
|
276
|
+
initial = false;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
record("signal", entry.id, name);
|
|
280
|
+
if (entry.breakpoint) {
|
|
281
|
+
debugger;
|
|
282
|
+
}
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
record(previous ? "hmr" : "mount", entry.id);
|
|
286
|
+
return () => { unregister(entry); };
|
|
287
|
+
}
|