@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 +47 -1
- package/dist/cli.js +35 -24
- package/dist/compiler.js +12 -7
- package/dist/devtools-ui/index.d.ts +14 -0
- package/dist/devtools-ui/index.js +351 -0
- package/dist/devtools.d.ts +65 -0
- package/dist/devtools.js +296 -0
- package/dist/dom.d.ts +9 -5
- package/dist/dom.js +76 -14
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/init.d.ts +2 -0
- package/dist/init.js +207 -0
- package/dist/internal.d.ts +13 -0
- package/dist/internal.js +11 -0
- package/dist/reactivity.d.ts +13 -1
- package/dist/reactivity.js +29 -7
- package/package.json +20 -5
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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
+
}
|