@meekojs/meeko 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Contrato del runtime generado por el compilador Meeko.
3
+ *
4
+ * Esta ruta es interna y puede cambiar junto con el compilador. Las aplicaciones
5
+ * deben importar las API documentadas desde la raíz del paquete.
6
+ */
7
+ export { createRoot } from "./reactivity.js";
8
+ export { attributeValue, escapeHtml, renderAttribute, serializeJson } from "./server.js";
9
+ export { bindAttribute, bindComponent, bindEvent, bindFor, bindIf, bindText, keyToken, renderComponent, renderFor, renderIf, withStateSnapshot } from "./dom.js";
10
+ export type { ComponentModule, ComponentRelease, DomScope } from "./dom.js";
11
+ export { enableHotModule, registerHotInstance, registerHotScope, replaceHotModule, replaceHotStyle, restoreSignals, snapshotSignals } from "./hmr.js";
12
+ export type { HotComponent, HotState } from "./hmr.js";
13
+ export { registerDevtoolsInstance } from "./devtools.js";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Contrato del runtime generado por el compilador Meeko.
3
+ *
4
+ * Esta ruta es interna y puede cambiar junto con el compilador. Las aplicaciones
5
+ * deben importar las API documentadas desde la raíz del paquete.
6
+ */
7
+ export { createRoot } from "./reactivity.js";
8
+ export { attributeValue, escapeHtml, renderAttribute, serializeJson } from "./server.js";
9
+ export { bindAttribute, bindComponent, bindEvent, bindFor, bindIf, bindText, keyToken, renderComponent, renderFor, renderIf, withStateSnapshot } from "./dom.js";
10
+ export { enableHotModule, registerHotInstance, registerHotScope, replaceHotModule, replaceHotStyle, restoreSignals, snapshotSignals } from "./hmr.js";
11
+ export { registerDevtoolsInstance } from "./devtools.js";
@@ -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. */
@@ -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.delete(observer);
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
- for (const observer of subscribers)
55
- pending.add(observer);
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,8 +1,8 @@
1
1
  {
2
2
  "name": "@meekojs/meeko",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "packageManager": "pnpm@12.14.0",
5
- "description": "Typed .meeko component engine with SSR, hydration, signals, and Vite integration.",
5
+ "description": "Typed .meeko component engine with SSR, hydration, signals, Vite, and DevTools.",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
8
  "files": ["dist/"],
@@ -10,7 +10,7 @@
10
10
  "repository": { "type": "git", "url": "git+https://github.com/meekojs/meeko.git" },
11
11
  "homepage": "https://github.com/meekojs/meeko#readme",
12
12
  "bugs": { "url": "https://github.com/meekojs/meeko/issues" },
13
- "keywords": ["meeko", "components", "ssr", "hydration", "reactivity", "vite", "typescript"],
13
+ "keywords": ["meeko", "components", "ssr", "hydration", "reactivity", "vite", "typescript", "devtools"],
14
14
  "publishConfig": { "access": "public" },
15
15
  "exports": {
16
16
  ".": {
@@ -20,6 +20,14 @@
20
20
  "./vite": {
21
21
  "types": "./dist/vite.d.ts",
22
22
  "default": "./dist/vite.js"
23
+ },
24
+ "./internal": {
25
+ "types": "./dist/internal.d.ts",
26
+ "default": "./dist/internal.js"
27
+ },
28
+ "./devtools-ui": {
29
+ "types": "./dist/devtools-ui/index.d.ts",
30
+ "default": "./dist/devtools-ui/index.js"
23
31
  }
24
32
  },
25
33
  "bin": {
@@ -37,11 +45,18 @@
37
45
  "vite:dev": "vite examples/vite --config examples/vite/vite.config.ts",
38
46
  "vite:types": "pnpm run build && node dist/cli.js types examples/counter/Counter.meeko && node dist/cli.js types examples/regions/Regions.meeko && node dist/cli.js types examples/vite/Hot.meeko && node dist/cli.js types examples/nested/Parent.meeko && tsc -p examples/vite/tsconfig.json --noEmit",
39
47
  "vite:build": "vite build examples/vite --config examples/vite/vite.config.ts",
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",
40
53
  "e2e": "pnpm run example:bundle && pnpm run vite:build && tsx --test tests/browser.e2e.ts",
41
- "benchmark:check": "pnpm run example:compile && tsc -p tsconfig.benchmark.json --noEmit",
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",
42
55
  "benchmark": "pnpm run build && pnpm run benchmark:check && node --expose-gc --import tsx benchmarks/run.ts",
56
+ "benchmark:scenarios": "pnpm run build && pnpm run benchmark:check && node --expose-gc --import tsx benchmarks/scenarios.ts",
43
57
  "package:smoke": "pwsh -NoProfile -File tests/package-consumer.ps1",
44
- "verify": "pnpm run check && pnpm run test && pnpm run example:check && tsc -p tsconfig.test.json --noEmit && pnpm run vite:types && pnpm run benchmark:check && pnpm run vite:build"
58
+ "devtools:check": "node --test tools/devtools-extension/tests/*.test.js && tsc -p tools/devtools-extension/tsconfig.json --noEmit",
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"
45
60
  },
46
61
  "dependencies": {
47
62
  "@typescript/typescript6": "6.0.2",