@meekojs/meeko 0.0.1 → 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/README.md CHANGED
@@ -16,7 +16,9 @@ El proyecto está en etapa temprana. Ya funciona el recorrido completo de un con
16
16
  - [API y límites de compatibilidad](docs/api.md)
17
17
  - [Plan de desarrollo y criterios de aceptación](docs/roadmap.md)
18
18
  - [Comparativa reproducible con Vue](docs/benchmarks.md)
19
+ - [DevTools para depurar componentes](docs/devtools.md)
19
20
  - [Publicación y verificaciones](docs/releasing.md)
21
+ - [Historial de cambios](CHANGELOG.md)
20
22
  - [Guía para contribuir](CONTRIBUTING.md)
21
23
 
22
24
  ## Requisitos
@@ -25,6 +27,24 @@ El proyecto está en etapa temprana. Ya funciona el recorrido completo de un con
25
27
  - `pnpm`.
26
28
  - TypeScript **7.0.2 estable** para compilar y comprobar los tipos. `@typescript/typescript6` se usa únicamente como analizador sintáctico del formato `.meeko`, porque TypeScript 7.0 todavía no ofrece una API programática estable para lenguajes incrustados.
27
29
 
30
+ ## Instalación en una aplicación
31
+
32
+ ```powershell
33
+ pnpm add @meekojs/meeko
34
+ pnpm add -D vite
35
+ ```
36
+
37
+ Si usas Vite, agrega el plugin a `vite.config.ts`:
38
+
39
+ ```ts
40
+ import { defineConfig } from "vite";
41
+ import { meeko } from "@meekojs/meeko/vite";
42
+
43
+ export default defineConfig({ plugins: [meeko()] });
44
+ ```
45
+
46
+ El paquete incluye TypeScript 7 y la CLI `meeko`. Puedes comprobar un componente con `pnpm exec meeko check ruta/Componente.meeko`. Vite es opcional si sólo usas el compilador y SSR.
47
+
28
48
  ## Estructura
29
49
 
30
50
  | Ruta | Responsabilidad |
@@ -37,12 +57,15 @@ El proyecto está en etapa temprana. Ya funciona el recorrido completo de un con
37
57
  | `src/server.ts` | Escapado de contenido dinámico para HTML. |
38
58
  | `src/cli.ts` | Comandos `meeko check` y `meeko compile`. |
39
59
  | `src/vite.ts` | Plugin de Vite para transformar `.meeko` en cliente y SSR. |
60
+ | `src/devtools.ts` | Instrumentación de instancias y señales sólo durante desarrollo. |
61
+ | `src/devtools-ui` | Panel de inspección dentro de la página. |
40
62
  | `examples/counter` | Componente de referencia tipado. |
41
63
  | `examples/vite` | Aplicación Vite sin generación manual de componentes. |
42
64
  | `examples/nested` | Composición de componentes con rangos de hidratación y props tipadas. |
43
65
  | `editors/vscode` | Extensión local con resaltado, diagnósticos y operaciones semánticas. |
44
66
  | `benchmarks` | Comparativa repetible de Meeko y Vue en SSR y Chrome. |
45
67
  | `tests` | Pruebas de SSR, hidratación y reactividad. |
68
+ | `tools/devtools-extension` | Panel local de Chrome DevTools. |
46
69
 
47
70
  ## Desarrollo
48
71
 
@@ -67,6 +90,8 @@ pnpm run vite:build
67
90
 
68
91
  El plugin `@meekojs/meeko/vite` transforma imports de `.meeko` en cliente y SSR y procesa el CSS del componente. En desarrollo, HMR sustituye las instancias hidratadas sin recargar la página, conserva las señales editables que mantienen su nombre y aplica cambios de CSS. Un cambio en el runtime del motor provoca una recarga completa.
69
92
 
93
+ El ejemplo Vite muestra [Meeko DevTools](docs/devtools.md) durante el desarrollo: permite seleccionar cada instancia, revisar props y señales, seguir eventos y activar un punto de parada cuando cambia una señal. También hay un panel local para Chrome DevTools. La funcionalidad se incorpora al paquete a partir de `0.0.2`.
94
+
70
95
  `vite:types` genera declaraciones `.d.meeko.ts` para los imports de la aplicación y comprueba el consumidor con TypeScript 7 y `allowArbitraryExtensions`. Las declaraciones del ejemplo se regeneran; no se versionan.
71
96
 
72
97
  Para compilar otro componente:
package/dist/compiler.js CHANGED
@@ -391,14 +391,14 @@ export function compile(source, filename = "anonymous.meeko", options = {}) {
391
391
  const hot = options.hot === true;
392
392
  const setupCode = `const __bindings = __setup(__inputProps);\n__meeko.restoreSignals(__bindings, __state);\nconst { ${bindings} } = __bindings;`;
393
393
  const releaseCode = hot
394
- ? `const __cleanup = () => { __release(); dispose(); };\nconst __component = { renderToString, hydrate, hydrateScope, css };\nconst __snapshot = () => __meeko.snapshotSignals(__bindings);\nconst __stop = __hotRoot ? __meeko.registerHotInstance(__hotId, __hotRoot, __inputProps, __component, __snapshot, __cleanup) : __scope.start ? __meeko.registerHotScope(__hotId, __scope, __inputProps, __component, __snapshot, __cleanup) : __cleanup;\nreturn __meeko.withStateSnapshot(__stop, __snapshot);`
394
+ ? `const __cleanup = () => { __release(); dispose(); };\nconst __component = { renderToString, hydrate, hydrateScope, css };\nconst __snapshot = () => __meeko.snapshotSignals(__bindings);\nconst __devtoolsStop = __meeko.registerDevtoolsInstance({ componentId: __hotId, name: ${quote(basename(filename, ".meeko"))}, file: ${quote(filename)}, scope: __scope, props: __inputProps, bindings: __bindings });\nconst __dispose = () => { try { __cleanup(); } finally { __devtoolsStop(); } };\nconst __stop = __hotRoot ? __meeko.registerHotInstance(__hotId, __hotRoot, __inputProps, __component, __snapshot, __dispose) : __scope.start ? __meeko.registerHotScope(__hotId, __scope, __inputProps, __component, __snapshot, __dispose) : __dispose;\nreturn __meeko.withStateSnapshot(__stop, __snapshot);`
395
395
  : `return __meeko.withStateSnapshot(() => { __release(); dispose(); }, () => __meeko.snapshotSignals(__bindings));`;
396
396
  const hotIdExport = hot ? `export const __hotId = ${quote(componentId)};\n` : "";
397
397
  const stateParam = ", __state?: __meeko.HotState";
398
398
  const scopeHotRootParam = hot ? ", __hotRoot?: Element" : "";
399
399
  const scopeForward = hot ? "__scope, __inputProps, __state, root" : "__scope, __inputProps, __state";
400
400
  const propsType = scriptParts.propsTypeOffset === undefined ? scriptParts.propsType : markSource(scriptParts.propsType, scriptParts.propsTypeOffset);
401
- const markedCode = `// Generated by Meeko. Edit the .meeko source instead.\nimport * as __meeko from "@meekojs/meeko";\n${scriptParts.imports.join("\n")}\n${scriptParts.types.join("\n")}\nfunction __setup(__props: ${propsType}) {\n${scriptParts.setup.join("\n")}\nreturn { ${bindings} };\n}\nexport type ComponentProps = ${propsType};\nexport const css = ${quote(css)};\n${hotIdExport}export function renderWithState(__inputProps: ComponentProps): { html: string; state: __meeko.HotState } {\nreturn __meeko.createRoot((dispose) => {\nconst __meeko_transfer = true;\nconst __bindings = __setup(__inputProps);\nconst { ${bindings} } = __bindings;\ntry {\nconst html = ${html};\nconst state = __meeko.snapshotSignals(__bindings);\n__meeko.serializeJson(state);\nreturn { html, state };\n} finally { dispose(); }\n});\n}\nexport function renderToString(__inputProps: ComponentProps${stateParam}): string {\nreturn __meeko.createRoot((dispose) => {\nconst __meeko_transfer = false;\n${setupCode}\ntry { return ${html}; } finally { dispose(); }\n});\n}\nexport function hydrate(root: Element, __inputProps: ComponentProps${stateParam}): () => void {\nconst __scope: __meeko.DomScope = { root };\nreturn hydrateScope(${scopeForward});\n}\nexport function hydrateScope(__scope: __meeko.DomScope, __inputProps: ComponentProps${stateParam}${scopeHotRootParam}): () => void {\nreturn __meeko.createRoot((dispose) => {\nconst __meeko_transfer = false;\n${setupCode}\nconst __release = (() => { ${hydrationBody(bindingsForTemplate, context)} })();\n${releaseCode}\n});\n}\n`;
401
+ const markedCode = `// Generated by Meeko. Edit the .meeko source instead.\nimport * as __meeko from "@meekojs/meeko/internal";\n${scriptParts.imports.join("\n")}\n${scriptParts.types.join("\n")}\nfunction __setup(__props: ${propsType}) {\n${scriptParts.setup.join("\n")}\nreturn { ${bindings} };\n}\nexport type ComponentProps = ${propsType};\nexport const css = ${quote(css)};\n${hotIdExport}export function renderWithState(__inputProps: ComponentProps): { html: string; state: __meeko.HotState } {\nreturn __meeko.createRoot((dispose) => {\nconst __meeko_transfer = true;\nconst __bindings = __setup(__inputProps);\nconst { ${bindings} } = __bindings;\ntry {\nconst html = ${html};\nconst state = __meeko.snapshotSignals(__bindings);\n__meeko.serializeJson(state);\nreturn { html, state };\n} finally { dispose(); }\n});\n}\nexport function renderToString(__inputProps: ComponentProps${stateParam}): string {\nreturn __meeko.createRoot((dispose) => {\nconst __meeko_transfer = false;\n${setupCode}\ntry { return ${html}; } finally { dispose(); }\n});\n}\nexport function hydrate(root: Element, __inputProps: ComponentProps${stateParam}): () => void {\nconst __scope: __meeko.DomScope = { root };\nreturn hydrateScope(${scopeForward});\n}\nexport function hydrateScope(__scope: __meeko.DomScope, __inputProps: ComponentProps${stateParam}${scopeHotRootParam}): () => void {\nreturn __meeko.createRoot((dispose) => {\nconst __meeko_transfer = false;\n${setupCode}\nconst __release = (() => { ${hydrationBody(bindingsForTemplate, context)} })();\n${releaseCode}\n});\n}\n`;
402
402
  const { code, mappings } = extractSourceMappings(markedCode);
403
403
  return { code, css, mappings };
404
404
  }
@@ -0,0 +1,14 @@
1
+ /** Opciones de montaje del panel integrado de desarrollo. */
2
+ export interface DevtoolsPanelOptions {
3
+ /** Elemento que recibirá el host del Shadow DOM. El valor predeterminado es `document.body`. */
4
+ container?: Element;
5
+ }
6
+ /** Control del ciclo de vida del panel. */
7
+ export interface DevtoolsPanel {
8
+ /** Reintenta encontrar el hook y actualiza los datos visibles. */
9
+ refresh(): void;
10
+ /** Quita la interfaz y libera las suscripciones y eventos instalados. */
11
+ unmount(): void;
12
+ }
13
+ /** Monta una interfaz aislada en Shadow DOM para inspeccionar instancias Meeko. */
14
+ export declare function mountDevtoolsPanel(options?: DevtoolsPanelOptions): DevtoolsPanel;
@@ -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;
@@ -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
+ }
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";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Contrato del runtime generado por el compilador Meeko.
3
+ *
4
+ * Esta ruta es interna y puede cambiar junto con el compilador. Las aplicaciones
5
+ * deben importar las API documentadas desde la raíz del paquete.
6
+ */
7
+ export { createRoot } from "./reactivity.js";
8
+ export { attributeValue, escapeHtml, renderAttribute, serializeJson } from "./server.js";
9
+ export { bindAttribute, bindComponent, bindEvent, bindFor, bindIf, bindText, keyToken, renderComponent, renderFor, renderIf, withStateSnapshot } from "./dom.js";
10
+ export type { ComponentModule, ComponentRelease, DomScope } from "./dom.js";
11
+ export { enableHotModule, registerHotInstance, registerHotScope, replaceHotModule, replaceHotStyle, restoreSignals, snapshotSignals } from "./hmr.js";
12
+ export type { HotComponent, HotState } from "./hmr.js";
13
+ export { registerDevtoolsInstance } from "./devtools.js";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Contrato del runtime generado por el compilador Meeko.
3
+ *
4
+ * Esta ruta es interna y puede cambiar junto con el compilador. Las aplicaciones
5
+ * deben importar las API documentadas desde la raíz del paquete.
6
+ */
7
+ export { createRoot } from "./reactivity.js";
8
+ export { attributeValue, escapeHtml, renderAttribute, serializeJson } from "./server.js";
9
+ export { bindAttribute, bindComponent, bindEvent, bindFor, bindIf, bindText, keyToken, renderComponent, renderFor, renderIf, withStateSnapshot } from "./dom.js";
10
+ export { enableHotModule, registerHotInstance, registerHotScope, replaceHotModule, replaceHotStyle, restoreSignals, snapshotSignals } from "./hmr.js";
11
+ export { registerDevtoolsInstance } from "./devtools.js";
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@meekojs/meeko",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "packageManager": "pnpm@12.14.0",
5
- "description": "Typed .meeko component engine with SSR, hydration, signals, and Vite integration.",
5
+ "description": "Typed .meeko component engine with SSR, hydration, signals, Vite, and DevTools.",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
8
  "files": ["dist/"],
@@ -10,7 +10,7 @@
10
10
  "repository": { "type": "git", "url": "git+https://github.com/meekojs/meeko.git" },
11
11
  "homepage": "https://github.com/meekojs/meeko#readme",
12
12
  "bugs": { "url": "https://github.com/meekojs/meeko/issues" },
13
- "keywords": ["meeko", "components", "ssr", "hydration", "reactivity", "vite", "typescript"],
13
+ "keywords": ["meeko", "components", "ssr", "hydration", "reactivity", "vite", "typescript", "devtools"],
14
14
  "publishConfig": { "access": "public" },
15
15
  "exports": {
16
16
  ".": {
@@ -20,6 +20,14 @@
20
20
  "./vite": {
21
21
  "types": "./dist/vite.d.ts",
22
22
  "default": "./dist/vite.js"
23
+ },
24
+ "./internal": {
25
+ "types": "./dist/internal.d.ts",
26
+ "default": "./dist/internal.js"
27
+ },
28
+ "./devtools-ui": {
29
+ "types": "./dist/devtools-ui/index.d.ts",
30
+ "default": "./dist/devtools-ui/index.js"
23
31
  }
24
32
  },
25
33
  "bin": {
@@ -38,10 +46,12 @@
38
46
  "vite:types": "pnpm run build && node dist/cli.js types examples/counter/Counter.meeko && node dist/cli.js types examples/regions/Regions.meeko && node dist/cli.js types examples/vite/Hot.meeko && node dist/cli.js types examples/nested/Parent.meeko && tsc -p examples/vite/tsconfig.json --noEmit",
39
47
  "vite:build": "vite build examples/vite --config examples/vite/vite.config.ts",
40
48
  "e2e": "pnpm run example:bundle && pnpm run vite:build && tsx --test tests/browser.e2e.ts",
41
- "benchmark:check": "pnpm run example:compile && tsc -p tsconfig.benchmark.json --noEmit",
49
+ "benchmark:check": "pnpm run example:compile && node dist/cli.js compile benchmarks/cases/TreeLeaf.meeko benchmarks/cases/TreeLeaf.generated.ts && node dist/cli.js compile benchmarks/cases/TreeBranch.meeko benchmarks/cases/TreeBranch.generated.ts && node dist/cli.js compile benchmarks/cases/Tree.meeko benchmarks/cases/Tree.generated.ts && node dist/cli.js compile benchmarks/cases/List.meeko benchmarks/cases/List.generated.ts && node dist/cli.js compile benchmarks/cases/Navigation.meeko benchmarks/cases/Navigation.generated.ts && tsc -p tsconfig.benchmark.json --noEmit",
42
50
  "benchmark": "pnpm run build && pnpm run benchmark:check && node --expose-gc --import tsx benchmarks/run.ts",
51
+ "benchmark:scenarios": "pnpm run build && pnpm run benchmark:check && node --expose-gc --import tsx benchmarks/scenarios.ts",
43
52
  "package:smoke": "pwsh -NoProfile -File tests/package-consumer.ps1",
44
- "verify": "pnpm run check && pnpm run test && pnpm run example:check && tsc -p tsconfig.test.json --noEmit && pnpm run vite:types && pnpm run benchmark:check && pnpm run vite:build"
53
+ "devtools:check": "node --test tools/devtools-extension/tests/*.test.js && tsc -p tools/devtools-extension/tsconfig.json --noEmit",
54
+ "verify": "pnpm run check && pnpm run test && pnpm run devtools:check && pnpm run example:check && tsc -p tsconfig.test.json --noEmit && pnpm run vite:types && pnpm run benchmark:check && pnpm run vite:build"
45
55
  },
46
56
  "dependencies": {
47
57
  "@typescript/typescript6": "6.0.2",