@meekojs/meeko 0.0.2 → 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 +22 -1
- package/dist/cli.js +35 -24
- package/dist/compiler.js +11 -6
- package/dist/devtools-ui/index.js +55 -6
- package/dist/devtools.d.ts +9 -0
- package/dist/devtools.js +15 -6
- package/dist/dom.d.ts +9 -5
- package/dist/dom.js +76 -14
- package/dist/init.d.ts +2 -0
- package/dist/init.js +207 -0
- package/dist/reactivity.d.ts +13 -1
- package/dist/reactivity.js +29 -7
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ El proyecto está en etapa temprana. Ya funciona el recorrido completo de un con
|
|
|
17
17
|
- [Plan de desarrollo y criterios de aceptación](docs/roadmap.md)
|
|
18
18
|
- [Comparativa reproducible con Vue](docs/benchmarks.md)
|
|
19
19
|
- [DevTools para depurar componentes](docs/devtools.md)
|
|
20
|
+
- [Aplicación piloto SSR](examples/pilot/README.md)
|
|
20
21
|
- [Publicación y verificaciones](docs/releasing.md)
|
|
21
22
|
- [Historial de cambios](CHANGELOG.md)
|
|
22
23
|
- [Guía para contribuir](CONTRIBUTING.md)
|
|
@@ -45,6 +46,21 @@ export default defineConfig({ plugins: [meeko()] });
|
|
|
45
46
|
|
|
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.
|
|
47
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
|
+
|
|
48
64
|
## Estructura
|
|
49
65
|
|
|
50
66
|
| Ruta | Responsabilidad |
|
|
@@ -73,11 +89,12 @@ El paquete incluye TypeScript 7 y la CLI `meeko`. Puedes comprobar un componente
|
|
|
73
89
|
pnpm install
|
|
74
90
|
pnpm run verify
|
|
75
91
|
pnpm run e2e
|
|
92
|
+
pnpm run pilot:e2e
|
|
76
93
|
pnpm run package:smoke
|
|
77
94
|
pnpm run example:serve
|
|
78
95
|
```
|
|
79
96
|
|
|
80
|
-
`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`.
|
|
81
98
|
|
|
82
99
|
Para probar la compilación automática con Vite:
|
|
83
100
|
|
|
@@ -108,6 +125,10 @@ node dist/cli.js types ruta/Componente.meeko
|
|
|
108
125
|
|
|
109
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.
|
|
110
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
|
+
|
|
111
132
|
## Sintaxis inicial
|
|
112
133
|
|
|
113
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> = [];
|
|
@@ -398,7 +403,7 @@ export function compile(source, filename = "anonymous.meeko", options = {}) {
|
|
|
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/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`;
|
|
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
|
}
|
|
@@ -25,7 +25,14 @@ button[aria-pressed="true"] { border-color: #79b8ff; background: #25476c; }
|
|
|
25
25
|
.muted { color: #aab7ca; }
|
|
26
26
|
.path { overflow-wrap: anywhere; }
|
|
27
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:
|
|
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; }
|
|
29
36
|
.events li { overflow-wrap: anywhere; }
|
|
30
37
|
.status { padding: 7px 12px; border-top: 1px solid #334155; color: #aab7ca; }
|
|
31
38
|
@media (max-width: 540px) {
|
|
@@ -81,7 +88,10 @@ function eventLabel(event) {
|
|
|
81
88
|
};
|
|
82
89
|
const time = Number.isFinite(event.at) ? new Date(event.at).toLocaleTimeString("es", { hour12: false }) : "hora desconocida";
|
|
83
90
|
const detail = event.detail ? ` · ${event.detail}` : "";
|
|
84
|
-
|
|
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}`;
|
|
85
95
|
}
|
|
86
96
|
/** Exige un documento conectado a una ventana para registrar eventos del hook. */
|
|
87
97
|
function requireView(document) {
|
|
@@ -131,9 +141,16 @@ export function mountDevtoolsPanel(options = {}) {
|
|
|
131
141
|
main.append(instancesSection, detailSection);
|
|
132
142
|
const eventsSection = element(ownerDocument, "section", undefined, "events");
|
|
133
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);
|
|
134
151
|
const eventsList = element(ownerDocument, "ol");
|
|
135
152
|
eventsList.setAttribute("aria-label", "Eventos recientes de Meeko");
|
|
136
|
-
eventsSection.append(
|
|
153
|
+
eventsSection.append(eventsHead, eventsList);
|
|
137
154
|
const status = element(ownerDocument, "div", undefined, "status");
|
|
138
155
|
status.setAttribute("role", "status");
|
|
139
156
|
status.setAttribute("aria-live", "polite");
|
|
@@ -262,11 +279,42 @@ export function mountDevtoolsPanel(options = {}) {
|
|
|
262
279
|
else {
|
|
263
280
|
detailSection.append(element(ownerDocument, "p", "Selecciona una instancia para ver props y señales."));
|
|
264
281
|
}
|
|
265
|
-
const recent = snapshot.events.slice(-30).reverse();
|
|
282
|
+
const recent = snapshot.events.filter((event) => !signalFilter.checked || event.kind === "signal").slice(-30).reverse();
|
|
266
283
|
if (recent.length === 0)
|
|
267
284
|
eventsList.append(element(ownerDocument, "li", "Sin eventos.", "muted"));
|
|
268
|
-
for (const event of recent)
|
|
269
|
-
|
|
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
|
+
}
|
|
270
318
|
restoreFocus(focusId, focusAction);
|
|
271
319
|
}
|
|
272
320
|
/** Descubre el hook al inicio, al recibir el evento de disponibilidad o al pulsar Actualizar. */
|
|
@@ -295,6 +343,7 @@ export function mountDevtoolsPanel(options = {}) {
|
|
|
295
343
|
hook = undefined;
|
|
296
344
|
}
|
|
297
345
|
view.addEventListener("meeko:devtools-ready", connect);
|
|
346
|
+
signalFilter.addEventListener("change", render);
|
|
298
347
|
refreshButton.addEventListener("click", connect);
|
|
299
348
|
closeButton.addEventListener("click", unmount);
|
|
300
349
|
connect();
|
package/dist/devtools.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DomScope } from "./dom.js";
|
|
2
|
+
import type { EffectSource } from "./reactivity.js";
|
|
2
3
|
/** Valores seguros para transportar una captura al panel de desarrollo. */
|
|
3
4
|
export type DevtoolsValue = null | boolean | number | string | DevtoolsValue[] | {
|
|
4
5
|
[key: string]: DevtoolsValue;
|
|
@@ -23,6 +24,14 @@ export interface DevtoolsEvent {
|
|
|
23
24
|
kind: "mount" | "unmount" | "signal" | "breakpoint" | "select" | "hmr";
|
|
24
25
|
instanceId: string;
|
|
25
26
|
detail?: string;
|
|
27
|
+
/** Identidad del componente conservada incluso después del desmontaje. */
|
|
28
|
+
componentName?: string;
|
|
29
|
+
/** Archivo fuente `.meeko` proporcionado por la compilación de desarrollo. */
|
|
30
|
+
file?: string;
|
|
31
|
+
/** Valor posterior de una señal, copiado con los mismos límites que una captura. */
|
|
32
|
+
value?: DevtoolsValue;
|
|
33
|
+
/** Expresiones de plantilla que dependían de la señal en esta instancia. No son el origen de la escritura. */
|
|
34
|
+
reads?: EffectSource[];
|
|
26
35
|
}
|
|
27
36
|
/** Captura independiente de referencias DOM y de valores no serializables. */
|
|
28
37
|
export interface DevtoolsSnapshot {
|
package/dist/devtools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { effect, isWritableSignal, untrack } from "./reactivity.js";
|
|
1
|
+
import { currentEffectOwner, effect, isWritableSignal, signalReadSites, untrack } from "./reactivity.js";
|
|
2
2
|
const active = new Map();
|
|
3
3
|
const identities = new WeakMap();
|
|
4
4
|
const listeners = new Set();
|
|
@@ -82,10 +82,19 @@ function notify() {
|
|
|
82
82
|
catch { /* Un panel externo no debe romper la aplicación. */ }
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
function record(kind, instanceId, detail) {
|
|
85
|
+
function record(kind, instanceId, detail, value, source, reads) {
|
|
86
86
|
const event = { id: nextEvent++, at: Date.now(), kind, instanceId };
|
|
87
87
|
if (detail !== undefined)
|
|
88
88
|
event.detail = detail;
|
|
89
|
+
const entry = source ?? active.get(instanceId);
|
|
90
|
+
if (entry) {
|
|
91
|
+
event.componentName = entry.name;
|
|
92
|
+
event.file = entry.file;
|
|
93
|
+
}
|
|
94
|
+
if (value !== undefined)
|
|
95
|
+
event.value = value;
|
|
96
|
+
if (reads?.length)
|
|
97
|
+
event.reads = reads;
|
|
89
98
|
recent.push(event);
|
|
90
99
|
if (recent.length > 100)
|
|
91
100
|
recent.shift();
|
|
@@ -246,7 +255,7 @@ function unregister(entry) {
|
|
|
246
255
|
selectedId = null;
|
|
247
256
|
if (highlightedId === entry.id)
|
|
248
257
|
removeHighlight();
|
|
249
|
-
record("unmount", entry.id);
|
|
258
|
+
record("unmount", entry.id, undefined, undefined, entry);
|
|
250
259
|
}
|
|
251
260
|
/** Registra señales y ámbito de una instancia hidratada en Vite desarrollo. */
|
|
252
261
|
export function registerDevtoolsInstance(input) {
|
|
@@ -263,7 +272,7 @@ export function registerDevtoolsInstance(input) {
|
|
|
263
272
|
const existing = active.get(identity.id);
|
|
264
273
|
if (existing)
|
|
265
274
|
unregister(existing);
|
|
266
|
-
const entry = { ...input, id: identity.id, key, breakpoint: identity.breakpoint, stopSignals: [] };
|
|
275
|
+
const entry = { ...input, id: identity.id, key, owner: currentEffectOwner(), breakpoint: identity.breakpoint, stopSignals: [] };
|
|
267
276
|
active.set(entry.id, entry);
|
|
268
277
|
for (const [name, value] of Object.entries(input.bindings)) {
|
|
269
278
|
if (!isWritableSignal(value))
|
|
@@ -271,12 +280,12 @@ export function registerDevtoolsInstance(input) {
|
|
|
271
280
|
const signal = value;
|
|
272
281
|
let initial = true;
|
|
273
282
|
entry.stopSignals.push(effect(() => {
|
|
274
|
-
|
|
283
|
+
const current = signal.value;
|
|
275
284
|
if (initial) {
|
|
276
285
|
initial = false;
|
|
277
286
|
return;
|
|
278
287
|
}
|
|
279
|
-
record("signal", entry.id, name);
|
|
288
|
+
record("signal", entry.id, name, safeValue(current), entry, signalReadSites(signal, entry.owner));
|
|
280
289
|
if (entry.breakpoint) {
|
|
281
290
|
debugger;
|
|
282
291
|
}
|
package/dist/dom.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Signal } from "./reactivity.js";
|
|
1
|
+
import type { EffectSource, Signal } from "./reactivity.js";
|
|
2
2
|
/** Fragmento de DOM que pertenece a una instancia o a una región dinámica. */
|
|
3
3
|
export interface DomScope {
|
|
4
4
|
root: Element;
|
|
@@ -26,9 +26,9 @@ export declare function renderComponent<Props extends object>(id: number, compon
|
|
|
26
26
|
/** Hidrata un hijo dentro de su rango y actualiza props reactivas con limpieza previa. */
|
|
27
27
|
export declare function bindComponent<Props extends object>(scope: DomScope, id: number, component: ComponentModule<Props>, read: () => Props, location: string): () => void;
|
|
28
28
|
/** Conecta un texto reactivo al marcador SSR y comprueba su valor inicial. */
|
|
29
|
-
export declare function bindText(scope: DomScope, id: number, read: () => unknown, location: string): () => void;
|
|
29
|
+
export declare function bindText(scope: DomScope, id: number, read: () => unknown, location: string, source?: EffectSource): () => void;
|
|
30
30
|
/** Conecta un atributo dinámico al elemento SSR y comprueba su valor inicial. */
|
|
31
|
-
export declare function bindAttribute(scope: DomScope, id: number, name: string, read: () => unknown, location: string): () => void;
|
|
31
|
+
export declare function bindAttribute(scope: DomScope, id: number, name: string, read: () => unknown, location: string, source?: EffectSource): () => void;
|
|
32
32
|
/** Conecta un evento al elemento SSR y entrega su función de limpieza. */
|
|
33
33
|
export declare function bindEvent(scope: DomScope, id: number, name: string, handler: (event: Event) => void): () => void;
|
|
34
34
|
/** Genera los límites estables de una condición en SSR. */
|
|
@@ -39,5 +39,9 @@ export declare function bindIf(scope: DomScope, id: number, read: () => unknown,
|
|
|
39
39
|
export declare function keyToken(key: unknown): string;
|
|
40
40
|
/** Renderiza una lista con límites identificados por claves únicas. */
|
|
41
41
|
export declare function renderFor<T>(id: number, items: Iterable<T>, keyOf: (item: T, index: number) => string | number, render: (item: T, index: number) => string): string;
|
|
42
|
-
/**
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Hidrata una lista y reordena regiones por clave sin reemplazar nodos conservados.
|
|
44
|
+
* El compilador desactiva `trackIndex` sólo cuando el cuerpo de hidratación no lee
|
|
45
|
+
* la señal de índice; las llamadas directas conservan la señal por defecto.
|
|
46
|
+
*/
|
|
47
|
+
export declare function bindFor<T>(scope: DomScope, id: number, read: () => Iterable<T>, keyOf: (item: T, index: number) => string | number, render: (item: T, index: number) => string, hydrate: (item: Signal<T>, index: Signal<number>, inner: DomScope) => () => void, trackIndex?: boolean): () => void;
|
package/dist/dom.js
CHANGED
|
@@ -133,27 +133,44 @@ export function bindComponent(scope, id, component, read, location) {
|
|
|
133
133
|
return () => { stop(); release(); };
|
|
134
134
|
}
|
|
135
135
|
/** Conecta un texto reactivo al marcador SSR y comprueba su valor inicial. */
|
|
136
|
-
export function bindText(scope, id, read, location) {
|
|
136
|
+
export function bindText(scope, id, read, location, source) {
|
|
137
137
|
const marker = findComment(scope, `meeko:text:${id}`);
|
|
138
138
|
const text = marker.nextSibling;
|
|
139
139
|
if (!text || text.nodeType !== 3)
|
|
140
140
|
throw new Error(`Missing text node in ${location}`);
|
|
141
|
-
|
|
141
|
+
const initial = String(read());
|
|
142
|
+
if (text.textContent !== initial)
|
|
142
143
|
throw new Error(`Hydration mismatch in ${location}`);
|
|
143
|
-
|
|
144
|
+
let first = true;
|
|
145
|
+
return effect(() => {
|
|
146
|
+
const next = String(read());
|
|
147
|
+
if (first) {
|
|
148
|
+
first = false;
|
|
149
|
+
if (text.textContent === next)
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
text.textContent = next;
|
|
153
|
+
}, source);
|
|
144
154
|
}
|
|
145
155
|
/** Conecta un atributo dinámico al elemento SSR y comprueba su valor inicial. */
|
|
146
|
-
export function bindAttribute(scope, id, name, read, location) {
|
|
156
|
+
export function bindAttribute(scope, id, name, read, location, source) {
|
|
147
157
|
const target = findElement(scope, `data-meeko-a-${id}`);
|
|
148
|
-
|
|
158
|
+
const initial = attributeValue(name, read());
|
|
159
|
+
if (target.getAttribute(name) !== initial)
|
|
149
160
|
throw new Error(`Hydration mismatch in ${location}`);
|
|
161
|
+
let first = true;
|
|
150
162
|
return effect(() => {
|
|
151
163
|
const next = attributeValue(name, read());
|
|
164
|
+
if (first) {
|
|
165
|
+
first = false;
|
|
166
|
+
if (target.getAttribute(name) === next)
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
152
169
|
if (next === null)
|
|
153
170
|
target.removeAttribute(name);
|
|
154
171
|
else
|
|
155
172
|
target.setAttribute(name, next);
|
|
156
|
-
});
|
|
173
|
+
}, source);
|
|
157
174
|
}
|
|
158
175
|
/** Conecta un evento al elemento SSR y entrega su función de limpieza. */
|
|
159
176
|
export function bindEvent(scope, id, name, handler) {
|
|
@@ -227,11 +244,42 @@ function moveRange(start, end, before) {
|
|
|
227
244
|
}
|
|
228
245
|
parent.insertBefore(fragment, before);
|
|
229
246
|
}
|
|
230
|
-
/**
|
|
231
|
-
|
|
247
|
+
/** Encuentra las posiciones que ya mantienen el orden relativo de la lista anterior. */
|
|
248
|
+
function stableListPositions(indices) {
|
|
249
|
+
const tails = [];
|
|
250
|
+
const previous = new Int32Array(indices.length).fill(-1);
|
|
251
|
+
for (let index = 0; index < indices.length; index++) {
|
|
252
|
+
const oldIndex = indices[index];
|
|
253
|
+
if (oldIndex < 0)
|
|
254
|
+
continue;
|
|
255
|
+
let low = 0;
|
|
256
|
+
let high = tails.length;
|
|
257
|
+
while (low < high) {
|
|
258
|
+
const middle = (low + high) >>> 1;
|
|
259
|
+
if (indices[tails[middle]] < oldIndex)
|
|
260
|
+
low = middle + 1;
|
|
261
|
+
else
|
|
262
|
+
high = middle;
|
|
263
|
+
}
|
|
264
|
+
if (low > 0)
|
|
265
|
+
previous[index] = tails[low - 1];
|
|
266
|
+
tails[low] = index;
|
|
267
|
+
}
|
|
268
|
+
const stable = new Set();
|
|
269
|
+
for (let index = tails.at(-1) ?? -1; index >= 0; index = previous[index])
|
|
270
|
+
stable.add(index);
|
|
271
|
+
return stable;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Hidrata una lista y reordena regiones por clave sin reemplazar nodos conservados.
|
|
275
|
+
* El compilador desactiva `trackIndex` sólo cuando el cuerpo de hidratación no lee
|
|
276
|
+
* la señal de índice; las llamadas directas conservan la señal por defecto.
|
|
277
|
+
*/
|
|
278
|
+
export function bindFor(scope, id, read, keyOf, render, hydrate, trackIndex = true) {
|
|
232
279
|
const outer = region(scope, "for", id);
|
|
233
280
|
const entries = new Map();
|
|
234
281
|
const initial = [...read()];
|
|
282
|
+
let currentKeys = [];
|
|
235
283
|
let cursor = outer.start.nextSibling;
|
|
236
284
|
for (let index = 0; index < initial.length; index++) {
|
|
237
285
|
const item = initial[index];
|
|
@@ -249,23 +297,35 @@ export function bindFor(scope, id, read, keyOf, render, hydrate) {
|
|
|
249
297
|
if (!end)
|
|
250
298
|
throw new Error(`Missing end marker for list key ${key}`);
|
|
251
299
|
const value = signal(item);
|
|
252
|
-
const position = signal(index);
|
|
300
|
+
const position = trackIndex ? signal(index) : undefined;
|
|
253
301
|
const release = createRoot((dispose) => {
|
|
254
302
|
const cleanup = hydrate(value, position, { root: scope.root, start, end: end });
|
|
255
303
|
return () => { cleanup(); dispose(); };
|
|
256
304
|
});
|
|
257
305
|
entries.set(key, { key, start, end: end, value, index: position, release });
|
|
306
|
+
currentKeys.push(key);
|
|
258
307
|
cursor = end.nextSibling;
|
|
259
308
|
}
|
|
260
309
|
if (cursor !== outer.end)
|
|
261
310
|
throw new Error(`Hydration mismatch in list ${id}: extra nodes`);
|
|
311
|
+
let initialItems = initial;
|
|
262
312
|
const stop = effect(() => {
|
|
263
313
|
const items = [...read()];
|
|
314
|
+
if (initialItems) {
|
|
315
|
+
const previous = initialItems;
|
|
316
|
+
initialItems = undefined;
|
|
317
|
+
if (items.length === previous.length && items.every((item, index) => Object.is(item, previous[index]) && keyToken(keyOf(item, index)) === currentKeys[index]))
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
264
320
|
const nextKeys = items.map((item, index) => keyToken(keyOf(item, index)));
|
|
265
|
-
|
|
321
|
+
const nextKeySet = new Set(nextKeys);
|
|
322
|
+
if (nextKeySet.size !== nextKeys.length)
|
|
266
323
|
throw new Error(`Duplicate Meeko list key in region ${id}`);
|
|
324
|
+
const oldPositions = new Map(currentKeys.map((key, index) => [key, index]));
|
|
325
|
+
const oldIndices = nextKeys.map((key) => oldPositions.get(key) ?? -1);
|
|
326
|
+
const stable = stableListPositions(oldIndices);
|
|
267
327
|
for (const [key, entry] of entries) {
|
|
268
|
-
if (
|
|
328
|
+
if (nextKeySet.has(key))
|
|
269
329
|
continue;
|
|
270
330
|
entry.release();
|
|
271
331
|
let node = entry.start;
|
|
@@ -293,7 +353,7 @@ export function bindFor(scope, id, read, keyOf, render, hydrate) {
|
|
|
293
353
|
if (!start)
|
|
294
354
|
throw new Error(`Missing new list marker for ${key}`);
|
|
295
355
|
const value = signal(item);
|
|
296
|
-
const position = signal(index);
|
|
356
|
+
const position = trackIndex ? signal(index) : undefined;
|
|
297
357
|
const release = createRoot((dispose) => {
|
|
298
358
|
const cleanup = hydrate(value, position, { root: scope.root, start: start, end });
|
|
299
359
|
return () => { cleanup(); dispose(); };
|
|
@@ -303,12 +363,14 @@ export function bindFor(scope, id, read, keyOf, render, hydrate) {
|
|
|
303
363
|
}
|
|
304
364
|
else {
|
|
305
365
|
entry.value.value = item;
|
|
306
|
-
entry.index
|
|
366
|
+
if (entry.index)
|
|
367
|
+
entry.index.value = index;
|
|
307
368
|
}
|
|
308
|
-
if (entry.end.nextSibling !== before)
|
|
369
|
+
if (oldIndices[index] !== -1 && !stable.has(index) && entry.end.nextSibling !== before)
|
|
309
370
|
moveRange(entry.start, entry.end, before);
|
|
310
371
|
before = entry.start;
|
|
311
372
|
}
|
|
373
|
+
currentKeys = nextKeys;
|
|
312
374
|
});
|
|
313
375
|
return () => { stop(); for (const entry of entries.values())
|
|
314
376
|
entry.release(); entries.clear(); };
|
package/dist/init.d.ts
ADDED
package/dist/init.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
/** Entradas que puede dejar un gestor de paquetes antes de generar el proyecto. */
|
|
4
|
+
const packageScaffoldEntries = new Set(["package.json", "package-lock.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", "yarn.lock", "bun.lock", "bun.lockb", "node_modules", ".npmrc", ".git"]);
|
|
5
|
+
/** Comprueba que un valor JSON sea un objeto de configuración. */
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
/** Archivos autocontenidos del proyecto inicial, incluidos en la CLI publicada. */
|
|
10
|
+
const starterFiles = {
|
|
11
|
+
".gitignore": "node_modules/\ndist/\n*.d.meeko.ts\n",
|
|
12
|
+
"index.html": `<!doctype html>
|
|
13
|
+
<html lang="es">
|
|
14
|
+
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Meeko SSR</title></head>
|
|
15
|
+
<body><div id="app"></div><script type="module" src="/src/entry-client.ts"></script></body>
|
|
16
|
+
</html>
|
|
17
|
+
`,
|
|
18
|
+
"vite.config.ts": `import { defineConfig } from "vite";
|
|
19
|
+
import { meeko } from "@meekojs/meeko/vite";
|
|
20
|
+
|
|
21
|
+
export default defineConfig({ plugins: [meeko()] });
|
|
22
|
+
`,
|
|
23
|
+
"tsconfig.json": `{
|
|
24
|
+
"compilerOptions": {
|
|
25
|
+
"target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext",
|
|
26
|
+
"lib": ["ES2022", "DOM"], "types": ["node", "vite/client"],
|
|
27
|
+
"strict": true, "noEmit": true, "allowArbitraryExtensions": true,
|
|
28
|
+
"skipLibCheck": true
|
|
29
|
+
},
|
|
30
|
+
"include": ["src/**/*.ts", "vite.config.ts"]
|
|
31
|
+
}
|
|
32
|
+
`,
|
|
33
|
+
"src/App.meeko": `<script lang="ts">
|
|
34
|
+
import { signal } from "@meekojs/meeko";
|
|
35
|
+
|
|
36
|
+
/** Propiedades transferidas desde el servidor. */
|
|
37
|
+
export interface Props { initial: number }
|
|
38
|
+
|
|
39
|
+
const props = defineProps<Props>();
|
|
40
|
+
const count = signal(props.initial);
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<main>
|
|
45
|
+
<h1>Meeko SSR</h1>
|
|
46
|
+
<button type="button" on:click="() => count.value++">Contador: {count.value}</button>
|
|
47
|
+
</main>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<style scoped>
|
|
51
|
+
main { max-width: 40rem; margin: 4rem auto; font: 1.2rem system-ui; }
|
|
52
|
+
button { cursor: pointer; padding: .65rem 1rem; }
|
|
53
|
+
</style>
|
|
54
|
+
`,
|
|
55
|
+
"src/entry-server.ts": `import { serializeJson } from "@meekojs/meeko";
|
|
56
|
+
import { css, renderWithState } from "./App.meeko";
|
|
57
|
+
|
|
58
|
+
/** Genera HTML y estado inicial desde el mismo componente que hidrata el cliente. */
|
|
59
|
+
export function renderPage(clientSource: string, initial = 1): string {
|
|
60
|
+
const props = { initial };
|
|
61
|
+
const rendered = renderWithState(props);
|
|
62
|
+
return '<!doctype html><html lang="es"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Meeko SSR</title><style>'
|
|
63
|
+
+ css + '</style></head><body><div id="app">' + rendered.html + '</div><script id="meeko-payload" type="application/json">'
|
|
64
|
+
+ serializeJson({ props, state: rendered.state }) + '</script><script type="module" src="' + clientSource + '"></script></body></html>';
|
|
65
|
+
}
|
|
66
|
+
`,
|
|
67
|
+
"src/entry-client.ts": `import { hydrate } from "./App.meeko";
|
|
68
|
+
import type { HotState } from "@meekojs/meeko";
|
|
69
|
+
|
|
70
|
+
const root = document.getElementById("app");
|
|
71
|
+
const payloadNode = document.getElementById("meeko-payload");
|
|
72
|
+
if (!root || !payloadNode?.textContent) throw new Error("Missing SSR root or payload");
|
|
73
|
+
const payload: unknown = JSON.parse(payloadNode.textContent);
|
|
74
|
+
if (typeof payload !== "object" || payload === null || !("props" in payload) || !("state" in payload)) throw new TypeError("Invalid SSR payload");
|
|
75
|
+
const { props, state } = payload;
|
|
76
|
+
if (typeof props !== "object" || props === null || !("initial" in props) || typeof props.initial !== "number" || !Number.isFinite(props.initial)) throw new TypeError("Invalid SSR props");
|
|
77
|
+
if (typeof state !== "object" || state === null || !("count" in state) || typeof state.count !== "number" || !Number.isFinite(state.count)) throw new TypeError("Invalid SSR state");
|
|
78
|
+
hydrate(root, { initial: props.initial }, { count: state.count } satisfies HotState);
|
|
79
|
+
`,
|
|
80
|
+
"server.mjs": `import { createServer } from "node:http";
|
|
81
|
+
import { readFile } from "node:fs/promises";
|
|
82
|
+
import { dirname, extname, resolve, sep } from "node:path";
|
|
83
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
84
|
+
import { createServer as createViteServer } from "vite";
|
|
85
|
+
|
|
86
|
+
const root = dirname(fileURLToPath(import.meta.url));
|
|
87
|
+
const production = process.argv.includes("--production");
|
|
88
|
+
const vite = production ? undefined : await createViteServer({ root, server: { middlewareMode: true }, appType: "custom" });
|
|
89
|
+
const clientRoot = resolve(root, "dist/client");
|
|
90
|
+
let clientSource = "/src/entry-client.ts";
|
|
91
|
+
if (production) {
|
|
92
|
+
const manifest = JSON.parse(await readFile(resolve(clientRoot, ".vite/manifest.json"), "utf8"));
|
|
93
|
+
const entry = manifest["index.html"];
|
|
94
|
+
if (!entry?.file) throw new Error("Missing Vite client entry");
|
|
95
|
+
clientSource = "/" + entry.file;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const contentTypes = { ".js": "text/javascript", ".css": "text/css", ".png": "image/png", ".svg": "image/svg+xml", ".ico": "image/x-icon" };
|
|
99
|
+
const server = createServer(async (request, response) => {
|
|
100
|
+
try {
|
|
101
|
+
const path = new URL(request.url ?? "/", "http://localhost").pathname;
|
|
102
|
+
if (path === "/") {
|
|
103
|
+
const module = vite ? await vite.ssrLoadModule("/src/entry-server.ts") : await import(pathToFileURL(resolve(root, "dist/server/entry-server.js")).href);
|
|
104
|
+
const html = module.renderPage(clientSource);
|
|
105
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
106
|
+
response.end(vite ? await vite.transformIndexHtml(path, html) : html);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (vite) {
|
|
110
|
+
vite.middlewares(request, response, () => { response.writeHead(404); response.end(); });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const file = resolve(clientRoot, "." + path);
|
|
114
|
+
if (!file.startsWith(clientRoot + sep)) { response.writeHead(403); response.end(); return; }
|
|
115
|
+
const body = await readFile(file);
|
|
116
|
+
response.writeHead(200, { "content-type": contentTypes[extname(file)] ?? "application/octet-stream" });
|
|
117
|
+
response.end(body);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(error);
|
|
120
|
+
response.writeHead(500);
|
|
121
|
+
response.end("Internal server error");
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const port = Number(process.env.PORT ?? 4173);
|
|
126
|
+
server.listen(port, "127.0.0.1", () => console.log("http://127.0.0.1:" + port));
|
|
127
|
+
process.on("SIGINT", () => { server.close(); void vite?.close(); });
|
|
128
|
+
`,
|
|
129
|
+
"README.md": `# Meeko SSR starter
|
|
130
|
+
|
|
131
|
+
Proyecto inicial con renderizado en servidor e hidratación del mismo componente \`.meeko\`.
|
|
132
|
+
|
|
133
|
+
\`\`\`sh
|
|
134
|
+
npm install
|
|
135
|
+
npm run dev
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
Abre http://127.0.0.1:4173. El servidor produce HTML y transfiere las props y señales al navegador.
|
|
139
|
+
|
|
140
|
+
\`npm run check\` genera los tipos del componente y comprueba TypeScript. \`npm run build\` compila cliente y servidor; \`npm start\` sirve ambas compilaciones con SSR.
|
|
141
|
+
`,
|
|
142
|
+
};
|
|
143
|
+
/** Crea un proyecto SSR independiente sin sobrescribir archivos del usuario. */
|
|
144
|
+
export async function initProject(directory) {
|
|
145
|
+
const target = resolve(directory);
|
|
146
|
+
const name = basename(target).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+|[._-]+$/g, "") || "meeko-app";
|
|
147
|
+
const packageMetadata = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
148
|
+
if (typeof packageMetadata !== "object" || packageMetadata === null || !("version" in packageMetadata) || typeof packageMetadata.version !== "string") {
|
|
149
|
+
throw new Error("Meeko package version is unavailable");
|
|
150
|
+
}
|
|
151
|
+
let existingEntries = [];
|
|
152
|
+
try {
|
|
153
|
+
if (!(await stat(target)).isDirectory())
|
|
154
|
+
throw new Error(`Destination is not a directory: ${target}`);
|
|
155
|
+
existingEntries = await readdir(target);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
const unexpected = existingEntries.filter((entry) => !packageScaffoldEntries.has(entry));
|
|
162
|
+
if (unexpected.length > 0 || (existingEntries.length > 0 && !existingEntries.includes("package.json"))) {
|
|
163
|
+
throw new Error(`Destination is not empty: ${target}${unexpected.length > 0 ? ` (${unexpected.join(", ")})` : ""}`);
|
|
164
|
+
}
|
|
165
|
+
const existing = existingEntries.includes("package.json") ? JSON.parse(await readFile(join(target, "package.json"), "utf8")) : {};
|
|
166
|
+
if (!isRecord(existing))
|
|
167
|
+
throw new Error(`Invalid package.json in ${target}`);
|
|
168
|
+
const existingScripts = existing.scripts ?? {};
|
|
169
|
+
const existingDependencies = existing.dependencies ?? {};
|
|
170
|
+
const existingDevDependencies = existing.devDependencies ?? {};
|
|
171
|
+
const existingEngines = existing.engines ?? {};
|
|
172
|
+
if (!isRecord(existingScripts) || !isRecord(existingDependencies) || !isRecord(existingDevDependencies) || !isRecord(existingEngines)) {
|
|
173
|
+
throw new Error(`Invalid package.json in ${target}`);
|
|
174
|
+
}
|
|
175
|
+
const scripts = {
|
|
176
|
+
dev: "node server.mjs",
|
|
177
|
+
check: "meeko types src/App.meeko && tsc --noEmit",
|
|
178
|
+
build: "npm run check && vite build --outDir dist/client --manifest && vite build --ssr src/entry-server.ts --outDir dist/server",
|
|
179
|
+
start: "node server.mjs --production",
|
|
180
|
+
};
|
|
181
|
+
const devDependencies = { "@types/node": "^24.0.0", typescript: "^7.0.2", vite: "^8.3.5" };
|
|
182
|
+
for (const command of Object.keys(scripts)) {
|
|
183
|
+
if (command in existingScripts)
|
|
184
|
+
throw new Error(`Destination already defines the ${command} script: ${target}`);
|
|
185
|
+
}
|
|
186
|
+
if (Object.keys(existingDependencies).some((dependency) => dependency !== "@meekojs/meeko") || Object.keys(existingDevDependencies).some((dependency) => !(dependency in devDependencies))) {
|
|
187
|
+
throw new Error(`Destination contains other dependencies: ${target}`);
|
|
188
|
+
}
|
|
189
|
+
const packageFile = JSON.stringify({
|
|
190
|
+
...existing,
|
|
191
|
+
name: typeof existing.name === "string" ? existing.name : name,
|
|
192
|
+
private: true,
|
|
193
|
+
type: "module",
|
|
194
|
+
engines: { ...existingEngines, node: ">=24" },
|
|
195
|
+
scripts: { ...existingScripts, ...scripts },
|
|
196
|
+
dependencies: { ...existingDependencies, "@meekojs/meeko": packageMetadata.version },
|
|
197
|
+
devDependencies: { ...existingDevDependencies, ...devDependencies },
|
|
198
|
+
}, null, 2) + "\n";
|
|
199
|
+
await mkdir(target, { recursive: true });
|
|
200
|
+
for (const [filename, content] of Object.entries(starterFiles)) {
|
|
201
|
+
const path = join(target, filename);
|
|
202
|
+
await mkdir(dirname(path), { recursive: true });
|
|
203
|
+
await writeFile(path, content, { encoding: "utf8", flag: "wx" });
|
|
204
|
+
}
|
|
205
|
+
await writeFile(join(target, "package.json"), packageFile, { encoding: "utf8", flag: existingEntries.includes("package.json") ? "w" : "wx" });
|
|
206
|
+
return target;
|
|
207
|
+
}
|
package/dist/reactivity.d.ts
CHANGED
|
@@ -6,12 +6,24 @@ export interface ReadonlySignal<T> {
|
|
|
6
6
|
export interface Signal<T> extends ReadonlySignal<T> {
|
|
7
7
|
value: T;
|
|
8
8
|
}
|
|
9
|
+
/** Ubicación de una expresión de plantilla que lee una señal durante desarrollo. */
|
|
10
|
+
export interface EffectSource {
|
|
11
|
+
file: string;
|
|
12
|
+
line: number;
|
|
13
|
+
column: number;
|
|
14
|
+
expression: string;
|
|
15
|
+
kind: "text" | "attribute";
|
|
16
|
+
}
|
|
9
17
|
/** Identifica señales editables creadas por Meeko para conservarlas durante HMR. */
|
|
10
18
|
export declare function isWritableSignal(value: unknown): value is Signal<unknown>;
|
|
11
19
|
/** Crea un valor observable. Las escrituras iguales no notifican cambios. */
|
|
12
20
|
export declare function signal<T>(initial: T): Signal<T>;
|
|
13
21
|
/** Ejecuta una función cuando cambian las señales leídas por ella. */
|
|
14
|
-
export declare function effect(fn: () => void): () => void;
|
|
22
|
+
export declare function effect(fn: () => void, source?: EffectSource): () => void;
|
|
23
|
+
/** Identidad opaca de la raíz reactiva activa; permite aislar instancias anidadas. */
|
|
24
|
+
export declare function currentEffectOwner(): object | undefined;
|
|
25
|
+
/** Devuelve expresiones de plantilla actualmente dependientes de una señal en una raíz. */
|
|
26
|
+
export declare function signalReadSites(value: Signal<unknown>, owner: object | undefined): EffectSource[];
|
|
15
27
|
/** Deriva un valor de otras señales y lo actualiza cuando cambian. */
|
|
16
28
|
export declare function computed<T>(fn: () => T): ReadonlySignal<T>;
|
|
17
29
|
/** Agrupa varias escrituras en una sola ronda de notificaciones. */
|
package/dist/reactivity.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const writableSignals = new WeakSet();
|
|
2
2
|
const derivedSignals = new WeakSet();
|
|
3
|
+
const effectSources = new WeakMap();
|
|
3
4
|
/** Identifica señales editables creadas por Meeko para conservarlas durante HMR. */
|
|
4
5
|
export function isWritableSignal(value) {
|
|
5
6
|
return typeof value === "object" && value !== null && writableSignals.has(value) && !derivedSignals.has(value);
|
|
@@ -11,7 +12,7 @@ let flushing = false;
|
|
|
11
12
|
const pending = new Set();
|
|
12
13
|
function detach(observer) {
|
|
13
14
|
for (const dependency of observer.dependencies)
|
|
14
|
-
dependency.subscribers
|
|
15
|
+
dependency.subscribers?.delete(observer);
|
|
15
16
|
observer.dependencies.clear();
|
|
16
17
|
}
|
|
17
18
|
function flush() {
|
|
@@ -37,12 +38,11 @@ function flush() {
|
|
|
37
38
|
/** Crea un valor observable. Las escrituras iguales no notifican cambios. */
|
|
38
39
|
export function signal(initial) {
|
|
39
40
|
let current = initial;
|
|
40
|
-
const subscribers = new Set();
|
|
41
41
|
const result = {
|
|
42
|
-
subscribers,
|
|
42
|
+
subscribers: undefined,
|
|
43
43
|
get value() {
|
|
44
44
|
if (activeObserver) {
|
|
45
|
-
subscribers.add(activeObserver);
|
|
45
|
+
(result.subscribers ??= new Set()).add(activeObserver);
|
|
46
46
|
activeObserver.dependencies.add(result);
|
|
47
47
|
}
|
|
48
48
|
return current;
|
|
@@ -51,8 +51,9 @@ export function signal(initial) {
|
|
|
51
51
|
if (Object.is(current, next))
|
|
52
52
|
return;
|
|
53
53
|
current = next;
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
if (result.subscribers)
|
|
55
|
+
for (const observer of result.subscribers)
|
|
56
|
+
pending.add(observer);
|
|
56
57
|
flush();
|
|
57
58
|
},
|
|
58
59
|
};
|
|
@@ -60,7 +61,7 @@ export function signal(initial) {
|
|
|
60
61
|
return result;
|
|
61
62
|
}
|
|
62
63
|
/** Ejecuta una función cuando cambian las señales leídas por ella. */
|
|
63
|
-
export function effect(fn) {
|
|
64
|
+
export function effect(fn, source) {
|
|
64
65
|
const owner = activeOwner;
|
|
65
66
|
const observer = {
|
|
66
67
|
dependencies: new Set(),
|
|
@@ -80,6 +81,8 @@ export function effect(fn) {
|
|
|
80
81
|
}
|
|
81
82
|
},
|
|
82
83
|
};
|
|
84
|
+
if (source)
|
|
85
|
+
effectSources.set(observer, { owner, source });
|
|
83
86
|
owner?.observers.add(observer);
|
|
84
87
|
observer.run();
|
|
85
88
|
return () => {
|
|
@@ -89,6 +92,25 @@ export function effect(fn) {
|
|
|
89
92
|
owner?.observers.delete(observer);
|
|
90
93
|
};
|
|
91
94
|
}
|
|
95
|
+
/** Identidad opaca de la raíz reactiva activa; permite aislar instancias anidadas. */
|
|
96
|
+
export function currentEffectOwner() {
|
|
97
|
+
return activeOwner;
|
|
98
|
+
}
|
|
99
|
+
/** Devuelve expresiones de plantilla actualmente dependientes de una señal en una raíz. */
|
|
100
|
+
export function signalReadSites(value, owner) {
|
|
101
|
+
if (!owner || !isWritableSignal(value))
|
|
102
|
+
return [];
|
|
103
|
+
const sites = new Map();
|
|
104
|
+
for (const observer of value.subscribers ?? []) {
|
|
105
|
+
const metadata = effectSources.get(observer);
|
|
106
|
+
if (observer.disposed || metadata?.owner !== owner || !metadata)
|
|
107
|
+
continue;
|
|
108
|
+
const source = metadata.source;
|
|
109
|
+
const key = `${source.file}\u0000${source.line}\u0000${source.column}\u0000${source.kind}\u0000${source.expression}`;
|
|
110
|
+
sites.set(key, source);
|
|
111
|
+
}
|
|
112
|
+
return [...sites.values()];
|
|
113
|
+
}
|
|
92
114
|
/** Deriva un valor de otras señales y lo actualiza cuando cambian. */
|
|
93
115
|
export function computed(fn) {
|
|
94
116
|
const result = signal(undefined);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meekojs/meeko",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"packageManager": "pnpm@12.14.0",
|
|
5
5
|
"description": "Typed .meeko component engine with SSR, hydration, signals, Vite, and DevTools.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,13 +45,18 @@
|
|
|
45
45
|
"vite:dev": "vite examples/vite --config examples/vite/vite.config.ts",
|
|
46
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",
|
|
47
47
|
"vite:build": "vite build examples/vite --config examples/vite/vite.config.ts",
|
|
48
|
+
"pilot:dev": "tsx examples/pilot/server.ts",
|
|
49
|
+
"pilot:check": "pnpm run build && node dist/cli.js check examples/pilot/App.meeko && node dist/cli.js types examples/pilot/App.meeko && tsc -p examples/pilot/tsconfig.json --noEmit",
|
|
50
|
+
"pilot:build": "tsx examples/pilot/build.ts",
|
|
51
|
+
"pilot:serve": "node examples/pilot/.output/server/http.js",
|
|
52
|
+
"pilot:e2e": "tsx --test tests/pilot.e2e.ts",
|
|
48
53
|
"e2e": "pnpm run example:bundle && pnpm run vite:build && tsx --test tests/browser.e2e.ts",
|
|
49
54
|
"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",
|
|
50
55
|
"benchmark": "pnpm run build && pnpm run benchmark:check && node --expose-gc --import tsx benchmarks/run.ts",
|
|
51
56
|
"benchmark:scenarios": "pnpm run build && pnpm run benchmark:check && node --expose-gc --import tsx benchmarks/scenarios.ts",
|
|
52
57
|
"package:smoke": "pwsh -NoProfile -File tests/package-consumer.ps1",
|
|
53
58
|
"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"
|
|
59
|
+
"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 && pnpm run pilot:check"
|
|
55
60
|
},
|
|
56
61
|
"dependencies": {
|
|
57
62
|
"@typescript/typescript6": "6.0.2",
|