@deijose/nix-js 0.1.0

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/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@deijose/nix-js",
3
+ "version": "0.1.0",
4
+ "description": "A lightweight, fully reactive micro-framework — no virtual DOM, no compiler, just signals and tagged templates.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "keywords": [
8
+ "reactive",
9
+ "signals",
10
+ "micro-framework",
11
+ "ui",
12
+ "dom",
13
+ "typescript",
14
+ "no-vdom",
15
+ "frontend"
16
+ ],
17
+
18
+ "exports": {
19
+ ".": {
20
+ "import": {
21
+ "types": "./dist/lib/index.d.ts",
22
+ "default": "./dist/lib/nix-js.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/lib/index.d.ts",
26
+ "default": "./dist/lib/nix-js.cjs"
27
+ }
28
+ }
29
+ },
30
+ "main": "./dist/lib/nix-js.cjs",
31
+ "module": "./dist/lib/nix-js.js",
32
+ "types": "./dist/lib/index.d.ts",
33
+
34
+ "files": [
35
+ "dist/lib",
36
+ "!dist/lib/*.map",
37
+ "src/nix",
38
+ "src/index.ts"
39
+ ],
40
+
41
+ "sideEffects": false,
42
+
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ },
46
+
47
+ "scripts": {
48
+ "dev": "vite",
49
+ "build": "tsc && vite build",
50
+ "preview": "vite preview",
51
+ "build:lib": "vite build --config vite.lib.config.ts && tsc --project tsconfig.lib.json",
52
+ "typecheck": "tsc --noEmit"
53
+ },
54
+
55
+ "devDependencies": {
56
+ "typescript": "~5.9.3",
57
+ "vite": "^8.0.0-beta.13"
58
+ },
59
+ "overrides": {
60
+ "vite": "^8.0.0-beta.13"
61
+ }
62
+ }
package/src/index.ts ADDED
@@ -0,0 +1,61 @@
1
+ // ❄️ Nix.js — Public Library Entry Point
2
+ //
3
+ // This file is the single entry point for the compiled library.
4
+ // Import from here when using Nix.js as an installed package:
5
+ //
6
+ // import { signal, html, NixComponent, mount } from "nix-js";
7
+
8
+ // ── Values ────────────────────────────────────────────────────────────────────
9
+ export {
10
+ // Reactivity
11
+ Signal,
12
+ signal,
13
+ effect,
14
+ computed,
15
+ batch,
16
+ watch,
17
+ untrack,
18
+ nextTick,
19
+ // Templates
20
+ html,
21
+ repeat,
22
+ ref,
23
+ // Components
24
+ mount,
25
+ NixComponent,
26
+ // Store
27
+ createStore,
28
+ // Router
29
+ createRouter,
30
+ RouterView,
31
+ Link,
32
+ useRouter,
33
+ // Async / Lazy
34
+ suspend,
35
+ lazy,
36
+ // Dependency Injection
37
+ provide,
38
+ inject,
39
+ createInjectionKey,
40
+ } from "./nix";
41
+
42
+ // ── Types ─────────────────────────────────────────────────────────────────────
43
+ export type {
44
+ // Reactivity
45
+ WatchOptions,
46
+ // Templates
47
+ NixTemplate,
48
+ NixMountHandle,
49
+ KeyedList,
50
+ NixRef,
51
+ // Store
52
+ Store,
53
+ StoreSignals,
54
+ // Router
55
+ Router,
56
+ RouteRecord,
57
+ // Async
58
+ SuspenseOptions,
59
+ // Dependency Injection
60
+ InjectionKey,
61
+ } from "./nix";
@@ -0,0 +1,164 @@
1
+ // src/nix/async.ts — Fase 8: Lazy loading + Suspense
2
+
3
+ import { signal } from "./reactivity";
4
+ import { NixComponent } from "./lifecycle";
5
+ import type { NixTemplate } from "./template";
6
+ import { html } from "./template";
7
+
8
+ // ── Tipos públicos ────────────────────────────────────────────────────────────
9
+
10
+ type AsyncState<T> =
11
+ | { status: "pending" }
12
+ | { status: "resolved"; data: T }
13
+ | { status: "error"; error: unknown };
14
+
15
+ export interface SuspenseOptions {
16
+ /**
17
+ * Template a mostrar mientras la promesa está pendiente.
18
+ * Por defecto: spinner de puntos animados.
19
+ */
20
+ fallback?: NixTemplate;
21
+ /**
22
+ * Factory que recibe el error y devuelve el template de error.
23
+ * Por defecto: mensaje de error en rojo.
24
+ */
25
+ errorFallback?: (err: unknown) => NixTemplate;
26
+ /**
27
+ * Si `true`, mantiene el fallback visible mientras `asyncFn` vuelve a
28
+ * ejecutarse tras un cambio reactivo. Si `false` (por defecto), durante
29
+ * las recargas se sigue mostrando el contenido anterior hasta que la nueva
30
+ * promesa se resuelva.
31
+ */
32
+ resetOnRefresh?: boolean;
33
+ }
34
+
35
+ // ── suspend() ─────────────────────────────────────────────────────────────────
36
+
37
+ /**
38
+ * Ejecuta una función async y renderiza según su estado (pending / resolved /
39
+ * error). Equivale al patrón <Suspense> de otros frameworks.
40
+ *
41
+ * @param asyncFn Función que devuelve una promesa con los datos.
42
+ * @param renderFn Recibe los datos resueltos y devuelve el template/componente.
43
+ * @param options `fallback`, `errorFallback`, `resetOnRefresh`.
44
+ *
45
+ * @example
46
+ * // Uso simple con fallback por defecto
47
+ * const userView = suspend(
48
+ * () => fetchUser(userId.value),
49
+ * user => html`<div>${user.name}</div>`
50
+ * );
51
+ *
52
+ * @example
53
+ * // Con fallback personalizado y manejo de error
54
+ * suspend(
55
+ * () => api.getItems(),
56
+ * items => html`<ul>${items.map(i => html`<li>${i}</li>`)}</ul>`,
57
+ * {
58
+ * fallback: html`<span>Cargando items…</span>`,
59
+ * errorFallback: err => html`<p style="color:red">Error: ${String(err)}</p>`,
60
+ * }
61
+ * )
62
+ */
63
+ export function suspend<T>(
64
+ asyncFn: () => Promise<T>,
65
+ renderFn: (data: T) => NixTemplate | NixComponent,
66
+ options: SuspenseOptions = {}
67
+ ): NixComponent {
68
+ const {
69
+ fallback,
70
+ errorFallback,
71
+ resetOnRefresh = false,
72
+ } = options;
73
+
74
+ const defaultFallback = fallback ?? html`
75
+ <span style="color:#52525b;font-size:13px;display:inline-flex;align-items:center;gap:6px">
76
+ <span class="nix-spinner" style="
77
+ display:inline-block;width:14px;height:14px;border-radius:50%;
78
+ border:2px solid #38bdf840;border-top-color:#38bdf8;
79
+ animation:nix-spin .7s linear infinite
80
+ "></span>
81
+ Cargando…
82
+ </span>
83
+ <style>@keyframes nix-spin{to{transform:rotate(360deg)}}</style>
84
+ `;
85
+
86
+ const defaultErrorFallback = errorFallback ?? ((err: unknown) => html`
87
+ <span style="color:#f87171;font-size:13px">
88
+ ⚠ ${err instanceof Error ? err.message : String(err)}
89
+ </span>
90
+ `);
91
+
92
+ class SuspendComponent extends NixComponent {
93
+ private _state = signal<AsyncState<T>>({ status: "pending" });
94
+
95
+ onMount(): void {
96
+ this._run();
97
+ }
98
+
99
+ private _run(): void {
100
+ if (resetOnRefresh || this._state.value.status === "pending") {
101
+ this._state.value = { status: "pending" };
102
+ }
103
+ asyncFn().then(
104
+ (data) => { this._state.value = { status: "resolved", data }; },
105
+ (err) => { this._state.value = { status: "error", error: err }; }
106
+ );
107
+ }
108
+
109
+ render(): NixTemplate {
110
+ return html`<div class="nix-suspense" style="display:contents">${() => {
111
+ const s = this._state.value;
112
+ if (s.status === "pending") return defaultFallback;
113
+ if (s.status === "error") return defaultErrorFallback(s.error);
114
+ return renderFn(s.data);
115
+ }}</div>`;
116
+ }
117
+ }
118
+
119
+ return new SuspendComponent();
120
+ }
121
+
122
+ // ── lazy() ────────────────────────────────────────────────────────────────────
123
+
124
+ /**
125
+ * Envuelve un import dinámico para lazy loading de componentes de ruta.
126
+ * El módulo se carga una sola vez (cacheado) y mientras tanto muestra el
127
+ * fallback. Compatible directamente con el campo `component` de `RouteRecord`.
128
+ *
129
+ * El módulo importado debe exportar el componente como **export default**.
130
+ *
131
+ * @param importFn Función que hace el import dinámico.
132
+ * @param fallback Template opcional mientras se descarga el chunk.
133
+ *
134
+ * @example
135
+ * createRouter([
136
+ * { path: "/", component: lazy(() => import("./pages/Home")) },
137
+ * { path: "/about", component: lazy(() => import("./pages/About")) },
138
+ * { path: "/admin", component: lazy(() => import("./pages/Admin"),
139
+ * html`<p>Cargando panel…</p>`) },
140
+ * ])
141
+ */
142
+ export function lazy(
143
+ importFn: () => Promise<{ default: new () => NixComponent }>,
144
+ fallback?: NixTemplate
145
+ ): () => NixComponent {
146
+ // Cache del constructor — null mientras no se haya cargado
147
+ let Cached: (new () => NixComponent) | null = null;
148
+
149
+ return (): NixComponent => {
150
+ // Si ya está cargado, instanciar directamente (sin Suspense)
151
+ if (Cached) return new Cached();
152
+
153
+ // Primera vez: cargar el chunk y cachear
154
+ return suspend(
155
+ async () => {
156
+ const mod = await importFn();
157
+ Cached = mod.default;
158
+ return Cached;
159
+ },
160
+ (Comp) => new Comp(),
161
+ { fallback }
162
+ );
163
+ };
164
+ }
@@ -0,0 +1,76 @@
1
+ // ═══════════════════════════════════════════════
2
+ // Nix.js ❄️ — Componentes (Fase 3)
3
+ // ═══════════════════════════════════════════════
4
+ //
5
+ // Un componente en Nix.js es una función simple:
6
+ //
7
+ // function MyComponent(props) {
8
+ // const count = signal(0);
9
+ // return html`<button @click=${() => count.update(n => n+1)}>
10
+ // ${() => count.value}
11
+ // </button>`;
12
+ // }
13
+ //
14
+ // No hay clases, no hay decoradores, no hay registro.
15
+ // Las actualizaciones ocurren via signals — la función
16
+ // se ejecuta UNA sola vez.
17
+ //
18
+ // mount(template, container) — monta la app raíz en el DOM.
19
+
20
+ import type { NixTemplate, NixMountHandle } from "./template";
21
+ import { isNixComponent, type NixComponent } from "./lifecycle";
22
+ import { _pushComponentContext, _popComponentContext } from "./context";
23
+
24
+ /**
25
+ * Monta un NixTemplate o NixComponent en el DOM.
26
+ *
27
+ * mount(App(), "#app"); // NixTemplate (función componente)
28
+ * mount(new Timer(), "#app"); // NixComponent (clase con lifecycle)
29
+ *
30
+ * @param component NixTemplate (resultado de html``) o instancia de NixComponent.
31
+ * @param container Selector CSS o HTMLElement donde se insertará.
32
+ * @returns { unmount() } para limpiar effects y remover el DOM.
33
+ */
34
+ export function mount(
35
+ component: NixTemplate | NixComponent,
36
+ container: Element | string
37
+ ): NixMountHandle {
38
+ if (isNixComponent(component)) {
39
+ const el =
40
+ typeof container === "string"
41
+ ? (document.querySelector(container) as Element)
42
+ : container;
43
+ if (!el) {
44
+ throw new Error(`[Nix] mount: contenedor no encontrado: ${container}`);
45
+ }
46
+
47
+ _pushComponentContext();
48
+ let cleanup: () => void;
49
+ try {
50
+ try { component.onInit?.(); } catch (e) { if (component.onError) component.onError(e); else throw e; }
51
+ cleanup = component.render()._render(el, null);
52
+ } finally {
53
+ _popComponentContext();
54
+ }
55
+ let mountCleanup: (() => void) | undefined;
56
+
57
+ try {
58
+ const ret = component.onMount?.();
59
+ if (typeof ret === "function") mountCleanup = ret;
60
+ } catch (e) {
61
+ if (component.onError) component.onError(e);
62
+ else throw e;
63
+ }
64
+
65
+ return {
66
+ unmount() {
67
+ try { component.onUnmount?.(); } catch { /* ignore */ }
68
+ try { mountCleanup?.(); } catch { /* ignore */ }
69
+ cleanup();
70
+ },
71
+ };
72
+ }
73
+
74
+ // NixTemplate: delegar al método .mount() interno
75
+ return (component as NixTemplate).mount(container);
76
+ }
@@ -0,0 +1,142 @@
1
+ // ═══════════════════════════════════════════════
2
+ // Nix.js ❄️ — Provide / Inject (Fase 13)
3
+ // ═══════════════════════════════════════════════
4
+ //
5
+ // Inyección de dependencias sin prop drilling:
6
+ //
7
+ // Proveedor:
8
+ // class ThemeProvider extends NixComponent {
9
+ // theme = signal("dark");
10
+ // onInit() { provide(THEME_KEY, this.theme); }
11
+ // render() { return html`...`; }
12
+ // }
13
+ //
14
+ // Consumidor (cualquier nivel de profundidad):
15
+ // class Button extends NixComponent {
16
+ // theme = inject(THEME_KEY); // Signal<string> | undefined
17
+ // render() {
18
+ // return html`<button class=${() => this.theme?.value}>OK</button>`;
19
+ // }
20
+ // }
21
+ //
22
+ // Cómo funciona:
23
+ // Cada componente tiene su propio mapa de valores provistos.
24
+ // Al renderizar, el motor apila los mapas padre → hijo.
25
+ // inject() busca desde el tope de la pila hacia la raíz.
26
+
27
+ // ─── Tipos públicos ───────────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Clave tipada para provide/inject.
31
+ * El parámetro genérico T garantiza coherencia entre proveedor y consumidor.
32
+ *
33
+ * @example
34
+ * const THEME_KEY = createInjectionKey<Signal<string>>("theme");
35
+ */
36
+ export type InjectionKey<T> = symbol & { readonly __nixType?: T };
37
+
38
+ /**
39
+ * Crea una InjectionKey única y tipada.
40
+ * Cada llamada produce un símbolo distinto, evitando colisiones.
41
+ *
42
+ * @param description Nombre descriptivo (aparece en Symbol.toString())
43
+ */
44
+ export function createInjectionKey<T>(description?: string): InjectionKey<T> {
45
+ return Symbol(description) as InjectionKey<T>;
46
+ }
47
+
48
+ // ─── Stack interno ────────────────────────────────────────────────────────────
49
+
50
+ /** Pila de mapas provide, uno por componente activo en el árbol de render. */
51
+ const _stack: Map<unknown, unknown>[] = [];
52
+
53
+ /** @internal — devuelve copia del stack (para capturar en closures de efectos). */
54
+ export function _captureContextSnapshot(): Map<unknown, unknown>[] {
55
+ return [..._stack];
56
+ }
57
+
58
+ /** @internal — push de un contexto vacío para un nuevo componente (render estático). */
59
+ export function _pushComponentContext(): void {
60
+ _stack.push(new Map());
61
+ }
62
+
63
+ /** @internal — pop del contexto del componente actual (render estático). */
64
+ export function _popComponentContext(): void {
65
+ _stack.pop();
66
+ }
67
+
68
+ /**
69
+ * @internal — ejecuta `fn` con `parentSnapshot` como ancestros y un nuevo
70
+ * contexto vacío en el tope, luego restaura el stack previo.
71
+ *
72
+ * Usado por efectos reactivos que pueden re-ejecutarse fuera del árbol de
73
+ * rendering original (p.ej. NixComponents dentro de `() => new MyComp()`).
74
+ */
75
+ export function _withComponentContext<T>(
76
+ parentSnapshot: Map<unknown, unknown>[],
77
+ fn: () => T
78
+ ): T {
79
+ const saved = _stack.splice(0);
80
+ parentSnapshot.forEach(m => _stack.push(m));
81
+ _stack.push(new Map()); // contexto propio, vacío al principio
82
+ try {
83
+ return fn();
84
+ } finally {
85
+ _stack.splice(0);
86
+ saved.forEach(m => _stack.push(m));
87
+ }
88
+ }
89
+
90
+ // ─── API pública ──────────────────────────────────────────────────────────────
91
+
92
+ /**
93
+ * Registra un valor para que los componentes descendientes puedan obtenerlo
94
+ * con `inject()`.
95
+ *
96
+ * Debe llamarse en `onInit()` o en el constructor de un `NixComponent`.
97
+ * Si se llama fuera del contexto de render de un componente, lanza un error.
98
+ *
99
+ * @example
100
+ * class ThemeProvider extends NixComponent {
101
+ * theme = signal("dark");
102
+ * onInit() { provide(THEME_KEY, this.theme); } // ← aquí
103
+ * render() { return html`${new ThemedButton()}`; }
104
+ * }
105
+ */
106
+ export function provide<T>(
107
+ key: InjectionKey<T> | string | symbol,
108
+ value: T
109
+ ): void {
110
+ const top = _stack[_stack.length - 1];
111
+ if (!top) {
112
+ throw new Error(
113
+ "[Nix] provide() debe llamarse dentro de onInit() de un NixComponent."
114
+ );
115
+ }
116
+ top.set(key, value);
117
+ }
118
+
119
+ /**
120
+ * Obtiene un valor provisto por un componente ancestro.
121
+ * Busca de hijo a padre; retorna `undefined` si la clave no fue provista.
122
+ *
123
+ * Úsalo como propiedad de clase o dentro de `onInit()`.
124
+ *
125
+ * @example
126
+ * class ThemedButton extends NixComponent {
127
+ * theme = inject(THEME_KEY); // Signal<string> | undefined
128
+ * render() {
129
+ * return html`<button class=${() => this.theme?.value ?? "light"}>OK</button>`;
130
+ * }
131
+ * }
132
+ */
133
+ export function inject<T>(
134
+ key: InjectionKey<T> | string | symbol
135
+ ): T | undefined {
136
+ for (let i = _stack.length - 1; i >= 0; i--) {
137
+ if (_stack[i].has(key)) {
138
+ return _stack[i].get(key) as T;
139
+ }
140
+ }
141
+ return undefined;
142
+ }
@@ -0,0 +1,14 @@
1
+ export { Signal, signal, effect, computed, batch, watch, untrack, nextTick } from "./reactivity";
2
+ export type { WatchOptions } from "./reactivity";
3
+ export { html, repeat, ref } from "./template";
4
+ export type { NixTemplate, NixMountHandle, KeyedList, NixRef } from "./template";
5
+ export { mount } from "./component";
6
+ export { NixComponent } from "./lifecycle";
7
+ export { createStore } from "./store";
8
+ export type { Store, StoreSignals } from "./store";
9
+ export { createRouter, RouterView, Link, useRouter } from "./router";
10
+ export type { Router, RouteRecord } from "./router";
11
+ export { suspend, lazy } from "./async";
12
+ export type { SuspenseOptions } from "./async";
13
+ export { provide, inject, createInjectionKey } from "./context";
14
+ export type { InjectionKey } from "./context";
@@ -0,0 +1,112 @@
1
+ // ═══════════════════════════════════════════════
2
+ // Nix.js ❄️ — Lifecycle Hooks (Fase 4)
3
+ // ═══════════════════════════════════════════════
4
+ //
5
+ // Orden de ejecución garantizado:
6
+ //
7
+ // new MyComponent() ← constructor (opcional)
8
+ // ↓
9
+ // onInit() ← sin DOM, síncrono, antes de render()
10
+ // ↓
11
+ // render() ← retorna NixTemplate
12
+ // ↓
13
+ // [DOM insertado]
14
+ // ↓
15
+ // onMount() ← con DOM; puede retornar cleanup
16
+ // ↓
17
+ // ...signals actualizan efectos internos...
18
+ // ↓
19
+ // onUnmount() ← DOM aún presente
20
+ // cleanup de onMount()
21
+ // ↓
22
+ // [DOM removido]
23
+ //
24
+ // onError(err) → captura errores de onInit y onMount.
25
+
26
+ import type { NixTemplate } from "./template";
27
+
28
+ // ─── NixComponent ─────────────────────────────────────────────────────────────
29
+
30
+ /**
31
+ * Clase base para componentes con lifecycle.
32
+ *
33
+ * Implementa `render()` y haz override de los hooks que necesites.
34
+ *
35
+ * @example
36
+ * class Timer extends NixComponent {
37
+ * ticks = signal(0);
38
+ *
39
+ * onMount() {
40
+ * const id = setInterval(() => this.ticks.update(n => n + 1), 1000);
41
+ * return () => clearInterval(id); // cleanup automático al desmontar
42
+ * }
43
+ *
44
+ * render() {
45
+ * return html`<span>${() => this.ticks.value}s</span>`;
46
+ * }
47
+ * }
48
+ *
49
+ * mount(new Timer(), "#app");
50
+ */
51
+ export abstract class NixComponent {
52
+ /** @internal – marca que identifica instancias NixComponent en el engine. */
53
+ readonly __isNixComponent = true as const;
54
+
55
+ /**
56
+ * Debe implementarse: retorna el template del componente.
57
+ * Se llama UNA sola vez al montar — las actualizaciones ocurren por signals.
58
+ */
59
+ abstract render(): NixTemplate;
60
+
61
+ /**
62
+ * Llamado ANTES de `render()` — sin DOM todavía.
63
+ * Útil para inicializar estado complejo derivado de props u otras
64
+ * operaciones síncronas que render() necesita.
65
+ *
66
+ * Los errores aquí son capturados por `onError` si está implementado.
67
+ *
68
+ * @example
69
+ * onInit() {
70
+ * this.derived = computed(() => this.base.value * 2);
71
+ * }
72
+ */
73
+ onInit?(): void;
74
+
75
+ /**
76
+ * Llamado DESPUÉS de que el componente se inserta en el DOM.
77
+ * Si retorna una función, se usa como cleanup automático al desmontar.
78
+ *
79
+ * @example
80
+ * onMount() {
81
+ * const id = setInterval(() => this.count.update(n => n + 1), 1000);
82
+ * return () => clearInterval(id);
83
+ * }
84
+ */
85
+ onMount?(): (() => void) | void;
86
+
87
+ /**
88
+ * Llamado ANTES de remover el componente del DOM.
89
+ * Se ejecuta siempre al desmontar, incluso si no se definió onMount.
90
+ */
91
+ onUnmount?(): void;
92
+
93
+ /**
94
+ * Captura errores lanzados dentro de `onMount`.
95
+ * Si se implementa, el error queda absorbido y el componente permanece montado.
96
+ * Si no se implementa, el error se re-lanza.
97
+ */
98
+ onError?(err: unknown): void;
99
+ }
100
+
101
+ // ─── Helper de tipo ───────────────────────────────────────────────────────────
102
+
103
+ /**
104
+ * @internal – Verifica si un valor es una instancia de NixComponent.
105
+ */
106
+ export function isNixComponent(v: unknown): v is NixComponent {
107
+ return (
108
+ v != null &&
109
+ typeof v === "object" &&
110
+ (v as Record<string, unknown>).__isNixComponent === true
111
+ );
112
+ }