@meekojs/meeko 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,10 @@ 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)
20
+ - [Aplicación piloto SSR](examples/pilot/README.md)
19
21
  - [Publicación y verificaciones](docs/releasing.md)
22
+ - [Historial de cambios](CHANGELOG.md)
20
23
  - [Guía para contribuir](CONTRIBUTING.md)
21
24
 
22
25
  ## Requisitos
@@ -25,6 +28,39 @@ El proyecto está en etapa temprana. Ya funciona el recorrido completo de un con
25
28
  - `pnpm`.
26
29
  - 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
30
 
31
+ ## Instalación en una aplicación
32
+
33
+ ```powershell
34
+ pnpm add @meekojs/meeko
35
+ pnpm add -D vite
36
+ ```
37
+
38
+ Si usas Vite, agrega el plugin a `vite.config.ts`:
39
+
40
+ ```ts
41
+ import { defineConfig } from "vite";
42
+ import { meeko } from "@meekojs/meeko/vite";
43
+
44
+ export default defineConfig({ plugins: [meeko()] });
45
+ ```
46
+
47
+ 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.
48
+
49
+ ### Crear una aplicación SSR
50
+
51
+ Con el paquete instalado, `meeko init` crea un proyecto independiente. El servidor renderiza el componente `.meeko` y transfiere props y señales para que el navegador hidrate el HTML existente.
52
+
53
+ ```powershell
54
+ pnpm exec meeko init mi-app
55
+ cd mi-app
56
+ npm install
57
+ npm run dev
58
+ ```
59
+
60
+ Abre `http://127.0.0.1:4173`. `npm run check` genera los tipos del componente y ejecuta TypeScript 7. `npm run build` compila cliente y servidor; `npm start` sirve la versión compilada con SSR. La CLI no sobrescribe archivos de aplicación existentes.
61
+
62
+ Desde `0.0.3`, si ya ejecutaste `pnpm add @meekojs/meeko` en una carpeta sin código, puedes inicializarla directamente con `pnpm exec meeko init .`. Se conservan el manifiesto, los lockfiles, el workspace y `node_modules`; la CLI rechaza archivos de aplicación o scripts que entrarían en conflicto. Después ejecuta `pnpm install` y `pnpm run dev`. Para actualizar desde `0.0.2`, usa `pnpm add @meekojs/meeko@0.0.3` antes de `init .`.
63
+
28
64
  ## Estructura
29
65
 
30
66
  | Ruta | Responsabilidad |
@@ -37,12 +73,15 @@ El proyecto está en etapa temprana. Ya funciona el recorrido completo de un con
37
73
  | `src/server.ts` | Escapado de contenido dinámico para HTML. |
38
74
  | `src/cli.ts` | Comandos `meeko check` y `meeko compile`. |
39
75
  | `src/vite.ts` | Plugin de Vite para transformar `.meeko` en cliente y SSR. |
76
+ | `src/devtools.ts` | Instrumentación de instancias y señales sólo durante desarrollo. |
77
+ | `src/devtools-ui` | Panel de inspección dentro de la página. |
40
78
  | `examples/counter` | Componente de referencia tipado. |
41
79
  | `examples/vite` | Aplicación Vite sin generación manual de componentes. |
42
80
  | `examples/nested` | Composición de componentes con rangos de hidratación y props tipadas. |
43
81
  | `editors/vscode` | Extensión local con resaltado, diagnósticos y operaciones semánticas. |
44
82
  | `benchmarks` | Comparativa repetible de Meeko y Vue en SSR y Chrome. |
45
83
  | `tests` | Pruebas de SSR, hidratación y reactividad. |
84
+ | `tools/devtools-extension` | Panel local de Chrome DevTools. |
46
85
 
47
86
  ## Desarrollo
48
87
 
@@ -50,11 +89,12 @@ El proyecto está en etapa temprana. Ya funciona el recorrido completo de un con
50
89
  pnpm install
51
90
  pnpm run verify
52
91
  pnpm run e2e
92
+ pnpm run pilot:e2e
53
93
  pnpm run package:smoke
54
94
  pnpm run example:serve
55
95
  ```
56
96
 
57
- `e2e` usa Chrome instalado localmente; `MEEKO_CHROME_PATH` permite indicar otro ejecutable compatible. Prueba Vite en desarrollo y producción, además de una página SSR con navegación. `package:smoke` instala el tarball en un proyecto temporal externo y prueba CLI, SSR, tipos y Vite. La última orden sirve el ejemplo en `http://127.0.0.1:4173`. El servidor envía HTML y las props iniciales como JSON escapado; el navegador hidrata el botón sin reemplazarlo.
97
+ `e2e` usa Chrome instalado localmente; `MEEKO_CHROME_PATH` permite indicar otro ejecutable compatible. Prueba Vite en desarrollo y producción, además de una página SSR con navegación. `pilot:e2e` prueba la aplicación piloto en Chromium, Firefox y WebKit instalados por Playwright, tanto en desarrollo como en producción. `package:smoke` instala el tarball en un proyecto temporal externo y prueba CLI, SSR, tipos, Vite y `meeko init`. La última orden sirve el contador en `http://127.0.0.1:4173`.
58
98
 
59
99
  Para probar la compilación automática con Vite:
60
100
 
@@ -67,6 +107,8 @@ pnpm run vite:build
67
107
 
68
108
  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
109
 
110
+ 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`.
111
+
70
112
  `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
113
 
72
114
  Para compilar otro componente:
@@ -83,6 +125,10 @@ node dist/cli.js types ruta/Componente.meeko
83
125
 
84
126
  La aplicación debe pasar **las mismas props** al render y a `hydrate`. `renderWithState(props)` entrega HTML y señales editables serializables; el ejemplo SSR transfiere ambos junto con las props mediante `serializeJson` y llama `hydrate(root, props, state)`. Meeko comprueba que el texto existente coincida durante la hidratación y devuelve una función que desconecta eventos y observadores. El HTML dinámico se escapa en el servidor.
85
127
 
128
+ ## Aplicación piloto
129
+
130
+ [`examples/pilot`](examples/pilot/README.md) reúne SSR, hidratación, componentes anidados, listas, formularios, rutas y una API asíncrona. Se ejecuta con `pnpm run pilot:dev`; `pnpm run pilot:build` y `pnpm run pilot:serve` prueban los artefactos de producción sin middleware de Vite. La prueba `pnpm run pilot:e2e` recorre ambos modos en Chromium, Firefox y WebKit. WebKit de Playwright no equivale a una prueba en Safari real.
131
+
86
132
  ## Sintaxis inicial
87
133
 
88
134
  Un archivo tiene `<script lang="ts">`, `<template>` y un `<style>` opcional; `scoped` limita las reglas CSS al componente. `defineProps<Props>()` declara las props. La plantilla acepta expresiones de texto `{expression}`, eventos `on:click="handler"` y atributos dinámicos `bind:title="expression"`. Los atributos booleanos, como `bind:disabled`, se agregan o eliminan según el valor.
package/dist/cli.js CHANGED
@@ -1,40 +1,51 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import { join, resolve } from "node:path";
3
5
  import { checkSource } from "./check.js";
4
6
  import { compile } from "./compiler.js";
5
7
  import { generateTypes } from "./types.js";
8
+ import { initProject } from "./init.js";
6
9
  const [command, input, output] = process.argv.slice(2);
7
- if ((command !== "compile" && command !== "check" && command !== "types") || !input || (command === "compile" && !output) || (command === "check" && output !== undefined && output !== "--json") || (command === "types" && output !== undefined)) {
8
- process.stderr.write("Usage: meeko check <component.meeko> [--json]\n meeko compile <component.meeko> <output.ts>\n meeko types <component.meeko>\n");
10
+ if ((command !== "compile" && command !== "check" && command !== "types" && command !== "init") || !input || (command === "compile" && !output) || (command === "check" && output !== undefined && output !== "--json") || ((command === "types" || command === "init") && output !== undefined)) {
11
+ process.stderr.write("Usage: meeko check <component.meeko> [--json]\n meeko compile <component.meeko> <output.ts>\n meeko types <component.meeko>\n meeko init <directory>\n");
9
12
  process.exitCode = 1;
10
13
  }
11
14
  else {
12
15
  try {
13
- const source = await readFile(input, "utf8");
14
- const diagnostics = await checkSource(source, input);
15
- if (command === "check" && output === "--json") {
16
- process.stdout.write(`${JSON.stringify(diagnostics)}\n`);
17
- }
18
- else if (diagnostics.length > 0) {
19
- for (const diagnostic of diagnostics) {
20
- const { line, column } = diagnostic.location;
21
- process.stderr.write(`${diagnostic.filename}:${line}:${column} ${diagnostic.code}: ${diagnostic.message}\n`);
22
- }
23
- }
24
- else if (command === "compile") {
25
- const result = compile(source, input);
26
- await writeFile(output, result.code, "utf8");
27
- process.stdout.write(`${output}\n`);
28
- }
29
- else if (command === "types") {
30
- for (const generated of await generateTypes(source, input))
31
- process.stdout.write(`${generated}\n`);
16
+ if (command === "init") {
17
+ const target = await initProject(input);
18
+ const manager = existsSync(join(target, "pnpm-lock.yaml")) ? "pnpm" : "npm";
19
+ const prefix = target === resolve(".") ? "" : `cd ${target} && `;
20
+ process.stdout.write(`Created ${target}\nNext: ${prefix}${manager} install && ${manager} run dev\n`);
32
21
  }
33
22
  else {
34
- process.stdout.write(`${input}: OK\n`);
23
+ const source = await readFile(input, "utf8");
24
+ const diagnostics = await checkSource(source, input);
25
+ if (command === "check" && output === "--json") {
26
+ process.stdout.write(`${JSON.stringify(diagnostics)}\n`);
27
+ }
28
+ else if (diagnostics.length > 0) {
29
+ for (const diagnostic of diagnostics) {
30
+ const { line, column } = diagnostic.location;
31
+ process.stderr.write(`${diagnostic.filename}:${line}:${column} ${diagnostic.code}: ${diagnostic.message}\n`);
32
+ }
33
+ }
34
+ else if (command === "compile") {
35
+ const result = compile(source, input);
36
+ await writeFile(output, result.code, "utf8");
37
+ process.stdout.write(`${output}\n`);
38
+ }
39
+ else if (command === "types") {
40
+ for (const generated of await generateTypes(source, input))
41
+ process.stdout.write(`${generated}\n`);
42
+ }
43
+ else {
44
+ process.stdout.write(`${input}: OK\n`);
45
+ }
46
+ if (diagnostics.length > 0)
47
+ process.exitCode = 1;
35
48
  }
36
- if (diagnostics.length > 0)
37
- process.exitCode = 1;
38
49
  }
39
50
  catch (error) {
40
51
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
package/dist/compiler.js CHANGED
@@ -311,18 +311,20 @@ function compileScript(section, source, filename) {
311
311
  }
312
312
  return { imports, types, setup, names, propsType: propsType ?? "Record<string, never>", propsTypeOffset };
313
313
  }
314
- function hydrationBody(bindings, context, locals = new Map()) {
314
+ function hydrationBody(bindings, context, hot, locals = new Map()) {
315
315
  const statements = [];
316
316
  const mappedExpression = (expression, offset, expressionLocals = locals) => markSource(rewriteLocals(expression, expressionLocals), offset, expression.length);
317
317
  for (const { id, expression, offset } of bindings.texts) {
318
318
  const point = locate(context.source, offset);
319
319
  const location = `${basename(context.filename)}:${point.line}:${point.column} (text ${id})`;
320
- statements.push(`cleanups.push(__meeko.bindText(__scope, ${id}, () => ${mappedExpression(expression, offset)}, ${quote(location)}));`);
320
+ const source = hot ? `, { file: ${quote(context.filename)}, line: ${point.line}, column: ${point.column}, expression: ${quote(expression)}, kind: "text" }` : "";
321
+ statements.push(`cleanups.push(__meeko.bindText(__scope, ${id}, () => ${mappedExpression(expression, offset)}, ${quote(location)}${source}));`);
321
322
  }
322
323
  for (const { id, name, expression, offset } of bindings.attributes) {
323
324
  const point = locate(context.source, offset);
324
325
  const location = `${basename(context.filename)}:${point.line}:${point.column} (attribute ${name})`;
325
- statements.push(`cleanups.push(__meeko.bindAttribute(__scope, ${id}, ${quote(name)}, () => ${mappedExpression(expression, offset)}, ${quote(location)}));`);
326
+ const source = hot ? `, { file: ${quote(context.filename)}, line: ${point.line}, column: ${point.column}, expression: ${quote(expression)}, kind: "attribute" }` : "";
327
+ statements.push(`cleanups.push(__meeko.bindAttribute(__scope, ${id}, ${quote(name)}, () => ${mappedExpression(expression, offset)}, ${quote(location)}${source}));`);
326
328
  }
327
329
  for (const { id, name, expression, offset } of bindings.events) {
328
330
  const eventType = ["click", "input", "change", "submit", "keydown", "keyup", "focus", "blur"].includes(name)
@@ -345,7 +347,7 @@ function hydrationBody(bindings, context, locals = new Map()) {
345
347
  statements.unshift(...componentStatements);
346
348
  for (const entry of bindings.regions) {
347
349
  if (entry.kind === "condition") {
348
- statements.push(`cleanups.push(__meeko.bindIf(__scope, ${entry.id}, () => ${mappedExpression(entry.expression, entry.offset)}, () => ${rewriteLocals(entry.render, locals)}, (__scope) => { ${hydrationBody(entry.bindings, context, locals)} }));`);
350
+ statements.push(`cleanups.push(__meeko.bindIf(__scope, ${entry.id}, () => ${mappedExpression(entry.expression, entry.offset)}, () => ${rewriteLocals(entry.render, locals)}, (__scope) => { ${hydrationBody(entry.bindings, context, hot, locals)} }));`);
349
351
  }
350
352
  else {
351
353
  const outerLocals = new Map(locals);
@@ -354,7 +356,10 @@ function hydrationBody(bindings, context, locals = new Map()) {
354
356
  const innerLocals = new Map(locals);
355
357
  innerLocals.set(entry.itemName, "__meeko_item.value");
356
358
  innerLocals.set(entry.indexName, "__meeko_index.value");
357
- statements.push(`cleanups.push(__meeko.bindFor(__scope, ${entry.id}, () => ${mappedExpression(entry.expression, entry.offset)}, (${entry.itemName}, ${entry.indexName}) => ${mappedExpression(entry.key, entry.keyOffset, outerLocals)}, (${entry.itemName}, ${entry.indexName}) => ${rewriteLocals(entry.render, outerLocals)}, (__meeko_item, __meeko_index, __scope) => { ${hydrationBody(entry.bindings, context, innerLocals)} }));`);
359
+ const innerHydration = hydrationBody(entry.bindings, context, hot, innerLocals);
360
+ // El callback generado sólo usa la señal de índice si aparece en su hidratación.
361
+ const trackIndex = innerHydration.includes("__meeko_index.value");
362
+ statements.push(`cleanups.push(__meeko.bindFor(__scope, ${entry.id}, () => ${mappedExpression(entry.expression, entry.offset)}, (${entry.itemName}, ${entry.indexName}) => ${mappedExpression(entry.key, entry.keyOffset, outerLocals)}, (${entry.itemName}, ${entry.indexName}) => ${rewriteLocals(entry.render, outerLocals)}, (__meeko_item, __meeko_index, __scope) => { ${innerHydration} }, ${trackIndex}));`);
358
363
  }
359
364
  }
360
365
  return `const cleanups: Array<() => void> = [];
@@ -391,14 +396,14 @@ export function compile(source, filename = "anonymous.meeko", options = {}) {
391
396
  const hot = options.hot === true;
392
397
  const setupCode = `const __bindings = __setup(__inputProps);\n__meeko.restoreSignals(__bindings, __state);\nconst { ${bindings} } = __bindings;`;
393
398
  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);`
399
+ ? `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
400
  : `return __meeko.withStateSnapshot(() => { __release(); dispose(); }, () => __meeko.snapshotSignals(__bindings));`;
396
401
  const hotIdExport = hot ? `export const __hotId = ${quote(componentId)};\n` : "";
397
402
  const stateParam = ", __state?: __meeko.HotState";
398
403
  const scopeHotRootParam = hot ? ", __hotRoot?: Element" : "";
399
404
  const scopeForward = hot ? "__scope, __inputProps, __state, root" : "__scope, __inputProps, __state";
400
405
  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`;
406
+ 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, hot)} })();\n${releaseCode}\n});\n}\n`;
402
407
  const { code, mappings } = extractSourceMappings(markedCode);
403
408
  return { code, css, mappings };
404
409
  }
@@ -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,351 @@
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: 160px; overflow: auto; }
29
+ .events-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px; }
30
+ .events-head label { color: #aab7ca; }
31
+ .event-button { width: 100%; text-align: left; }
32
+ .event-reads { margin: 4px 0 8px; color: #aab7ca; }
33
+ .event-reads summary { cursor: pointer; }
34
+ .event-reads li { overflow-wrap: anywhere; }
35
+ .event-reads code { color: #dbeafe; }
36
+ .events li { overflow-wrap: anywhere; }
37
+ .status { padding: 7px 12px; border-top: 1px solid #334155; color: #aab7ca; }
38
+ @media (max-width: 540px) {
39
+ .mobile-switch { display: flex; }
40
+ .content { display: block; min-height: 0; }
41
+ .content[data-view="instances"] .detail { display: none; }
42
+ .content[data-view="detail"] .instances { display: none; }
43
+ section + section { border-left: 0; }
44
+ }
45
+ `;
46
+ /** Formatea datos arbitrarios sin insertar HTML procedente de la aplicación. */
47
+ function formatValue(value) {
48
+ const seen = new WeakSet();
49
+ try {
50
+ const result = JSON.stringify(value, (_key, current) => {
51
+ if (typeof current === "bigint")
52
+ return `${current}n`;
53
+ if (typeof current === "function")
54
+ return "[función]";
55
+ if (typeof current === "symbol")
56
+ return String(current);
57
+ if (typeof current === "object" && current !== null) {
58
+ if (seen.has(current))
59
+ return "[circular]";
60
+ seen.add(current);
61
+ }
62
+ return current;
63
+ }, 2);
64
+ return (result ?? String(value)).slice(0, 8000);
65
+ }
66
+ catch {
67
+ return "[valor no representable]";
68
+ }
69
+ }
70
+ /** Crea un elemento con texto seguro y una clase opcional. */
71
+ function element(document, tag, text, className) {
72
+ const node = document.createElement(tag);
73
+ if (text !== undefined)
74
+ node.textContent = text;
75
+ if (className)
76
+ node.className = className;
77
+ return node;
78
+ }
79
+ /** Muestra el momento relativo comunicado por el runtime. */
80
+ function eventLabel(event) {
81
+ const kinds = {
82
+ mount: "Montaje",
83
+ unmount: "Desmontaje",
84
+ signal: "Señal",
85
+ breakpoint: "Punto de parada",
86
+ select: "Selección",
87
+ hmr: "HMR",
88
+ };
89
+ const time = Number.isFinite(event.at) ? new Date(event.at).toLocaleTimeString("es", { hour12: false }) : "hora desconocida";
90
+ const detail = event.detail ? ` · ${event.detail}` : "";
91
+ const source = event.componentName ? ` · ${event.componentName}` : "";
92
+ const file = event.file ? ` · ${event.file}` : "";
93
+ const value = event.kind === "signal" && "value" in event ? ` → ${formatValue(event.value).replace(/\s+/g, " ").slice(0, 240)}` : "";
94
+ return `${time} · ${kinds[event.kind]} · ${event.instanceId}${source}${file}${detail}${value}`;
95
+ }
96
+ /** Exige un documento conectado a una ventana para registrar eventos del hook. */
97
+ function requireView(document) {
98
+ const view = document.defaultView;
99
+ if (!view)
100
+ throw new Error("Meeko DevTools requires a browser document");
101
+ return view;
102
+ }
103
+ /** Monta una interfaz aislada en Shadow DOM para inspeccionar instancias Meeko. */
104
+ export function mountDevtoolsPanel(options = {}) {
105
+ const container = options.container ?? document.body;
106
+ const ownerDocument = container.ownerDocument;
107
+ const view = requireView(ownerDocument);
108
+ const host = element(ownerDocument, "div");
109
+ host.setAttribute("data-meeko-devtools-panel", "");
110
+ const shadow = host.attachShadow({ mode: "open" });
111
+ const style = element(ownerDocument, "style", styles);
112
+ const panel = element(ownerDocument, "div", undefined, "panel");
113
+ const header = element(ownerDocument, "header");
114
+ const title = element(ownerDocument, "strong", "Meeko DevTools");
115
+ const refreshButton = element(ownerDocument, "button", "Actualizar");
116
+ refreshButton.type = "button";
117
+ refreshButton.setAttribute("aria-label", "Actualizar instancias de Meeko");
118
+ const closeButton = element(ownerDocument, "button", "Cerrar");
119
+ closeButton.type = "button";
120
+ closeButton.setAttribute("aria-label", "Cerrar Meeko DevTools");
121
+ header.append(title, refreshButton, closeButton);
122
+ const mobileSwitch = element(ownerDocument, "nav", undefined, "mobile-switch");
123
+ mobileSwitch.setAttribute("aria-label", "Vistas de Meeko DevTools");
124
+ const instancesButton = element(ownerDocument, "button", "Instancias");
125
+ instancesButton.type = "button";
126
+ instancesButton.setAttribute("aria-controls", "meeko-devtools-instances");
127
+ const detailButton = element(ownerDocument, "button", "Detalle");
128
+ detailButton.type = "button";
129
+ detailButton.setAttribute("aria-controls", "meeko-devtools-detail");
130
+ mobileSwitch.append(instancesButton, detailButton);
131
+ const main = element(ownerDocument, "div", undefined, "content");
132
+ const instancesSection = element(ownerDocument, "section", undefined, "instances");
133
+ instancesSection.id = "meeko-devtools-instances";
134
+ const instancesTitle = element(ownerDocument, "h2", "Instancias");
135
+ const instancesList = element(ownerDocument, "ul");
136
+ instancesList.setAttribute("aria-label", "Instancias de Meeko");
137
+ instancesSection.append(instancesTitle, instancesList);
138
+ const detailSection = element(ownerDocument, "section", undefined, "detail");
139
+ detailSection.id = "meeko-devtools-detail";
140
+ detailSection.setAttribute("aria-label", "Detalle de instancia");
141
+ main.append(instancesSection, detailSection);
142
+ const eventsSection = element(ownerDocument, "section", undefined, "events");
143
+ const eventsTitle = element(ownerDocument, "h2", "Eventos recientes");
144
+ const eventsHead = element(ownerDocument, "div", undefined, "events-head");
145
+ const signalFilterLabel = element(ownerDocument, "label", " Sólo señales");
146
+ const signalFilter = element(ownerDocument, "input");
147
+ signalFilter.type = "checkbox";
148
+ signalFilter.setAttribute("aria-label", "Mostrar sólo eventos de señales");
149
+ signalFilterLabel.prepend(signalFilter);
150
+ eventsHead.append(eventsTitle, signalFilterLabel);
151
+ const eventsList = element(ownerDocument, "ol");
152
+ eventsList.setAttribute("aria-label", "Eventos recientes de Meeko");
153
+ eventsSection.append(eventsHead, eventsList);
154
+ const status = element(ownerDocument, "div", undefined, "status");
155
+ status.setAttribute("role", "status");
156
+ status.setAttribute("aria-live", "polite");
157
+ panel.append(header, mobileSwitch, main, eventsSection, status);
158
+ shadow.append(style, panel);
159
+ container.appendChild(host);
160
+ let hook;
161
+ let unsubscribe;
162
+ let highlightedId = null;
163
+ let mounted = true;
164
+ let mobileView = "instances";
165
+ /** Cambia la vista móvil y comunica el botón activo a tecnologías de asistencia. */
166
+ function showMobileView(next) {
167
+ mobileView = next;
168
+ main.dataset.view = mobileView;
169
+ instancesButton.setAttribute("aria-pressed", String(mobileView === "instances"));
170
+ detailButton.setAttribute("aria-pressed", String(mobileView === "detail"));
171
+ }
172
+ showMobileView(mobileView);
173
+ instancesButton.addEventListener("click", () => showMobileView("instances"));
174
+ detailButton.addEventListener("click", () => showMobileView("detail"));
175
+ /** Conserva el foco al reconstruir una fila después de recibir una notificación. */
176
+ function restoreFocus(id, action) {
177
+ if (!id || !action)
178
+ return;
179
+ for (const button of shadow.querySelectorAll("button[data-instance-id]")) {
180
+ if (button.dataset.instanceId === id && button.dataset.action === action) {
181
+ button.focus();
182
+ break;
183
+ }
184
+ }
185
+ }
186
+ /** Reconstruye sólo el contenido variable usando nodos de texto seguros. */
187
+ function render() {
188
+ if (!mounted)
189
+ return;
190
+ const active = shadow.activeElement;
191
+ const focusId = active?.dataset.instanceId;
192
+ const focusAction = active?.dataset.action;
193
+ instancesList.replaceChildren();
194
+ detailSection.replaceChildren();
195
+ eventsList.replaceChildren();
196
+ if (!hook) {
197
+ status.textContent = "Hook no disponible. Abre una página Meeko con Vite en desarrollo y pulsa Actualizar.";
198
+ instancesList.append(element(ownerDocument, "li", "No hay instancias disponibles.", "muted"));
199
+ detailSection.append(element(ownerDocument, "p", "El runtime de desarrollo aún no está conectado."));
200
+ eventsList.append(element(ownerDocument, "li", "Sin eventos.", "muted"));
201
+ return;
202
+ }
203
+ let snapshot;
204
+ try {
205
+ snapshot = hook.snapshot();
206
+ }
207
+ catch (error) {
208
+ status.textContent = `No se pudo leer el hook: ${error instanceof Error ? error.message : String(error)}`;
209
+ return;
210
+ }
211
+ status.textContent = `${snapshot.instances.length} instancia(s) · ${snapshot.events.length} evento(s)`;
212
+ if (snapshot.instances.length === 0)
213
+ instancesList.append(element(ownerDocument, "li", "No hay componentes hidratados.", "muted"));
214
+ const byId = new Map(snapshot.instances.map((instance) => [instance.id, instance]));
215
+ const rendered = new Set();
216
+ function appendInstance(instance, list) {
217
+ if (rendered.has(instance.id))
218
+ return;
219
+ rendered.add(instance.id);
220
+ const row = element(ownerDocument, "li", undefined, "tree-node");
221
+ const selectButton = element(ownerDocument, "button", instance.name || instance.componentId);
222
+ selectButton.type = "button";
223
+ selectButton.title = `${instance.name} · ${instance.file}`;
224
+ selectButton.setAttribute("aria-label", `Seleccionar ${instance.name || instance.componentId}`);
225
+ selectButton.dataset.instanceId = instance.id;
226
+ selectButton.dataset.action = "select";
227
+ selectButton.setAttribute("aria-pressed", String(snapshot.selectedId === instance.id));
228
+ selectButton.addEventListener("click", () => {
229
+ hook?.select(instance.id);
230
+ showMobileView("detail");
231
+ render();
232
+ if (view.matchMedia("(max-width: 540px)").matches)
233
+ detailButton.focus();
234
+ });
235
+ row.append(selectButton);
236
+ list.append(row);
237
+ const children = instance.childIds.map((id) => byId.get(id)).filter((child) => child !== undefined);
238
+ if (children.length > 0) {
239
+ const nested = element(ownerDocument, "ul", undefined, "tree-children");
240
+ for (const child of children)
241
+ appendInstance(child, nested);
242
+ if (nested.childElementCount > 0)
243
+ row.append(nested);
244
+ }
245
+ }
246
+ for (const instance of snapshot.instances)
247
+ if (!instance.parentId || !byId.has(instance.parentId))
248
+ appendInstance(instance, instancesList);
249
+ for (const instance of snapshot.instances)
250
+ appendInstance(instance, instancesList);
251
+ const selected = snapshot.instances.find((instance) => instance.id === snapshot.selectedId);
252
+ if (selected) {
253
+ detailSection.append(element(ownerDocument, "h2", selected.name || selected.componentId));
254
+ detailSection.append(element(ownerDocument, "p", selected.file, "path"));
255
+ detailSection.append(element(ownerDocument, "p", `ID: ${selected.id} · Estado: ${selected.status === "hydrated" ? "Hidratado" : selected.status} · Padre: ${selected.parentId ?? "ninguno"} · Hijos: ${selected.childIds.length}`));
256
+ const actions = element(ownerDocument, "div", undefined, "actions");
257
+ const highlightButton = element(ownerDocument, "button", highlightedId === selected.id ? "Quitar resaltado" : "Resaltar en página");
258
+ highlightButton.type = "button";
259
+ highlightButton.setAttribute("aria-pressed", String(highlightedId === selected.id));
260
+ highlightButton.dataset.instanceId = selected.id;
261
+ highlightButton.dataset.action = "highlight";
262
+ highlightButton.addEventListener("click", () => {
263
+ highlightedId = highlightedId === selected.id ? null : selected.id;
264
+ hook?.highlight(highlightedId);
265
+ render();
266
+ });
267
+ const breakpointButton = element(ownerDocument, "button", selected.breakpoint ? "Desactivar punto de parada" : "Activar punto de parada");
268
+ breakpointButton.type = "button";
269
+ breakpointButton.setAttribute("aria-pressed", String(selected.breakpoint));
270
+ breakpointButton.dataset.instanceId = selected.id;
271
+ breakpointButton.dataset.action = "breakpoint";
272
+ breakpointButton.addEventListener("click", () => {
273
+ hook?.setBreakpoint(selected.id, !selected.breakpoint);
274
+ render();
275
+ });
276
+ actions.append(highlightButton, breakpointButton);
277
+ detailSection.append(actions, element(ownerDocument, "h3", "Props"), element(ownerDocument, "pre", formatValue(selected.props)), element(ownerDocument, "h3", "Señales"), element(ownerDocument, "pre", formatValue(selected.signals)));
278
+ }
279
+ else {
280
+ detailSection.append(element(ownerDocument, "p", "Selecciona una instancia para ver props y señales."));
281
+ }
282
+ const recent = snapshot.events.filter((event) => !signalFilter.checked || event.kind === "signal").slice(-30).reverse();
283
+ if (recent.length === 0)
284
+ eventsList.append(element(ownerDocument, "li", "Sin eventos.", "muted"));
285
+ for (const event of recent) {
286
+ const row = element(ownerDocument, "li");
287
+ const instance = byId.get(event.instanceId);
288
+ const label = eventLabel(event);
289
+ if (instance) {
290
+ const button = element(ownerDocument, "button", label, "event-button");
291
+ button.type = "button";
292
+ button.dataset.instanceId = instance.id;
293
+ button.dataset.action = "event";
294
+ button.addEventListener("click", () => {
295
+ hook?.select(instance.id);
296
+ showMobileView("detail");
297
+ render();
298
+ });
299
+ row.append(button);
300
+ }
301
+ else {
302
+ row.textContent = label;
303
+ }
304
+ if (event.kind === "signal" && event.reads?.length) {
305
+ const sites = element(ownerDocument, "details", undefined, "event-reads");
306
+ sites.append(element(ownerDocument, "summary", `${event.reads.length} expresión(es) que dependen de la señal`));
307
+ const list = element(ownerDocument, "ul");
308
+ for (const read of event.reads) {
309
+ const usage = element(ownerDocument, "li");
310
+ usage.append(element(ownerDocument, "code", `${read.file}:${read.line}:${read.column}`), element(ownerDocument, "span", ` ${read.kind}: `), element(ownerDocument, "code", read.expression));
311
+ list.append(usage);
312
+ }
313
+ sites.append(list);
314
+ row.append(sites);
315
+ }
316
+ eventsList.append(row);
317
+ }
318
+ restoreFocus(focusId, focusAction);
319
+ }
320
+ /** Descubre el hook al inicio, al recibir el evento de disponibilidad o al pulsar Actualizar. */
321
+ function connect() {
322
+ if (!mounted)
323
+ return;
324
+ const next = view.__MEEKO_DEVTOOLS__;
325
+ if (next !== hook) {
326
+ unsubscribe?.();
327
+ hook = next;
328
+ unsubscribe = hook?.subscribe(render);
329
+ }
330
+ render();
331
+ }
332
+ function unmount() {
333
+ if (!mounted)
334
+ return;
335
+ mounted = false;
336
+ unsubscribe?.();
337
+ view.removeEventListener("meeko:devtools-ready", connect);
338
+ refreshButton.removeEventListener("click", connect);
339
+ closeButton.removeEventListener("click", unmount);
340
+ if (highlightedId)
341
+ hook?.highlight(null);
342
+ host.remove();
343
+ hook = undefined;
344
+ }
345
+ view.addEventListener("meeko:devtools-ready", connect);
346
+ signalFilter.addEventListener("change", render);
347
+ refreshButton.addEventListener("click", connect);
348
+ closeButton.addEventListener("click", unmount);
349
+ connect();
350
+ return { refresh: connect, unmount };
351
+ }