@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/README.md +1024 -0
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/nix/async.d.ts +69 -0
- package/dist/lib/nix/component.d.ts +13 -0
- package/dist/lib/nix/context.d.ts +61 -0
- package/dist/lib/nix/index.d.ts +14 -0
- package/dist/lib/nix/lifecycle.d.ts +70 -0
- package/dist/lib/nix/reactivity.d.ts +116 -0
- package/dist/lib/nix/router.d.ts +115 -0
- package/dist/lib/nix/store.d.ts +40 -0
- package/dist/lib/nix/template.d.ts +62 -0
- package/dist/lib/nix-js.cjs +22 -0
- package/dist/lib/nix-js.js +764 -0
- package/package.json +62 -0
- package/src/index.ts +61 -0
- package/src/nix/async.ts +164 -0
- package/src/nix/component.ts +76 -0
- package/src/nix/context.ts +142 -0
- package/src/nix/index.ts +14 -0
- package/src/nix/lifecycle.ts +112 -0
- package/src/nix/reactivity.ts +308 -0
- package/src/nix/router.ts +393 -0
- package/src/nix/store.ts +117 -0
- package/src/nix/template.ts +793 -0
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";
|
package/src/nix/async.ts
ADDED
|
@@ -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
|
+
}
|
package/src/nix/index.ts
ADDED
|
@@ -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
|
+
}
|