@meekojs/meeko 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +126 -2
- package/dist/check.d.ts +3 -0
- package/dist/check.js +130 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +43 -0
- package/dist/compiler/mapping.d.ts +16 -0
- package/dist/compiler/mapping.js +48 -0
- package/dist/compiler/sfc.d.ts +43 -0
- package/dist/compiler/sfc.js +70 -0
- package/dist/compiler/template.d.ts +31 -0
- package/dist/compiler/template.js +44 -0
- package/dist/compiler.d.ts +13 -0
- package/dist/compiler.js +404 -0
- package/dist/devtools-ui/index.d.ts +14 -0
- package/dist/devtools-ui/index.js +302 -0
- package/dist/devtools.d.ts +56 -0
- package/dist/devtools.js +287 -0
- package/dist/dom.d.ts +43 -0
- package/dist/dom.js +315 -0
- package/dist/hmr.d.ts +24 -0
- package/dist/hmr.js +137 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -0
- package/dist/internal.d.ts +13 -0
- package/dist/internal.js +11 -0
- package/dist/reactivity.d.ts +22 -0
- package/dist/reactivity.js +151 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +79 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +38 -0
- package/dist/vite.d.ts +3 -0
- package/dist/vite.js +47 -0
- package/package.json +79 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MeekoJS contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,127 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Meeko
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="assets/brand/meeko-logo.png" alt="Carita de Meeko, una gatita de ojos azules, orejas oscuras y pelaje crema" width="200" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
Este proyecto está dedicado a mi gatita Meeko. Su carita inspira el logo que lo acompaña.
|
|
8
|
+
|
|
9
|
+
**Meeko** es un motor experimental de componentes para TypeScript. Los archivos `.meeko` combinan lógica tipada, plantilla y estilos; el compilador produce HTML para SSR y conecta la reactividad al HTML existente durante la hidratación.
|
|
10
|
+
|
|
11
|
+
El código se distribuye bajo la [licencia MIT](LICENSE). El paquete público se llama `@meekojs/meeko`; la CLI se invoca como `meeko` y el plugin de Vite se importa desde `@meekojs/meeko/vite`.
|
|
12
|
+
|
|
13
|
+
El proyecto está en etapa temprana. Ya funciona el recorrido completo de un contador renderizado en servidor, hidratado en navegador y actualizado por señales. La sintaxis y las API públicas pueden cambiar antes de la primera versión estable.
|
|
14
|
+
|
|
15
|
+
- [Arquitectura y contratos](docs/architecture.md)
|
|
16
|
+
- [API y límites de compatibilidad](docs/api.md)
|
|
17
|
+
- [Plan de desarrollo y criterios de aceptación](docs/roadmap.md)
|
|
18
|
+
- [Comparativa reproducible con Vue](docs/benchmarks.md)
|
|
19
|
+
- [DevTools para depurar componentes](docs/devtools.md)
|
|
20
|
+
- [Publicación y verificaciones](docs/releasing.md)
|
|
21
|
+
- [Historial de cambios](CHANGELOG.md)
|
|
22
|
+
- [Guía para contribuir](CONTRIBUTING.md)
|
|
23
|
+
|
|
24
|
+
## Requisitos
|
|
25
|
+
|
|
26
|
+
- Node.js 24 o superior, gestionado localmente con `mise`.
|
|
27
|
+
- `pnpm`.
|
|
28
|
+
- TypeScript **7.0.2 estable** para compilar y comprobar los tipos. `@typescript/typescript6` se usa únicamente como analizador sintáctico del formato `.meeko`, porque TypeScript 7.0 todavía no ofrece una API programática estable para lenguajes incrustados.
|
|
29
|
+
|
|
30
|
+
## Instalación en una aplicación
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
pnpm add @meekojs/meeko
|
|
34
|
+
pnpm add -D vite
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Si usas Vite, agrega el plugin a `vite.config.ts`:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { defineConfig } from "vite";
|
|
41
|
+
import { meeko } from "@meekojs/meeko/vite";
|
|
42
|
+
|
|
43
|
+
export default defineConfig({ plugins: [meeko()] });
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
El paquete incluye TypeScript 7 y la CLI `meeko`. Puedes comprobar un componente con `pnpm exec meeko check ruta/Componente.meeko`. Vite es opcional si sólo usas el compilador y SSR.
|
|
47
|
+
|
|
48
|
+
## Estructura
|
|
49
|
+
|
|
50
|
+
| Ruta | Responsabilidad |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `src/reactivity.ts` | `signal`, `computed`, `effect`, `batch` y liberación de efectos por instancia. |
|
|
53
|
+
| `src/compiler/sfc.ts` | Analiza secciones `.meeko` y conserva posiciones para diagnósticos. |
|
|
54
|
+
| `src/compiler.ts` | Genera un módulo TypeScript con SSR, hidratación y mapeos al archivo fuente. |
|
|
55
|
+
| `src/check.ts` | Comprueba tipos con TypeScript 7 y ubica diagnósticos en `.meeko`. |
|
|
56
|
+
| `src/types.ts` | Genera declaraciones para imports `.meeko` en proyectos TypeScript. |
|
|
57
|
+
| `src/server.ts` | Escapado de contenido dinámico para HTML. |
|
|
58
|
+
| `src/cli.ts` | Comandos `meeko check` y `meeko compile`. |
|
|
59
|
+
| `src/vite.ts` | Plugin de Vite para transformar `.meeko` en cliente y SSR. |
|
|
60
|
+
| `src/devtools.ts` | Instrumentación de instancias y señales sólo durante desarrollo. |
|
|
61
|
+
| `src/devtools-ui` | Panel de inspección dentro de la página. |
|
|
62
|
+
| `examples/counter` | Componente de referencia tipado. |
|
|
63
|
+
| `examples/vite` | Aplicación Vite sin generación manual de componentes. |
|
|
64
|
+
| `examples/nested` | Composición de componentes con rangos de hidratación y props tipadas. |
|
|
65
|
+
| `editors/vscode` | Extensión local con resaltado, diagnósticos y operaciones semánticas. |
|
|
66
|
+
| `benchmarks` | Comparativa repetible de Meeko y Vue en SSR y Chrome. |
|
|
67
|
+
| `tests` | Pruebas de SSR, hidratación y reactividad. |
|
|
68
|
+
| `tools/devtools-extension` | Panel local de Chrome DevTools. |
|
|
69
|
+
|
|
70
|
+
## Desarrollo
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
pnpm install
|
|
74
|
+
pnpm run verify
|
|
75
|
+
pnpm run e2e
|
|
76
|
+
pnpm run package:smoke
|
|
77
|
+
pnpm run example:serve
|
|
78
|
+
```
|
|
79
|
+
|
|
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 Vite. La última orden sirve el ejemplo en `http://127.0.0.1:4173`. El servidor envía HTML y las props iniciales como JSON escapado; el navegador hidrata el botón sin reemplazarlo.
|
|
81
|
+
|
|
82
|
+
Para probar la compilación automática con Vite:
|
|
83
|
+
|
|
84
|
+
```powershell
|
|
85
|
+
pnpm run build
|
|
86
|
+
pnpm run vite:dev
|
|
87
|
+
pnpm run vite:types
|
|
88
|
+
pnpm run vite:build
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
El plugin `@meekojs/meeko/vite` transforma imports de `.meeko` en cliente y SSR y procesa el CSS del componente. En desarrollo, HMR sustituye las instancias hidratadas sin recargar la página, conserva las señales editables que mantienen su nombre y aplica cambios de CSS. Un cambio en el runtime del motor provoca una recarga completa.
|
|
92
|
+
|
|
93
|
+
El ejemplo Vite muestra [Meeko DevTools](docs/devtools.md) durante el desarrollo: permite seleccionar cada instancia, revisar props y señales, seguir eventos y activar un punto de parada cuando cambia una señal. También hay un panel local para Chrome DevTools. La funcionalidad se incorpora al paquete a partir de `0.0.2`.
|
|
94
|
+
|
|
95
|
+
`vite:types` genera declaraciones `.d.meeko.ts` para los imports de la aplicación y comprueba el consumidor con TypeScript 7 y `allowArbitraryExtensions`. Las declaraciones del ejemplo se regeneran; no se versionan.
|
|
96
|
+
|
|
97
|
+
Para compilar otro componente:
|
|
98
|
+
|
|
99
|
+
```powershell
|
|
100
|
+
pnpm run build
|
|
101
|
+
node dist/cli.js check ruta/Componente.meeko
|
|
102
|
+
node dist/cli.js check ruta/Componente.meeko --json
|
|
103
|
+
node dist/cli.js compile ruta/Componente.meeko ruta/Componente.generated.ts
|
|
104
|
+
node dist/cli.js types ruta/Componente.meeko
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`check` devuelve errores de sintaxis y tipos con línea y columna del `.meeko`; `--json` entrega diagnósticos estructurados para editores. `compile` ejecuta la misma comprobación y no escribe el módulo si encuentra errores. `types` genera declaraciones del componente y sus imports `.meeko` para que TypeScript compruebe las props del consumidor. La verificación usa TypeScript 7 y resuelve imports desde el directorio del componente.
|
|
108
|
+
|
|
109
|
+
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
|
+
|
|
111
|
+
## Sintaxis inicial
|
|
112
|
+
|
|
113
|
+
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.
|
|
114
|
+
|
|
115
|
+
`if:when="expression"` muestra u oculta un elemento. Una lista usa `each:of="items"`, `each:as="item"` y `each:key="item.id"`; `each:index="position"` permite nombrar el índice, que se llama `index` por defecto. Las claves deben ser cadenas o números finitos y no pueden repetirse. Meeko conserva los nodos de las claves existentes al reordenar la lista. [Regions.meeko](examples/regions/Regions.meeko) muestra condiciones, listas anidadas, eventos y atributos dinámicos.
|
|
116
|
+
|
|
117
|
+
Un componente importado en el script se inserta con `<component is="Child" bind:label="expression"></component>`. Las props se comprueban con TypeScript y cada hijo se hidrata dentro de un rango de comentarios, sin un elemento envolvente. El cierre explícito es obligatorio; la sintaxis autocerrada de HTML no se admite para `<component>`. [Parent.meeko](examples/nested/Parent.meeko) muestra hijos y nietos dentro de condiciones y listas.
|
|
118
|
+
|
|
119
|
+
Los errores de sintaxis de script, plantilla y CSS, así como los errores de tipos en declaraciones y expresiones, tienen ubicación en `.meeko`. Los errores atribuibles sólo al código auxiliar generado se identifican explícitamente.
|
|
120
|
+
|
|
121
|
+
Los nombres e identificadores de código se escriben en inglés. La API pública y sus contratos llevan JSDoc en español.
|
|
122
|
+
|
|
123
|
+
La [extensión local de VS Code](editors/vscode/README.md) comparte los diagnósticos del CLI y ofrece completado, navegación, hover, renombrado y formato en script y plantilla. El VSIX se validó en un host VS Code aislado con un proyecto consumidor externo; su README explica cómo repetir la prueba y las limitaciones actuales.
|
|
124
|
+
|
|
125
|
+
## Alcance actual
|
|
126
|
+
|
|
127
|
+
Esta base todavía no implementa `:global` en CSS, un servidor de lenguaje independiente ni streaming. El compilador rechaza varias construcciones no soportadas. Estas capacidades necesitan conservar el mismo contrato de SSR e hidratación antes de añadirse.
|
package/dist/check.d.ts
ADDED
package/dist/check.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { compile } from "./compiler.js";
|
|
8
|
+
import { originalOffset } from "./compiler/mapping.js";
|
|
9
|
+
import { compileError, MeekoCompileError, locate } from "./compiler/sfc.js";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const tscPath = join(dirname(fileURLToPath(import.meta.resolve("typescript/package.json"))), "bin", "tsc");
|
|
12
|
+
async function compileGraph(source, filename) {
|
|
13
|
+
const byOriginal = new Map();
|
|
14
|
+
const byGenerated = new Map();
|
|
15
|
+
const nonce = randomUUID();
|
|
16
|
+
async function add(original, content) {
|
|
17
|
+
const previous = byOriginal.get(original);
|
|
18
|
+
if (previous)
|
|
19
|
+
return previous;
|
|
20
|
+
const compiled = compile(content, original);
|
|
21
|
+
const entry = {
|
|
22
|
+
original, source: content, code: compiled.code,
|
|
23
|
+
generated: join(dirname(original), `.${basename(original)}.__meeko_check_${nonce}.ts`),
|
|
24
|
+
mappings: compiled.mappings.map((mapping) => ({ ...mapping })),
|
|
25
|
+
};
|
|
26
|
+
byOriginal.set(original, entry);
|
|
27
|
+
byGenerated.set(entry.generated, entry);
|
|
28
|
+
const importPattern = /\b(?:from\s*|import\s*)["']([^"']+\.meeko)["']/g;
|
|
29
|
+
const edits = [];
|
|
30
|
+
for (const match of entry.code.matchAll(importPattern)) {
|
|
31
|
+
const specifier = match[1];
|
|
32
|
+
const start = match.index + match[0].indexOf(specifier);
|
|
33
|
+
if (!specifier.startsWith(".")) {
|
|
34
|
+
throw compileError(content, original, "MEEK033", "Component imports must use relative .meeko paths", originalOffset(entry.mappings, start) ?? 0);
|
|
35
|
+
}
|
|
36
|
+
const childFilename = resolve(dirname(original), specifier);
|
|
37
|
+
let childSource;
|
|
38
|
+
try {
|
|
39
|
+
childSource = await readFile(childFilename, "utf8");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw compileError(content, original, "MEEK034", `Component import not found: ${specifier}`, originalOffset(entry.mappings, start) ?? 0);
|
|
43
|
+
}
|
|
44
|
+
const child = await add(childFilename, childSource);
|
|
45
|
+
let target = relative(dirname(entry.generated), child.generated).replaceAll("\\", "/").replace(/\.ts$/, ".js");
|
|
46
|
+
if (!target.startsWith("./") && !target.startsWith("../"))
|
|
47
|
+
target = `./${target}`;
|
|
48
|
+
edits.push({ start, end: start + specifier.length, text: target });
|
|
49
|
+
}
|
|
50
|
+
for (const edit of edits.reverse()) {
|
|
51
|
+
const delta = edit.text.length - (edit.end - edit.start);
|
|
52
|
+
entry.code = entry.code.slice(0, edit.start) + edit.text + entry.code.slice(edit.end);
|
|
53
|
+
for (const mapping of entry.mappings) {
|
|
54
|
+
if (mapping.generatedStart >= edit.end)
|
|
55
|
+
mapping.generatedStart += delta;
|
|
56
|
+
if (mapping.generatedEnd >= edit.end)
|
|
57
|
+
mapping.generatedEnd += delta;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return entry;
|
|
61
|
+
}
|
|
62
|
+
const root = await add(filename, source);
|
|
63
|
+
return { root, entries: byGenerated };
|
|
64
|
+
}
|
|
65
|
+
function generatedOffset(code, line, column) {
|
|
66
|
+
const lines = code.split("\n");
|
|
67
|
+
let offset = 0;
|
|
68
|
+
for (let index = 0; index < line - 1; index++)
|
|
69
|
+
offset += (lines[index]?.length ?? 0) + 1;
|
|
70
|
+
return offset + column - 1;
|
|
71
|
+
}
|
|
72
|
+
function parseTypeScriptOutput(output) {
|
|
73
|
+
const diagnostics = [];
|
|
74
|
+
for (const line of output.split(/\r?\n/)) {
|
|
75
|
+
const match = /^(.*)\((\d+),(\d+)\): error TS(\d+): (.*)$/.exec(line);
|
|
76
|
+
if (match)
|
|
77
|
+
diagnostics.push({ file: match[1] ?? "", line: Number(match[2]), column: Number(match[3]), code: `TS${match[4]}`, message: match[5] ?? "" });
|
|
78
|
+
else if (line.trim() && diagnostics.length > 0)
|
|
79
|
+
diagnostics[diagnostics.length - 1].message += `\n${line}`;
|
|
80
|
+
}
|
|
81
|
+
return diagnostics;
|
|
82
|
+
}
|
|
83
|
+
/** Comprueba un componente con TypeScript 7 y devuelve errores ubicados en el .meeko original. */
|
|
84
|
+
export async function checkSource(source, filename) {
|
|
85
|
+
const absolute = resolve(filename);
|
|
86
|
+
let graph;
|
|
87
|
+
try {
|
|
88
|
+
graph = await compileGraph(source, absolute);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error instanceof MeekoCompileError)
|
|
92
|
+
return [error.diagnostic];
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
const written = [];
|
|
96
|
+
let output = "";
|
|
97
|
+
try {
|
|
98
|
+
for (const entry of graph.entries.values()) {
|
|
99
|
+
await writeFile(entry.generated, entry.code, { encoding: "utf8", flag: "wx" });
|
|
100
|
+
written.push(entry.generated);
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const result = await execFileAsync(process.execPath, [
|
|
104
|
+
tscPath, "--ignoreConfig", "--noEmit", "--pretty", "false", "--strict", "--skipLibCheck",
|
|
105
|
+
"--noUncheckedIndexedAccess", "--exactOptionalPropertyTypes", "--verbatimModuleSyntax",
|
|
106
|
+
"--module", "nodenext", "--moduleResolution", "nodenext", "--target", "es2022",
|
|
107
|
+
"--lib", "es2022,dom", graph.root.generated,
|
|
108
|
+
], { cwd: dirname(absolute), maxBuffer: 8 * 1024 * 1024 });
|
|
109
|
+
output = result.stdout + result.stderr;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
const failure = error;
|
|
113
|
+
output = `${failure.stdout ?? ""}${failure.stderr ?? ""}`;
|
|
114
|
+
if (!output.trim())
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await Promise.all(written.map((path) => unlink(path)));
|
|
120
|
+
}
|
|
121
|
+
const typed = parseTypeScriptOutput(output);
|
|
122
|
+
if (typed.length === 0 && output.trim())
|
|
123
|
+
throw new Error(`TypeScript 7 check failed: ${output.trim()}`);
|
|
124
|
+
return typed.map((diagnostic) => {
|
|
125
|
+
const entry = graph.entries.get(resolve(dirname(absolute), diagnostic.file));
|
|
126
|
+
const offset = entry ? originalOffset(entry.mappings, generatedOffset(entry.code, diagnostic.line, diagnostic.column)) : undefined;
|
|
127
|
+
const message = entry && offset !== undefined ? diagnostic.message : `Generated TypeScript: ${diagnostic.message}`;
|
|
128
|
+
return { code: diagnostic.code, message, filename: entry?.original ?? absolute, location: locate(entry?.source ?? source, offset ?? 0) };
|
|
129
|
+
});
|
|
130
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { checkSource } from "./check.js";
|
|
4
|
+
import { compile } from "./compiler.js";
|
|
5
|
+
import { generateTypes } from "./types.js";
|
|
6
|
+
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");
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
try {
|
|
13
|
+
const source = await readFile(input, "utf8");
|
|
14
|
+
const diagnostics = await checkSource(source, input);
|
|
15
|
+
if (command === "check" && output === "--json") {
|
|
16
|
+
process.stdout.write(`${JSON.stringify(diagnostics)}\n`);
|
|
17
|
+
}
|
|
18
|
+
else if (diagnostics.length > 0) {
|
|
19
|
+
for (const diagnostic of diagnostics) {
|
|
20
|
+
const { line, column } = diagnostic.location;
|
|
21
|
+
process.stderr.write(`${diagnostic.filename}:${line}:${column} ${diagnostic.code}: ${diagnostic.message}\n`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else if (command === "compile") {
|
|
25
|
+
const result = compile(source, input);
|
|
26
|
+
await writeFile(output, result.code, "utf8");
|
|
27
|
+
process.stdout.write(`${output}\n`);
|
|
28
|
+
}
|
|
29
|
+
else if (command === "types") {
|
|
30
|
+
for (const generated of await generateTypes(source, input))
|
|
31
|
+
process.stdout.write(`${generated}\n`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
process.stdout.write(`${input}: OK\n`);
|
|
35
|
+
}
|
|
36
|
+
if (diagnostics.length > 0)
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Segmento que relaciona una porción del módulo generado con el archivo .meeko. */
|
|
2
|
+
export interface SourceMapping {
|
|
3
|
+
generatedStart: number;
|
|
4
|
+
generatedEnd: number;
|
|
5
|
+
originalStart: number;
|
|
6
|
+
originalLength: number;
|
|
7
|
+
}
|
|
8
|
+
/** Marca código conservando la ubicación de su expresión o declaración original. */
|
|
9
|
+
export declare function markSource(code: string, originalStart: number, originalLength?: number): string;
|
|
10
|
+
/** Retira marcas internas y produce el mapa de segmentos del módulo emitido. */
|
|
11
|
+
export declare function extractSourceMappings(markedCode: string): {
|
|
12
|
+
code: string;
|
|
13
|
+
mappings: SourceMapping[];
|
|
14
|
+
};
|
|
15
|
+
/** Convierte un desplazamiento generado en uno del archivo .meeko si existe origen preciso. */
|
|
16
|
+
export declare function originalOffset(mappings: readonly SourceMapping[], generatedOffset: number): number | undefined;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const markerToken = randomUUID().replaceAll("-", "");
|
|
2
|
+
const marker = new RegExp(`/\\*__MEEK_SOURCE_${markerToken}_(START|END):(\\d+):(\\d+)__\\*/`, "g");
|
|
3
|
+
/** Marca código conservando la ubicación de su expresión o declaración original. */
|
|
4
|
+
export function markSource(code, originalStart, originalLength = code.length) {
|
|
5
|
+
return `/*__MEEK_SOURCE_${markerToken}_START:${originalStart}:${originalLength}__*/${code}/*__MEEK_SOURCE_${markerToken}_END:${originalStart}:${originalLength}__*/`;
|
|
6
|
+
}
|
|
7
|
+
/** Retira marcas internas y produce el mapa de segmentos del módulo emitido. */
|
|
8
|
+
export function extractSourceMappings(markedCode) {
|
|
9
|
+
const mappings = [];
|
|
10
|
+
const open = [];
|
|
11
|
+
const parts = [];
|
|
12
|
+
let generatedOffset = 0;
|
|
13
|
+
let cursor = 0;
|
|
14
|
+
for (const match of markedCode.matchAll(marker)) {
|
|
15
|
+
const index = match.index;
|
|
16
|
+
const chunk = markedCode.slice(cursor, index);
|
|
17
|
+
parts.push(chunk);
|
|
18
|
+
generatedOffset += chunk.length;
|
|
19
|
+
const originalStart = Number(match[2]);
|
|
20
|
+
const originalLength = Number(match[3]);
|
|
21
|
+
if (match[1] === "START") {
|
|
22
|
+
open.push({ generatedStart: generatedOffset, originalStart, originalLength });
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const span = open.pop();
|
|
26
|
+
if (!span || span.originalStart !== originalStart || span.originalLength !== originalLength) {
|
|
27
|
+
throw new Error("Invalid Meeko source mapping markers");
|
|
28
|
+
}
|
|
29
|
+
mappings.push({ ...span, generatedEnd: generatedOffset });
|
|
30
|
+
}
|
|
31
|
+
cursor = index + match[0].length;
|
|
32
|
+
}
|
|
33
|
+
const tail = markedCode.slice(cursor);
|
|
34
|
+
parts.push(tail);
|
|
35
|
+
if (open.length !== 0)
|
|
36
|
+
throw new Error("Unclosed Meeko source mapping marker");
|
|
37
|
+
return { code: parts.join(""), mappings: mappings.sort((a, b) => a.generatedStart - b.generatedStart) };
|
|
38
|
+
}
|
|
39
|
+
/** Convierte un desplazamiento generado en uno del archivo .meeko si existe origen preciso. */
|
|
40
|
+
export function originalOffset(mappings, generatedOffset) {
|
|
41
|
+
const span = mappings
|
|
42
|
+
.filter((entry) => generatedOffset >= entry.generatedStart && generatedOffset < entry.generatedEnd)
|
|
43
|
+
.sort((a, b) => (a.generatedEnd - a.generatedStart) - (b.generatedEnd - b.generatedStart))[0];
|
|
44
|
+
if (!span)
|
|
45
|
+
return undefined;
|
|
46
|
+
return span.originalStart + Math.min(generatedOffset - span.generatedStart, Math.max(0, span.originalLength - 1));
|
|
47
|
+
}
|
|
48
|
+
import { randomUUID } from "node:crypto";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Ubicación de un diagnóstico en el archivo .meeko original. */
|
|
2
|
+
export interface SourceLocation {
|
|
3
|
+
offset: number;
|
|
4
|
+
line: number;
|
|
5
|
+
column: number;
|
|
6
|
+
}
|
|
7
|
+
/** Error de compilación con ubicación y código estable para herramientas. */
|
|
8
|
+
export interface CompileDiagnostic {
|
|
9
|
+
code: string;
|
|
10
|
+
message: string;
|
|
11
|
+
filename: string;
|
|
12
|
+
location: SourceLocation;
|
|
13
|
+
}
|
|
14
|
+
/** Error que conserva el diagnóstico estructurado para CLI y editor. */
|
|
15
|
+
export declare class MeekoCompileError extends Error {
|
|
16
|
+
readonly diagnostic: CompileDiagnostic;
|
|
17
|
+
constructor(diagnostic: CompileDiagnostic);
|
|
18
|
+
}
|
|
19
|
+
/** Una sección SFC con posiciones conservadas en el archivo fuente. */
|
|
20
|
+
export interface SfcSection {
|
|
21
|
+
name: "script" | "template" | "style";
|
|
22
|
+
start: number;
|
|
23
|
+
end: number;
|
|
24
|
+
content: string;
|
|
25
|
+
contentStart: number;
|
|
26
|
+
contentEnd: number;
|
|
27
|
+
attributes: ReadonlyArray<{
|
|
28
|
+
name: string;
|
|
29
|
+
value: string;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
/** Estructura validada de las secciones de un componente. */
|
|
33
|
+
export interface SfcDocument {
|
|
34
|
+
script: SfcSection;
|
|
35
|
+
template: SfcSection;
|
|
36
|
+
style?: SfcSection;
|
|
37
|
+
}
|
|
38
|
+
/** Convierte un desplazamiento UTF-16 en línea y columna, empezando en uno. */
|
|
39
|
+
export declare function locate(source: string, offset: number): SourceLocation;
|
|
40
|
+
/** Crea un error de compilación asociado al archivo original. */
|
|
41
|
+
export declare function compileError(source: string, filename: string, code: string, message: string, offset: number): MeekoCompileError;
|
|
42
|
+
/** Analiza y valida las secciones superiores de un archivo .meeko. */
|
|
43
|
+
export declare function parseSfc(source: string, filename?: string): SfcDocument;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { parseFragment } from "parse5";
|
|
2
|
+
/** Error que conserva el diagnóstico estructurado para CLI y editor. */
|
|
3
|
+
export class MeekoCompileError extends Error {
|
|
4
|
+
diagnostic;
|
|
5
|
+
constructor(diagnostic) {
|
|
6
|
+
super(`${diagnostic.filename}:${diagnostic.location.line}:${diagnostic.location.column} ${diagnostic.code}: ${diagnostic.message}`);
|
|
7
|
+
this.name = "MeekoCompileError";
|
|
8
|
+
this.diagnostic = diagnostic;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Convierte un desplazamiento UTF-16 en línea y columna, empezando en uno. */
|
|
12
|
+
export function locate(source, offset) {
|
|
13
|
+
const before = source.slice(0, offset);
|
|
14
|
+
const lines = before.split(/\r\n|\r|\n/);
|
|
15
|
+
return { offset, line: lines.length, column: (lines.at(-1)?.length ?? 0) + 1 };
|
|
16
|
+
}
|
|
17
|
+
/** Crea un error de compilación asociado al archivo original. */
|
|
18
|
+
export function compileError(source, filename, code, message, offset) {
|
|
19
|
+
return new MeekoCompileError({ code, message, filename, location: locate(source, offset) });
|
|
20
|
+
}
|
|
21
|
+
/** Analiza y valida las secciones superiores de un archivo .meeko. */
|
|
22
|
+
export function parseSfc(source, filename = "anonymous.meeko") {
|
|
23
|
+
const parseErrors = [];
|
|
24
|
+
const root = parseFragment(source, {
|
|
25
|
+
sourceCodeLocationInfo: true,
|
|
26
|
+
onParseError(error) {
|
|
27
|
+
parseErrors.push({ code: error.code, startOffset: error.startOffset });
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
if (parseErrors[0]) {
|
|
31
|
+
const error = parseErrors[0];
|
|
32
|
+
throw compileError(source, filename, "MEEK001", `Invalid component HTML: ${error.code}`, error.startOffset);
|
|
33
|
+
}
|
|
34
|
+
const sections = new Map();
|
|
35
|
+
for (const node of root.childNodes ?? []) {
|
|
36
|
+
if (node.nodeName === "#comment" || (node.nodeName === "#text" && !(node.value ?? "").trim()))
|
|
37
|
+
continue;
|
|
38
|
+
const tag = node.tagName;
|
|
39
|
+
const location = node.sourceCodeLocation;
|
|
40
|
+
if (tag !== "script" && tag !== "template" && tag !== "style") {
|
|
41
|
+
throw compileError(source, filename, "MEEK002", "Only script, template and style sections are allowed at the top level", location?.startOffset ?? 0);
|
|
42
|
+
}
|
|
43
|
+
if (!location?.startTag || !location.endTag) {
|
|
44
|
+
throw compileError(source, filename, "MEEK003", `Section <${tag}> must have a closing tag`, location?.startOffset ?? 0);
|
|
45
|
+
}
|
|
46
|
+
if (sections.has(tag)) {
|
|
47
|
+
throw compileError(source, filename, "MEEK004", `Duplicate <${tag}> section`, location.startOffset);
|
|
48
|
+
}
|
|
49
|
+
sections.set(tag, {
|
|
50
|
+
name: tag,
|
|
51
|
+
start: location.startOffset,
|
|
52
|
+
end: location.endOffset,
|
|
53
|
+
content: source.slice(location.startTag.endOffset, location.endTag.startOffset),
|
|
54
|
+
contentStart: location.startTag.endOffset,
|
|
55
|
+
contentEnd: location.endTag.startOffset,
|
|
56
|
+
attributes: node.attrs ?? [],
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const script = sections.get("script");
|
|
60
|
+
const template = sections.get("template");
|
|
61
|
+
if (!script)
|
|
62
|
+
throw compileError(source, filename, "MEEK005", "A <script lang=\"ts\"> section is required", 0);
|
|
63
|
+
if (!script.attributes.some((attribute) => attribute.name === "lang" && attribute.value === "ts")) {
|
|
64
|
+
throw compileError(source, filename, "MEEK006", "The script section must use lang=\"ts\"", script.contentStart);
|
|
65
|
+
}
|
|
66
|
+
if (!template)
|
|
67
|
+
throw compileError(source, filename, "MEEK007", "A <template> section is required", source.length);
|
|
68
|
+
const style = sections.get("style");
|
|
69
|
+
return style ? { script, template, style } : { script, template };
|
|
70
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SfcSection } from "./sfc.js";
|
|
2
|
+
/** Atributo normalizado de una etiqueta de plantilla. */
|
|
3
|
+
export interface TemplateAttribute {
|
|
4
|
+
name: string;
|
|
5
|
+
value: string;
|
|
6
|
+
start: number;
|
|
7
|
+
end: number;
|
|
8
|
+
}
|
|
9
|
+
/** Nodo propio de plantilla con límites absolutos en el archivo .meeko. */
|
|
10
|
+
export interface TemplateNode {
|
|
11
|
+
nodeName: string;
|
|
12
|
+
tagName?: string;
|
|
13
|
+
value?: string;
|
|
14
|
+
attrs?: TemplateAttribute[];
|
|
15
|
+
childNodes?: TemplateNode[];
|
|
16
|
+
content?: {
|
|
17
|
+
childNodes: TemplateNode[];
|
|
18
|
+
};
|
|
19
|
+
start: number;
|
|
20
|
+
end: number;
|
|
21
|
+
sourceCodeLocation?: {
|
|
22
|
+
startOffset: number;
|
|
23
|
+
endOffset: number;
|
|
24
|
+
attrs?: Record<string, {
|
|
25
|
+
startOffset: number;
|
|
26
|
+
endOffset: number;
|
|
27
|
+
}>;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Analiza HTML y entrega un AST independiente del parser con posiciones originales. */
|
|
31
|
+
export declare function parseTemplate(source: string, template: SfcSection, filename: string): TemplateNode;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { parseFragment } from "parse5";
|
|
2
|
+
import { compileError } from "./sfc.js";
|
|
3
|
+
function normalize(node, template) {
|
|
4
|
+
const location = node.sourceCodeLocation;
|
|
5
|
+
const attrs = node.attrs?.map((attribute) => {
|
|
6
|
+
const span = location?.attrs?.[attribute.name];
|
|
7
|
+
return {
|
|
8
|
+
name: attribute.name,
|
|
9
|
+
value: attribute.value,
|
|
10
|
+
start: template.contentStart + (span?.startOffset ?? location?.startOffset ?? 0),
|
|
11
|
+
end: template.contentStart + (span?.endOffset ?? location?.endOffset ?? 0),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
const result = {
|
|
15
|
+
nodeName: node.nodeName,
|
|
16
|
+
start: template.contentStart + (location?.startOffset ?? 0),
|
|
17
|
+
end: template.contentStart + (location?.endOffset ?? template.content.length),
|
|
18
|
+
};
|
|
19
|
+
if (node.tagName !== undefined)
|
|
20
|
+
result.tagName = node.tagName;
|
|
21
|
+
if (node.value !== undefined)
|
|
22
|
+
result.value = node.value;
|
|
23
|
+
if (attrs !== undefined)
|
|
24
|
+
result.attrs = attrs;
|
|
25
|
+
if (node.childNodes !== undefined)
|
|
26
|
+
result.childNodes = node.childNodes.map((child) => normalize(child, template));
|
|
27
|
+
if (node.content !== undefined)
|
|
28
|
+
result.content = { childNodes: (node.content.childNodes ?? []).map((child) => normalize(child, template)) };
|
|
29
|
+
if (location !== undefined)
|
|
30
|
+
result.sourceCodeLocation = location;
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
/** Analiza HTML y entrega un AST independiente del parser con posiciones originales. */
|
|
34
|
+
export function parseTemplate(source, template, filename) {
|
|
35
|
+
const errors = [];
|
|
36
|
+
const parsed = parseFragment(template.content, {
|
|
37
|
+
sourceCodeLocationInfo: true,
|
|
38
|
+
onParseError(error) { errors.push({ code: error.code, startOffset: error.startOffset }); },
|
|
39
|
+
});
|
|
40
|
+
const error = errors[0];
|
|
41
|
+
if (error)
|
|
42
|
+
throw compileError(source, filename, "MEEK008", `Invalid template HTML: ${error.code}`, template.contentStart + error.startOffset);
|
|
43
|
+
return normalize(parsed, template);
|
|
44
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SourceMapping } from "./compiler/mapping.js";
|
|
2
|
+
/** Resultado del compilador de un componente .meeko. */
|
|
3
|
+
export interface CompileResult {
|
|
4
|
+
code: string;
|
|
5
|
+
css: string;
|
|
6
|
+
mappings: readonly SourceMapping[];
|
|
7
|
+
}
|
|
8
|
+
/** Opciones de generación; HMR se incluye solo en módulos de desarrollo. */
|
|
9
|
+
export interface CompileOptions {
|
|
10
|
+
hot?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Compila una parte tipada de .meeko para SSR e hidratación del HTML existente. */
|
|
13
|
+
export declare function compile(source: string, filename?: string, options?: CompileOptions): CompileResult;
|