lumix-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 +156 -0
- package/bin/lumix.js +63 -0
- package/dist/bind.d.ts +13 -0
- package/dist/bind.js +103 -0
- package/dist/cli/build.d.ts +1 -0
- package/dist/cli/build.js +69 -0
- package/dist/cli/dev.d.ts +1 -0
- package/dist/cli/dev.js +69 -0
- package/dist/cli/init.d.ts +5 -0
- package/dist/cli/init.js +199 -0
- package/dist/cli/loader.d.ts +9 -0
- package/dist/cli/loader.js +34 -0
- package/dist/cli/preview.d.ts +1 -0
- package/dist/cli/preview.js +39 -0
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.js +4 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.js +21 -0
- package/dist/control.d.ts +17 -0
- package/dist/control.js +47 -0
- package/dist/dom.d.ts +8 -0
- package/dist/dom.js +136 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/lifecycle.d.ts +16 -0
- package/dist/lifecycle.js +45 -0
- package/dist/signals.d.ts +21 -0
- package/dist/signals.js +144 -0
- package/dist/store.d.ts +37 -0
- package/dist/store.js +147 -0
- package/package.json +47 -0
- package/templates/blank/lumix.config.mjs +10 -0
- package/templates/blank/main.js +5 -0
- package/templates/blank/package.json +15 -0
- package/templates/blank/src/App.lumix +28 -0
- package/templates/blank-ts/lumix-env.d.ts +4 -0
- package/templates/blank-ts/lumix.config.mts +10 -0
- package/templates/blank-ts/main.ts +5 -0
- package/templates/blank-ts/package.json +17 -0
- package/templates/blank-ts/src/App.lumix +28 -0
- package/templates/blank-ts/tsconfig.json +16 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LuminConfig } from "../config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Find the config file path in the given directory.
|
|
4
|
+
*/
|
|
5
|
+
export declare function findConfigPath(cwd: string): string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Load and return the LuminJS config from the project directory.
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadConfig(cwd?: string): Promise<LuminConfig>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { bundleRequire } from "bundle-require";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
const CONFIG_FILES = [
|
|
5
|
+
"lumix.config.mjs",
|
|
6
|
+
"lumix.config.js",
|
|
7
|
+
"lumix.config.ts",
|
|
8
|
+
"lumix.config.mts",
|
|
9
|
+
];
|
|
10
|
+
/**
|
|
11
|
+
* Find the config file path in the given directory.
|
|
12
|
+
*/
|
|
13
|
+
export function findConfigPath(cwd) {
|
|
14
|
+
for (const file of CONFIG_FILES) {
|
|
15
|
+
const fullPath = path.resolve(cwd, file);
|
|
16
|
+
if (fs.existsSync(fullPath)) {
|
|
17
|
+
return fullPath;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Load and return the LuminJS config from the project directory.
|
|
24
|
+
*/
|
|
25
|
+
export async function loadConfig(cwd = process.cwd()) {
|
|
26
|
+
const configPath = findConfigPath(cwd);
|
|
27
|
+
if (!configPath) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
const { mod } = await bundleRequire({
|
|
31
|
+
filepath: configPath,
|
|
32
|
+
});
|
|
33
|
+
return mod.default || mod;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function preview(): Promise<void>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { preview as vitePreview } from "vite";
|
|
2
|
+
import lumin from "../../../vite-plugin-lumix/dist/index.js";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { loadConfig } from "./loader.js";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { __dirname } from "./utils.js";
|
|
7
|
+
export async function preview() {
|
|
8
|
+
const cwd = process.cwd();
|
|
9
|
+
const config = await loadConfig(cwd);
|
|
10
|
+
console.log("");
|
|
11
|
+
console.log(` ${pc.bold(pc.cyan("⚡ LumixJS"))} ${pc.dim("v0.1.0")}`);
|
|
12
|
+
console.log(pc.dim(" Previewing production build...\n"));
|
|
13
|
+
try {
|
|
14
|
+
const server = await vitePreview({
|
|
15
|
+
root: cwd,
|
|
16
|
+
base: "/",
|
|
17
|
+
plugins: [lumin(config), ...(config.vite?.plugins || [])],
|
|
18
|
+
preview: {
|
|
19
|
+
port: 4173,
|
|
20
|
+
...config.vite?.preview,
|
|
21
|
+
},
|
|
22
|
+
resolve: {
|
|
23
|
+
alias: {
|
|
24
|
+
"lumix-js": path.resolve(__dirname, "../index.js"),
|
|
25
|
+
...config.vite?.resolve?.alias,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
...config.vite,
|
|
29
|
+
});
|
|
30
|
+
const address = server.resolvedUrls?.local?.[0] || `http://localhost:4173/`;
|
|
31
|
+
console.log(` ${pc.green("➜")} ${pc.bold("Preview:")} ${pc.cyan(address)}`);
|
|
32
|
+
console.log(` ${pc.green("➜")} ${pc.dim("press")} ${pc.bold("Ctrl+C")} ${pc.dim("to stop")}\n`);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
console.error(pc.red(pc.bold("\n Preview failed!")));
|
|
36
|
+
console.error(pc.red(` ${e.message}\n`));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface LuminConfig {
|
|
2
|
+
/**
|
|
3
|
+
* Application title, injected into the <title> tag.
|
|
4
|
+
*/
|
|
5
|
+
title?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Path to the favicon file (e.g. "./favicon.ico" or "/icon.png").
|
|
8
|
+
*/
|
|
9
|
+
favicon?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Head metadata (meta tags, link tags, etc.)
|
|
12
|
+
*/
|
|
13
|
+
head?: {
|
|
14
|
+
meta?: Array<Record<string, string>>;
|
|
15
|
+
link?: Array<Record<string, string>>;
|
|
16
|
+
script?: Array<Record<string, string>>;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Root element ID to hydrate into. Defaults to 'app'.
|
|
20
|
+
*/
|
|
21
|
+
rootId?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Vite specific configuration overrides.
|
|
24
|
+
*/
|
|
25
|
+
vite?: any;
|
|
26
|
+
/**
|
|
27
|
+
* Source directory. Defaults to the root of the project.
|
|
28
|
+
*/
|
|
29
|
+
srcDir?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Enable or disable semantic type checking during build/dev.
|
|
32
|
+
* Defaults to true in TS projects, can be disabled for JS projects.
|
|
33
|
+
*/
|
|
34
|
+
checkTypes?: boolean;
|
|
35
|
+
/** Allow extra keys without TS errors, but we'll warn at runtime */
|
|
36
|
+
[key: string]: any;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Helper function to provide types for the LuminJS config.
|
|
40
|
+
* Validates the config and warns about unknown fields.
|
|
41
|
+
*/
|
|
42
|
+
export declare function defineConfig(config: LuminConfig): LuminConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const VALID_KEYS = new Set([
|
|
2
|
+
"title",
|
|
3
|
+
"favicon",
|
|
4
|
+
"head",
|
|
5
|
+
"rootId",
|
|
6
|
+
"vite",
|
|
7
|
+
"srcDir",
|
|
8
|
+
"checkTypes",
|
|
9
|
+
]);
|
|
10
|
+
/**
|
|
11
|
+
* Helper function to provide types for the LuminJS config.
|
|
12
|
+
* Validates the config and warns about unknown fields.
|
|
13
|
+
*/
|
|
14
|
+
export function defineConfig(config) {
|
|
15
|
+
const unknown = Object.keys(config).filter((k) => !VALID_KEYS.has(k));
|
|
16
|
+
if (unknown.length > 0) {
|
|
17
|
+
console.warn(`\x1b[33m[lumin] Warning: Unknown config field(s): ${unknown.map((k) => `"${k}"`).join(", ")}.\x1b[0m`);
|
|
18
|
+
console.warn(`\x1b[33m[lumin] Valid fields are: ${[...VALID_KEYS].join(", ")}.\x1b[0m`);
|
|
19
|
+
}
|
|
20
|
+
return config;
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type ControlBranch = {
|
|
2
|
+
cond?: () => any;
|
|
3
|
+
body: () => any | any[];
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Reactive If/Else helper
|
|
7
|
+
* @param condition Initial condition for the first branch
|
|
8
|
+
* @param branches List of branches with optional conditions and bodies
|
|
9
|
+
*/
|
|
10
|
+
export declare function __if(condition: () => any, branches: ControlBranch[]): () => any;
|
|
11
|
+
/**
|
|
12
|
+
* Reactive For loop helper
|
|
13
|
+
* @param list Closure returning the array to iterate over
|
|
14
|
+
* @param render Closure to render each item
|
|
15
|
+
* @param keyFn Optional closure to extract a unique key from each item
|
|
16
|
+
*/
|
|
17
|
+
export declare function __for<T>(list: () => T[], render: (item: T, index: number) => any | any[], keyFn?: (item: T) => any): () => any[];
|
package/dist/control.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive If/Else helper
|
|
3
|
+
* @param condition Initial condition for the first branch
|
|
4
|
+
* @param branches List of branches with optional conditions and bodies
|
|
5
|
+
*/
|
|
6
|
+
export function __if(condition, branches) {
|
|
7
|
+
return () => {
|
|
8
|
+
if (condition())
|
|
9
|
+
return branches[0].body();
|
|
10
|
+
for (let i = 1; i < branches.length; i++) {
|
|
11
|
+
const b = branches[i];
|
|
12
|
+
if (!b.cond || b.cond())
|
|
13
|
+
return b.body();
|
|
14
|
+
}
|
|
15
|
+
return [];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Reactive For loop helper
|
|
20
|
+
* @param list Closure returning the array to iterate over
|
|
21
|
+
* @param render Closure to render each item
|
|
22
|
+
* @param keyFn Optional closure to extract a unique key from each item
|
|
23
|
+
*/
|
|
24
|
+
export function __for(list, render, keyFn) {
|
|
25
|
+
const cache = new Map();
|
|
26
|
+
return () => {
|
|
27
|
+
const items = list() || [];
|
|
28
|
+
if (!keyFn)
|
|
29
|
+
return items.map((item, index) => render(item, index));
|
|
30
|
+
const newNodes = items.map((item, index) => {
|
|
31
|
+
const key = keyFn(item);
|
|
32
|
+
if (cache.has(key))
|
|
33
|
+
return cache.get(key);
|
|
34
|
+
const rendered = render(item, index);
|
|
35
|
+
cache.set(key, rendered);
|
|
36
|
+
return rendered;
|
|
37
|
+
});
|
|
38
|
+
// Cleanup cache for items no longer present
|
|
39
|
+
const itemKeys = new Set(items.map(keyFn));
|
|
40
|
+
for (const key of cache.keys()) {
|
|
41
|
+
if (!itemKeys.has(key)) {
|
|
42
|
+
cache.delete(key);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return newNodes;
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/dom.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Signal } from "./signals.js";
|
|
2
|
+
export type AttrValue = string | number | boolean | (() => any) | Signal<any>;
|
|
3
|
+
export interface Props {
|
|
4
|
+
[key: string]: AttrValue;
|
|
5
|
+
}
|
|
6
|
+
export declare function h(tag: string | ((props: Props, ...children: any[]) => any), props: Props | null, ...children: any[]): any;
|
|
7
|
+
export declare function Fragment(_props: any, ...children: any[]): any[];
|
|
8
|
+
export declare function hydrate(root: HTMLElement, component: (props?: any) => HTMLElement, props?: any): void;
|
package/dist/dom.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { effect } from "./signals.js";
|
|
2
|
+
import { bind } from "./bind.js";
|
|
3
|
+
import { withHooks, runHooks } from "./lifecycle.js";
|
|
4
|
+
export function h(tag, props, ...children) {
|
|
5
|
+
if (typeof tag === "function") {
|
|
6
|
+
const { result, mount, destroy } = withHooks(() => tag(props || {}, ...children));
|
|
7
|
+
// If it's a component that returns a single element, we can attach hooks to it
|
|
8
|
+
if (result instanceof HTMLElement) {
|
|
9
|
+
if (mount.length > 0) {
|
|
10
|
+
// Simple trick: use MutationObserver or just run on next tick if added
|
|
11
|
+
setTimeout(() => runHooks(mount), 0);
|
|
12
|
+
}
|
|
13
|
+
if (destroy.length > 0) {
|
|
14
|
+
result._luminDestroy = destroy;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
const el = document.createElement(tag);
|
|
20
|
+
if (props) {
|
|
21
|
+
for (const [key, value] of Object.entries(props)) {
|
|
22
|
+
// ── bind: directive ──────────────────────────────
|
|
23
|
+
if (key.startsWith("bind:")) {
|
|
24
|
+
const property = key.slice(5); // "bind:value" → "value"
|
|
25
|
+
if (typeof value === "function" && "_peek" in value) {
|
|
26
|
+
// It's a Signal — set up two-way binding
|
|
27
|
+
bind(el, property, value);
|
|
28
|
+
}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
// ── Event handlers ───────────────────────────────
|
|
32
|
+
if (key.startsWith("on") && typeof value === "function") {
|
|
33
|
+
const eventName = key.slice(2).toLowerCase();
|
|
34
|
+
el.addEventListener(eventName, value);
|
|
35
|
+
}
|
|
36
|
+
// ── Reactive attribute (Signal or closure) ───────
|
|
37
|
+
else if (typeof value === "function") {
|
|
38
|
+
effect(() => {
|
|
39
|
+
const v = value();
|
|
40
|
+
if (key === "value" ||
|
|
41
|
+
key === "checked" ||
|
|
42
|
+
key === "disabled" ||
|
|
43
|
+
key === "selected") {
|
|
44
|
+
// DOM properties not attributes
|
|
45
|
+
el[key] = v;
|
|
46
|
+
}
|
|
47
|
+
else if (typeof v === "boolean") {
|
|
48
|
+
if (v)
|
|
49
|
+
el.setAttribute(key, "");
|
|
50
|
+
else
|
|
51
|
+
el.removeAttribute(key);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
el.setAttribute(key, String(v));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// ── Static attribute ─────────────────────────────
|
|
59
|
+
else {
|
|
60
|
+
el.setAttribute(key, String(value));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const child of children.flat(Infinity)) {
|
|
65
|
+
if (child === null || child === undefined)
|
|
66
|
+
continue;
|
|
67
|
+
if (typeof child === "function") {
|
|
68
|
+
// Reactive block (Signal, closure, or control flow helper)
|
|
69
|
+
const startMarker = document.createComment("cf-start");
|
|
70
|
+
const endMarker = document.createComment("cf-end");
|
|
71
|
+
el.appendChild(startMarker);
|
|
72
|
+
el.appendChild(endMarker);
|
|
73
|
+
let prevNodes = [];
|
|
74
|
+
effect(() => {
|
|
75
|
+
let v = child();
|
|
76
|
+
// Normalize to array of nodes
|
|
77
|
+
let newNodes = [];
|
|
78
|
+
const items = Array.isArray(v) ? v.flat(Infinity) : [v];
|
|
79
|
+
for (let item of items) {
|
|
80
|
+
if (item === null || item === undefined)
|
|
81
|
+
continue;
|
|
82
|
+
while (typeof item === "function") {
|
|
83
|
+
item = item();
|
|
84
|
+
}
|
|
85
|
+
if (item instanceof Node) {
|
|
86
|
+
newNodes.push(item);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
newNodes.push(document.createTextNode(String(item)));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// --- Improved Reconciliation ---
|
|
93
|
+
const newNodeSet = new Set(newNodes);
|
|
94
|
+
// 1. Remove and unmount only nodes that are NOT in the new set
|
|
95
|
+
for (const node of prevNodes) {
|
|
96
|
+
if (!newNodeSet.has(node)) {
|
|
97
|
+
unmount(node);
|
|
98
|
+
if (node.parentNode === el) {
|
|
99
|
+
el.removeChild(node);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// 2. Insert or move nodes
|
|
104
|
+
// insertBefore naturally handles moves (reaches same state if already correctly positioned)
|
|
105
|
+
for (const node of newNodes) {
|
|
106
|
+
el.insertBefore(node, endMarker);
|
|
107
|
+
}
|
|
108
|
+
prevNodes = newNodes;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else if (child instanceof Node) {
|
|
112
|
+
el.appendChild(child);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
el.appendChild(document.createTextNode(String(child)));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return el;
|
|
119
|
+
}
|
|
120
|
+
export function Fragment(_props, ...children) {
|
|
121
|
+
return children.flat(Infinity);
|
|
122
|
+
}
|
|
123
|
+
function unmount(node) {
|
|
124
|
+
if (node instanceof HTMLElement) {
|
|
125
|
+
const hooks = node._luminDestroy;
|
|
126
|
+
if (hooks)
|
|
127
|
+
runHooks(hooks);
|
|
128
|
+
}
|
|
129
|
+
node.childNodes.forEach(unmount);
|
|
130
|
+
}
|
|
131
|
+
export function hydrate(root, component, props) {
|
|
132
|
+
while (root.firstChild)
|
|
133
|
+
root.removeChild(root.firstChild);
|
|
134
|
+
const el = h(component, props || {});
|
|
135
|
+
root.appendChild(el);
|
|
136
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type Hook = () => void;
|
|
2
|
+
export declare function onMount(fn: Hook): void;
|
|
3
|
+
export declare function onDestroy(fn: Hook): void;
|
|
4
|
+
/**
|
|
5
|
+
* Internal helper to catch hooks during component execution
|
|
6
|
+
*/
|
|
7
|
+
export declare function withHooks<T>(fn: () => T): {
|
|
8
|
+
result: T;
|
|
9
|
+
mount: Hook[];
|
|
10
|
+
destroy: Hook[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Internal helper to run a set of hooks
|
|
14
|
+
*/
|
|
15
|
+
export declare function runHooks(hooks: Hook[]): void;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
let currentHooks = null;
|
|
2
|
+
export function onMount(fn) {
|
|
3
|
+
if (currentHooks) {
|
|
4
|
+
currentHooks.mount.push(fn);
|
|
5
|
+
}
|
|
6
|
+
else {
|
|
7
|
+
console.warn("onMount must be called during component initialization.");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function onDestroy(fn) {
|
|
11
|
+
if (currentHooks) {
|
|
12
|
+
currentHooks.destroy.push(fn);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
console.warn("onDestroy must be called during component initialization.");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Internal helper to catch hooks during component execution
|
|
20
|
+
*/
|
|
21
|
+
export function withHooks(fn) {
|
|
22
|
+
const previous = currentHooks;
|
|
23
|
+
const hooks = { mount: [], destroy: [] };
|
|
24
|
+
currentHooks = hooks;
|
|
25
|
+
try {
|
|
26
|
+
const result = fn();
|
|
27
|
+
return { result, ...hooks };
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
currentHooks = previous;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Internal helper to run a set of hooks
|
|
35
|
+
*/
|
|
36
|
+
export function runHooks(hooks) {
|
|
37
|
+
for (const hook of hooks) {
|
|
38
|
+
try {
|
|
39
|
+
hook();
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
console.error("Error in hook:", e);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type Subscriber<T> = (value: T) => void;
|
|
2
|
+
export type Unsubscribe = () => void;
|
|
3
|
+
export type CleanupFn = () => void;
|
|
4
|
+
export interface Signal<T> {
|
|
5
|
+
(): T;
|
|
6
|
+
(next: T): T;
|
|
7
|
+
readonly value: T;
|
|
8
|
+
_subscribe(fn: Subscriber<T>): Unsubscribe;
|
|
9
|
+
_peek(): T;
|
|
10
|
+
}
|
|
11
|
+
export interface ReadonlySignal<T> {
|
|
12
|
+
(): T;
|
|
13
|
+
readonly value: T;
|
|
14
|
+
_subscribe(fn: Subscriber<T>): Unsubscribe;
|
|
15
|
+
_peek(): T;
|
|
16
|
+
}
|
|
17
|
+
export declare function signal<T>(initialValue: T): Signal<T>;
|
|
18
|
+
export declare function effect(fn: () => void | CleanupFn): Unsubscribe;
|
|
19
|
+
export declare function computed<T>(fn: () => T): ReadonlySignal<T>;
|
|
20
|
+
export declare function batch<T>(fn: () => T): T;
|
|
21
|
+
export declare function untrack<T>(fn: () => T): T;
|
package/dist/signals.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
let activeEffect = null;
|
|
2
|
+
let batchDepth = 0;
|
|
3
|
+
const pendingEffects = new Set();
|
|
4
|
+
function runEffect(node) {
|
|
5
|
+
// Cleanup previous run
|
|
6
|
+
if (node.cleanup)
|
|
7
|
+
node.cleanup();
|
|
8
|
+
// Remove this effect from all its previous dependency sets
|
|
9
|
+
for (const depSet of node.deps) {
|
|
10
|
+
depSet.delete(node);
|
|
11
|
+
}
|
|
12
|
+
node.deps.clear();
|
|
13
|
+
// Run with tracking
|
|
14
|
+
const prev = activeEffect;
|
|
15
|
+
activeEffect = node;
|
|
16
|
+
try {
|
|
17
|
+
node.cleanup = node.execute();
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
activeEffect = prev;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function notify(subscribers) {
|
|
24
|
+
for (const node of [...subscribers]) {
|
|
25
|
+
if (batchDepth > 0) {
|
|
26
|
+
pendingEffects.add(node);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
runEffect(node);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ─── signal() ──────────────────────────────────────────────
|
|
34
|
+
export function signal(initialValue) {
|
|
35
|
+
let value = initialValue;
|
|
36
|
+
const subscribers = new Set();
|
|
37
|
+
function readWrite(next) {
|
|
38
|
+
if (arguments.length === 0) {
|
|
39
|
+
// Track dependency
|
|
40
|
+
if (activeEffect) {
|
|
41
|
+
subscribers.add(activeEffect);
|
|
42
|
+
activeEffect.deps.add(subscribers);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const newVal = next;
|
|
48
|
+
if (!Object.is(value, newVal)) {
|
|
49
|
+
value = newVal;
|
|
50
|
+
notify(subscribers);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
Object.defineProperty(readWrite, "value", {
|
|
56
|
+
get() {
|
|
57
|
+
return readWrite();
|
|
58
|
+
},
|
|
59
|
+
set(v) {
|
|
60
|
+
readWrite(v);
|
|
61
|
+
},
|
|
62
|
+
enumerable: true,
|
|
63
|
+
});
|
|
64
|
+
readWrite._subscribe = (fn) => {
|
|
65
|
+
const node = {
|
|
66
|
+
execute: () => fn(value),
|
|
67
|
+
deps: new Set(),
|
|
68
|
+
cleanup: undefined,
|
|
69
|
+
};
|
|
70
|
+
subscribers.add(node);
|
|
71
|
+
return () => {
|
|
72
|
+
subscribers.delete(node);
|
|
73
|
+
for (const depSet of node.deps)
|
|
74
|
+
depSet.delete(node);
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
readWrite._peek = () => value;
|
|
78
|
+
return readWrite;
|
|
79
|
+
}
|
|
80
|
+
// ─── effect() ──────────────────────────────────────────────
|
|
81
|
+
export function effect(fn) {
|
|
82
|
+
const node = {
|
|
83
|
+
execute: fn,
|
|
84
|
+
deps: new Set(),
|
|
85
|
+
cleanup: undefined,
|
|
86
|
+
};
|
|
87
|
+
runEffect(node);
|
|
88
|
+
return () => {
|
|
89
|
+
if (node.cleanup)
|
|
90
|
+
node.cleanup();
|
|
91
|
+
for (const depSet of node.deps) {
|
|
92
|
+
depSet.delete(node);
|
|
93
|
+
}
|
|
94
|
+
node.deps.clear();
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// ─── computed() ────────────────────────────────────────────
|
|
98
|
+
export function computed(fn) {
|
|
99
|
+
const inner = signal(undefined);
|
|
100
|
+
effect(() => {
|
|
101
|
+
inner(fn());
|
|
102
|
+
});
|
|
103
|
+
// Return read-only wrapper
|
|
104
|
+
function readOnly() {
|
|
105
|
+
return inner();
|
|
106
|
+
}
|
|
107
|
+
Object.defineProperty(readOnly, "value", {
|
|
108
|
+
get() {
|
|
109
|
+
return inner();
|
|
110
|
+
},
|
|
111
|
+
enumerable: true,
|
|
112
|
+
});
|
|
113
|
+
readOnly._subscribe = inner._subscribe;
|
|
114
|
+
readOnly._peek = inner._peek;
|
|
115
|
+
return readOnly;
|
|
116
|
+
}
|
|
117
|
+
// ─── batch() ───────────────────────────────────────────────
|
|
118
|
+
export function batch(fn) {
|
|
119
|
+
batchDepth++;
|
|
120
|
+
try {
|
|
121
|
+
return fn();
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
batchDepth--;
|
|
125
|
+
if (batchDepth === 0) {
|
|
126
|
+
const effects = [...pendingEffects];
|
|
127
|
+
pendingEffects.clear();
|
|
128
|
+
for (const node of effects) {
|
|
129
|
+
runEffect(node);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// ─── untrack() ─────────────────────────────────────────────
|
|
135
|
+
export function untrack(fn) {
|
|
136
|
+
const prev = activeEffect;
|
|
137
|
+
activeEffect = null;
|
|
138
|
+
try {
|
|
139
|
+
return fn();
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
activeEffect = prev;
|
|
143
|
+
}
|
|
144
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Signal, Unsubscribe } from "./signals.js";
|
|
2
|
+
export type StoreListener<T> = (state: T, key?: string) => void;
|
|
3
|
+
export interface Store<T extends Record<string, any>> {
|
|
4
|
+
/** Read a key reactively */
|
|
5
|
+
get<K extends keyof T>(key: K): T[K];
|
|
6
|
+
/** Write a key and notify subscribers */
|
|
7
|
+
set<K extends keyof T>(key: K, value: T[K]): void;
|
|
8
|
+
/** Update multiple keys in a batch */
|
|
9
|
+
update(partial: Partial<T>): void;
|
|
10
|
+
/** Reset to initial state */
|
|
11
|
+
reset(): void;
|
|
12
|
+
/** Get an immutable snapshot */
|
|
13
|
+
snapshot(): Readonly<T>;
|
|
14
|
+
/** Subscribe to all changes */
|
|
15
|
+
subscribe(fn: StoreListener<T>): Unsubscribe;
|
|
16
|
+
/** Get underlying signal for a key */
|
|
17
|
+
signal<K extends keyof T>(key: K): Signal<T[K]>;
|
|
18
|
+
/** Reactive proxy — read/write with `store.state.key` */
|
|
19
|
+
readonly state: T;
|
|
20
|
+
}
|
|
21
|
+
export interface PersistOptions {
|
|
22
|
+
/** Storage key for persistence */
|
|
23
|
+
key: string;
|
|
24
|
+
/** Storage backend — defaults to localStorage */
|
|
25
|
+
storage?: Storage;
|
|
26
|
+
/** Custom serializer — defaults to JSON.stringify */
|
|
27
|
+
serialize?: (value: any) => string;
|
|
28
|
+
/** Custom deserializer — defaults to JSON.parse */
|
|
29
|
+
deserialize?: (value: string) => any;
|
|
30
|
+
/** Keys to include (whitelist). If omitted, all keys are persisted. */
|
|
31
|
+
include?: string[];
|
|
32
|
+
/** Keys to exclude (blacklist). */
|
|
33
|
+
exclude?: string[];
|
|
34
|
+
}
|
|
35
|
+
export declare function store<T extends Record<string, any>>(initialState: T): Store<T>;
|
|
36
|
+
export declare function persist<T extends Record<string, any>>(st: Store<T>, options: PersistOptions): Unsubscribe;
|
|
37
|
+
export declare function derived<T extends Record<string, any>, R>(st: Store<T>, fn: (state: Readonly<T>) => R): Signal<R>;
|