@epoch32/visage 0.0.0-stage → 1.0.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/LICENSE +373 -0
- package/README.md +110 -2
- package/dist/src/api/index.js +4002 -0
- package/dist/src/charts/index.js +2717 -0
- package/dist/src/devtools/index.js +4677 -0
- package/dist/src/dom/index.js +1550 -0
- package/dist/src/form/index.js +1415 -0
- package/dist/src/index.js +11826 -0
- package/dist/src/jsx-runtime/index.js +1630 -0
- package/dist/src/query/index.js +2422 -0
- package/dist/src/router/index.js +1856 -0
- package/dist/src/rpc/index.js +3484 -0
- package/dist/src/server/index.js +1173 -0
- package/dist/src/signals/index.js +1077 -0
- package/dist/src/style/index.js +2340 -0
- package/dist/src/template/index.js +1149 -0
- package/dist/src/ui/index.js +4155 -0
- package/dist/src/virtual/index.js +1955 -0
- package/dist/src/vite/index.js +984 -0
- package/package.json +140 -4
- package/src/api/client.ts +371 -0
- package/src/api/file-router.ts +183 -0
- package/src/api/handler.ts +326 -0
- package/src/api/index.ts +71 -0
- package/src/api/middleware.ts +54 -0
- package/src/api/pragma.ts +155 -0
- package/src/api/procedure.ts +172 -0
- package/src/api/types.ts +249 -0
- package/src/charts/Area.tsx +111 -0
- package/src/charts/Axis.tsx +126 -0
- package/src/charts/Bar.tsx +86 -0
- package/src/charts/CartesianGrid.tsx +72 -0
- package/src/charts/Chart.tsx +269 -0
- package/src/charts/Cursor.tsx +42 -0
- package/src/charts/Legend.tsx +56 -0
- package/src/charts/Line.tsx +106 -0
- package/src/charts/Pie.tsx +88 -0
- package/src/charts/Tooltip.tsx +127 -0
- package/src/charts/context.ts +30 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/path.ts +155 -0
- package/src/charts/scale.ts +133 -0
- package/src/charts/types.ts +50 -0
- package/src/devtools/DomTab.tsx +134 -0
- package/src/devtools/GraphInspector.tsx +196 -0
- package/src/devtools/GraphTab.tsx +315 -0
- package/src/devtools/QueryTab.tsx +200 -0
- package/src/devtools/RpcTab.tsx +190 -0
- package/src/devtools/SignalsTab.tsx +129 -0
- package/src/devtools/component.tsx +243 -0
- package/src/devtools/highlighter.ts +134 -0
- package/src/devtools/hub.ts +175 -0
- package/src/devtools/index.ts +20 -0
- package/src/devtools/styles.ts +313 -0
- package/src/devtools/types.ts +45 -0
- package/src/dom/async.ts +232 -0
- package/src/dom/control-flow.ts +353 -0
- package/src/dom/index.ts +32 -0
- package/src/dom/lazy.ts +82 -0
- package/src/dom/render.ts +286 -0
- package/src/dom/resource.ts +120 -0
- package/src/form/form.ts +218 -0
- package/src/form/index.ts +17 -0
- package/src/form/schema.ts +299 -0
- package/src/index.ts +413 -0
- package/src/jsx-dev-runtime/index.ts +1 -0
- package/src/jsx-runtime/index.ts +112 -0
- package/src/jsx-runtime/jsx.d.ts +278 -0
- package/src/query/create-mutation.ts +64 -0
- package/src/query/create-query.ts +426 -0
- package/src/query/focus-manager.ts +68 -0
- package/src/query/index.ts +63 -0
- package/src/query/mutation-cache.ts +138 -0
- package/src/query/online-manager.ts +67 -0
- package/src/query/query-cache.ts +369 -0
- package/src/query/query-client.ts +248 -0
- package/src/query/types.ts +221 -0
- package/src/query/utils.ts +132 -0
- package/src/router/index.ts +19 -0
- package/src/router/router.tsx +352 -0
- package/src/rpc/client.ts +317 -0
- package/src/rpc/error.ts +70 -0
- package/src/rpc/index.ts +52 -0
- package/src/rpc/procedure.ts +88 -0
- package/src/rpc/router.ts +105 -0
- package/src/rpc/server.ts +511 -0
- package/src/rpc/types.ts +328 -0
- package/src/server/hydrate.ts +21 -0
- package/src/server/index.ts +10 -0
- package/src/server/render-to-stream.ts +31 -0
- package/src/server/render-to-string.ts +125 -0
- package/src/signals/computed.ts +217 -0
- package/src/signals/context.ts +89 -0
- package/src/signals/effect.ts +161 -0
- package/src/signals/index.ts +57 -0
- package/src/signals/scope.ts +132 -0
- package/src/signals/signal.ts +203 -0
- package/src/signals/store.ts +163 -0
- package/src/signals/system.ts +124 -0
- package/src/signals/types.ts +64 -0
- package/src/style/component.tsx +122 -0
- package/src/style/create-style.ts +229 -0
- package/src/style/cx.ts +75 -0
- package/src/style/hash.ts +19 -0
- package/src/style/index.ts +8 -0
- package/src/style/layout.tsx +406 -0
- package/src/style/manager.ts +87 -0
- package/src/style/parser.ts +257 -0
- package/src/style/scale.ts +237 -0
- package/src/template/index.ts +4 -0
- package/src/template/template.ts +126 -0
- package/src/ui/accordion.tsx +286 -0
- package/src/ui/command-dialog.tsx +49 -0
- package/src/ui/command.tsx +400 -0
- package/src/ui/data-table.tsx +322 -0
- package/src/ui/dialog.tsx +362 -0
- package/src/ui/dropdown-menu.tsx +381 -0
- package/src/ui/icon.tsx +69 -0
- package/src/ui/index.ts +138 -0
- package/src/ui/layout.tsx +13 -0
- package/src/ui/popover.tsx +249 -0
- package/src/ui/switch.tsx +114 -0
- package/src/ui/tabs.tsx +297 -0
- package/src/ui/toast.tsx +280 -0
- package/src/ui/tooltip.tsx +262 -0
- package/src/ui/utils.ts +110 -0
- package/src/virtual/index.ts +12 -0
- package/src/virtual/virtual-list.tsx +185 -0
- package/src/virtual/virtualizer.ts +274 -0
- package/src/vite/hmr.ts +38 -0
- package/src/vite/index.ts +3 -0
- package/src/vite/lucide.ts +124 -0
- package/src/vite/plugin.ts +102 -0
- package/src/vite/rpc-middleware.ts +82 -0
- package/src/vite/types.ts +65 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visage Core Writable Signal & State Primitives
|
|
3
|
+
*/
|
|
4
|
+
import { batch, currentScope, currentSubscriber, warnedCyclicSubscribers } from "./system";
|
|
5
|
+
import {
|
|
6
|
+
type IDependency,
|
|
7
|
+
type ISubscriber,
|
|
8
|
+
type ReadonlySignal,
|
|
9
|
+
SIGNAL_BRAND,
|
|
10
|
+
type SignalOptions,
|
|
11
|
+
SubscriberState,
|
|
12
|
+
type WritableSignal
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
export type { SignalOptions };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Core Writable Signal implementation
|
|
19
|
+
*/
|
|
20
|
+
export class Signal<T> implements IDependency {
|
|
21
|
+
[SIGNAL_BRAND] = true as const;
|
|
22
|
+
private _value: T;
|
|
23
|
+
private _subscribers: Set<ISubscriber> = new Set();
|
|
24
|
+
private _equals: (a: T, b: T) => boolean;
|
|
25
|
+
version = 0;
|
|
26
|
+
|
|
27
|
+
constructor(initialValue: T, equals?: (a: T, b: T) => boolean) {
|
|
28
|
+
this._value = initialValue;
|
|
29
|
+
this._equals = equals ?? Object.is;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get(): T {
|
|
33
|
+
if (currentSubscriber !== null && currentSubscriber.state !== SubscriberState.DISPOSED) {
|
|
34
|
+
this._subscribers.add(currentSubscriber);
|
|
35
|
+
currentSubscriber.sources.add(this);
|
|
36
|
+
currentSubscriber.sourceVersions.set(this, this.version);
|
|
37
|
+
}
|
|
38
|
+
return this._value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
set(newValue: T): void {
|
|
42
|
+
if (this._equals(this._value, newValue)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (currentSubscriber !== null && this._subscribers.has(currentSubscriber)) {
|
|
47
|
+
if (!warnedCyclicSubscribers.has(currentSubscriber)) {
|
|
48
|
+
warnedCyclicSubscribers.add(currentSubscriber);
|
|
49
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
50
|
+
console.warn(
|
|
51
|
+
`[Visage Reactivity] Cyclic update detected: An active subscriber modified a signal that it depends on. ` +
|
|
52
|
+
`To read a signal inside an effect/computed without creating a circular dependency, wrap the read in untrack():\n` +
|
|
53
|
+
` untrack(() => signal())`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this._value = newValue;
|
|
60
|
+
this.version++;
|
|
61
|
+
batch(() => {
|
|
62
|
+
this.notify();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
update(fn: (prev: T) => T): void {
|
|
67
|
+
this.set(fn(this._value));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get value(): T {
|
|
71
|
+
return this.get();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
set value(val: T) {
|
|
75
|
+
this.set(val);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
removeSubscriber(sub: ISubscriber): void {
|
|
79
|
+
this._subscribers.delete(sub);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private notify(): void {
|
|
83
|
+
if (this._subscribers.size === 0) return;
|
|
84
|
+
|
|
85
|
+
const subs = Array.from(this._subscribers);
|
|
86
|
+
for (const sub of subs) {
|
|
87
|
+
sub.markDirty(SubscriberState.DIRTY);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Factory for creating writable, fine-grained reactive signals.
|
|
94
|
+
*
|
|
95
|
+
* Signals are the core primitive of Visage's reactivity. Reading a signal inside
|
|
96
|
+
* a component JSX body, `computed()`, or `effect()` automatically registers that
|
|
97
|
+
* computation as a dependent subscriber.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```tsx
|
|
101
|
+
* const count = signal(0);
|
|
102
|
+
*
|
|
103
|
+
* // Read value
|
|
104
|
+
* console.log(count()); // 0
|
|
105
|
+
* console.log(count.value); // 0
|
|
106
|
+
*
|
|
107
|
+
* // Write value
|
|
108
|
+
* count.set(1);
|
|
109
|
+
* count(2);
|
|
110
|
+
* count.update(prev => prev + 1);
|
|
111
|
+
*
|
|
112
|
+
* // In JSX
|
|
113
|
+
* <div>Current count: {count}</div>
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @param initialValue - The initial value stored in the signal
|
|
117
|
+
* @param optionsOrEquals - Custom equality comparison function or SignalOptions
|
|
118
|
+
* @returns A writable reactive signal function accessor
|
|
119
|
+
*/
|
|
120
|
+
export function signal<T>(
|
|
121
|
+
initialValue: T,
|
|
122
|
+
optionsOrEquals?: ((a: T, b: T) => boolean) | SignalOptions<T>
|
|
123
|
+
): WritableSignal<T> {
|
|
124
|
+
const equals = typeof optionsOrEquals === "function" ? optionsOrEquals : optionsOrEquals?.equals;
|
|
125
|
+
const isInternal = typeof optionsOrEquals === "object" ? optionsOrEquals?.internal : false;
|
|
126
|
+
const customName = typeof optionsOrEquals === "object" ? optionsOrEquals?.name : undefined;
|
|
127
|
+
|
|
128
|
+
const s = new Signal(initialValue, equals);
|
|
129
|
+
|
|
130
|
+
const accessor = ((...args: [value?: T]): T => {
|
|
131
|
+
if (args.length === 0) {
|
|
132
|
+
return s.get();
|
|
133
|
+
}
|
|
134
|
+
s.set(args[0] as T);
|
|
135
|
+
return args[0] as T;
|
|
136
|
+
}) as WritableSignal<T>;
|
|
137
|
+
|
|
138
|
+
Object.defineProperties(accessor, {
|
|
139
|
+
[SIGNAL_BRAND]: { value: true },
|
|
140
|
+
get: { value: () => s.get() },
|
|
141
|
+
set: { value: (v: T) => s.set(v) },
|
|
142
|
+
update: { value: (fn: (prev: T) => T) => s.update(fn) },
|
|
143
|
+
value: {
|
|
144
|
+
get: () => s.get(),
|
|
145
|
+
set: (v: T) => s.set(v)
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!isInternal && typeof window !== "undefined" && (window as any).__VISAGE_DEVTOOLS__) {
|
|
150
|
+
const devtools = (window as any).__VISAGE_DEVTOOLS__;
|
|
151
|
+
const sigId = devtools.registerSignal({
|
|
152
|
+
name: customName,
|
|
153
|
+
type: "signal",
|
|
154
|
+
getValue: () => s.get(),
|
|
155
|
+
version: () => s.version,
|
|
156
|
+
subscribersCount: () => (s as any)._subscribers?.size ?? 0,
|
|
157
|
+
dependents: () =>
|
|
158
|
+
Array.from((s as any)._subscribers ?? [])
|
|
159
|
+
.map((sub: any) => sub._devId)
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
});
|
|
162
|
+
(s as any)._devId = sigId;
|
|
163
|
+
if (currentScope) {
|
|
164
|
+
currentScope.addCleanup(() => {
|
|
165
|
+
devtools.unregisterSignal(sigId);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return accessor;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Tuple-based signal constructor returning `[getter, setter]` (Solid / Preact Signals convention).
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```tsx
|
|
178
|
+
* const [count, setCount] = createSignal(0);
|
|
179
|
+
*
|
|
180
|
+
* setCount(5);
|
|
181
|
+
* setCount(prev => prev + 1);
|
|
182
|
+
*
|
|
183
|
+
* <div>Count: {count}</div>
|
|
184
|
+
* ```
|
|
185
|
+
*
|
|
186
|
+
* @param initialValue - The initial value stored in the signal
|
|
187
|
+
* @param optionsOrEquals - Custom equality comparison function or SignalOptions
|
|
188
|
+
* @returns A tuple containing `[ReadonlySignal<T>, (value: T | ((prev: T) => T)) => void]`
|
|
189
|
+
*/
|
|
190
|
+
export function createSignal<T>(
|
|
191
|
+
initialValue: T,
|
|
192
|
+
optionsOrEquals?: ((a: T, b: T) => boolean) | SignalOptions<T>
|
|
193
|
+
): [ReadonlySignal<T>, (value: T | ((prev: T) => T)) => void] {
|
|
194
|
+
const s = signal(initialValue, optionsOrEquals);
|
|
195
|
+
const setter = (value: T | ((prev: T) => T)) => {
|
|
196
|
+
if (typeof value === "function") {
|
|
197
|
+
s.update(value as (prev: T) => T);
|
|
198
|
+
} else {
|
|
199
|
+
s.set(value);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
return [s, setter];
|
|
203
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Signal } from "./signal";
|
|
2
|
+
import { batch } from "./system";
|
|
3
|
+
import { isSignal } from "./types";
|
|
4
|
+
|
|
5
|
+
const STORE_RAW = Symbol("visage.store.raw");
|
|
6
|
+
const STORE_SIGNALS = Symbol("visage.store.signals");
|
|
7
|
+
const STORE_PROXY = Symbol("visage.store.proxy");
|
|
8
|
+
|
|
9
|
+
export type Store<T> = T;
|
|
10
|
+
|
|
11
|
+
function isObject(val: unknown): val is Record<string | symbol, any> {
|
|
12
|
+
return (
|
|
13
|
+
typeof val === "object" &&
|
|
14
|
+
val !== null &&
|
|
15
|
+
(typeof Node === "undefined" || !(val instanceof Node)) &&
|
|
16
|
+
!isSignal(val)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getOrCreatePropertySignal(target: any, prop: string | symbol): Signal<any> {
|
|
21
|
+
let signalsMap: Map<string | symbol, Signal<any>> = target[STORE_SIGNALS];
|
|
22
|
+
if (!signalsMap) {
|
|
23
|
+
signalsMap = new Map();
|
|
24
|
+
Object.defineProperty(target, STORE_SIGNALS, {
|
|
25
|
+
value: signalsMap,
|
|
26
|
+
enumerable: false,
|
|
27
|
+
writable: true
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let sig = signalsMap.get(prop);
|
|
32
|
+
if (!sig) {
|
|
33
|
+
sig = new Signal(target[prop]);
|
|
34
|
+
signalsMap.set(prop, sig);
|
|
35
|
+
}
|
|
36
|
+
return sig;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createStoreProxy<T extends object>(target: T): T {
|
|
40
|
+
if (!isObject(target)) return target;
|
|
41
|
+
if ((target as any)[STORE_PROXY]) return (target as any)[STORE_PROXY];
|
|
42
|
+
|
|
43
|
+
const proxy = new Proxy(target, {
|
|
44
|
+
get(target, prop, receiver) {
|
|
45
|
+
if (prop === STORE_RAW) return target;
|
|
46
|
+
if (prop === STORE_PROXY) return proxy;
|
|
47
|
+
|
|
48
|
+
// Track property access
|
|
49
|
+
if (typeof prop === "string" || typeof prop === "symbol") {
|
|
50
|
+
const sig = getOrCreatePropertySignal(target, prop);
|
|
51
|
+
sig.get(); // Register dependency
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const value = Reflect.get(target, prop, receiver);
|
|
55
|
+
|
|
56
|
+
// Lazily wrap nested objects in proxy
|
|
57
|
+
if (isObject(value)) {
|
|
58
|
+
return createStoreProxy(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return value;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
set(target, prop, value, receiver) {
|
|
65
|
+
const prevValue = Reflect.get(target, prop, receiver);
|
|
66
|
+
if (Object.is(prevValue, value)) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const success = Reflect.set(target, prop, value, receiver);
|
|
71
|
+
if (success) {
|
|
72
|
+
const sig = getOrCreatePropertySignal(target, prop);
|
|
73
|
+
sig.set(value);
|
|
74
|
+
|
|
75
|
+
// If array length or property keys changed, notify length/keys
|
|
76
|
+
if (Array.isArray(target) && prop !== "length") {
|
|
77
|
+
const lenSig = getOrCreatePropertySignal(target, "length");
|
|
78
|
+
lenSig.set(target.length);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return success;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
deleteProperty(target, prop) {
|
|
85
|
+
const hadProp = Reflect.has(target, prop);
|
|
86
|
+
const success = Reflect.deleteProperty(target, prop);
|
|
87
|
+
if (hadProp && success) {
|
|
88
|
+
const sig = getOrCreatePropertySignal(target, prop);
|
|
89
|
+
sig.set(undefined);
|
|
90
|
+
}
|
|
91
|
+
return success;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
Object.defineProperty(target, STORE_PROXY, {
|
|
96
|
+
value: proxy,
|
|
97
|
+
enumerable: false,
|
|
98
|
+
writable: true
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return proxy;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create a deep reactive proxy store with per-property fine-grained signal tracking.
|
|
106
|
+
*
|
|
107
|
+
* Nested objects and arrays are lazily wrapped in reactive proxies. Reading a property
|
|
108
|
+
* tracks that exact key, and updating a property only notifies computations observing that key.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```tsx
|
|
112
|
+
* const state = createStore({
|
|
113
|
+
* user: { name: "Alice", role: "admin" },
|
|
114
|
+
* items: ["Apple", "Banana"]
|
|
115
|
+
* });
|
|
116
|
+
*
|
|
117
|
+
* // Fine-grained read
|
|
118
|
+
* <div>User: {() => state.user.name}</div>
|
|
119
|
+
*
|
|
120
|
+
* // Fine-grained write (mutates only user.name subscribers)
|
|
121
|
+
* state.user.name = "Bob";
|
|
122
|
+
* state.items.push("Cherry");
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* @param initialState - An object or array representing the initial state tree
|
|
126
|
+
* @returns A deeply reactive proxy store
|
|
127
|
+
*/
|
|
128
|
+
export function createStore<T extends object>(initialState: T): Store<T> {
|
|
129
|
+
if (!isObject(initialState)) {
|
|
130
|
+
throw new Error("createStore requires an object or array as initial state");
|
|
131
|
+
}
|
|
132
|
+
return createStoreProxy(initialState);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Mutate a store inside a synchronous batch operation.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```tsx
|
|
140
|
+
* mutateStore(state, (draft) => {
|
|
141
|
+
* draft.user.name = "Charlie";
|
|
142
|
+
* draft.user.role = "user";
|
|
143
|
+
* });
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export function mutateStore<T extends object>(store: Store<T>, mutator: (draft: T) => void): void {
|
|
147
|
+
batch(() => {
|
|
148
|
+
mutator(store);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Unwrap a reactive proxy store to get the underlying raw, un-proxied JavaScript object.
|
|
154
|
+
*
|
|
155
|
+
* @param store - The reactive store proxy
|
|
156
|
+
* @returns The raw underlying object
|
|
157
|
+
*/
|
|
158
|
+
export function unwrapStore<T>(store: T): T {
|
|
159
|
+
if (isObject(store) && (store as any)[STORE_RAW]) {
|
|
160
|
+
return (store as any)[STORE_RAW];
|
|
161
|
+
}
|
|
162
|
+
return store;
|
|
163
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visage Reactivity System Execution Context & Scheduler
|
|
3
|
+
*/
|
|
4
|
+
import { type ISubscriber, SubscriberState } from "./types";
|
|
5
|
+
|
|
6
|
+
export let currentSubscriber: ISubscriber | null = null;
|
|
7
|
+
export let currentScope: any = null;
|
|
8
|
+
export let batchDepth = 0;
|
|
9
|
+
export const pendingEffects: Set<any> = new Set();
|
|
10
|
+
export const warnedCyclicSubscribers: WeakSet<ISubscriber> = new WeakSet();
|
|
11
|
+
export let isFlushing = false;
|
|
12
|
+
|
|
13
|
+
export function setCurrentSubscriber(sub: ISubscriber | null): void {
|
|
14
|
+
currentSubscriber = sub;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getCurrentSubscriber(): ISubscriber | null {
|
|
18
|
+
return currentSubscriber;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function setCurrentScope(scope: any): void {
|
|
22
|
+
currentScope = scope;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getCurrentScope(): any {
|
|
26
|
+
return currentScope;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function addPendingEffect(eff: any): void {
|
|
30
|
+
pendingEffects.add(eff);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function removePendingEffect(eff: any): void {
|
|
34
|
+
pendingEffects.delete(eff);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const MAX_FLUSH_COUNT = 100;
|
|
38
|
+
|
|
39
|
+
export function flushEffects(): void {
|
|
40
|
+
if (isFlushing || batchDepth > 0) return;
|
|
41
|
+
isFlushing = true;
|
|
42
|
+
let iterations = 0;
|
|
43
|
+
try {
|
|
44
|
+
while (pendingEffects.size > 0) {
|
|
45
|
+
if (++iterations > MAX_FLUSH_COUNT) {
|
|
46
|
+
pendingEffects.clear();
|
|
47
|
+
throw new Error(
|
|
48
|
+
`[Visage Reactivity] Maximum update depth exceeded (${MAX_FLUSH_COUNT} cycles). ` +
|
|
49
|
+
`An effect continuously triggered state mutations (infinite loop detected). ` +
|
|
50
|
+
`Ensure effects that modify signals do not unconditionally depend on them, or use untrack() to read state.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const effectsToRun = Array.from(pendingEffects);
|
|
54
|
+
pendingEffects.clear();
|
|
55
|
+
|
|
56
|
+
for (const eff of effectsToRun) {
|
|
57
|
+
if (eff.state === SubscriberState.DIRTY || eff.state === SubscriberState.CHECK_DIRTY) {
|
|
58
|
+
eff.run();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} finally {
|
|
63
|
+
isFlushing = false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Batch multiple signal mutations so downstreams and effects update synchronously once at completion.
|
|
69
|
+
*
|
|
70
|
+
* Prevents intermediate flicker, wasted recomputations, and glitch states.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```tsx
|
|
74
|
+
* const firstName = signal("Jane");
|
|
75
|
+
* const lastName = signal("Doe");
|
|
76
|
+
*
|
|
77
|
+
* batch(() => {
|
|
78
|
+
* firstName.set("John");
|
|
79
|
+
* lastName.set("Smith");
|
|
80
|
+
* // Downstream computed and effects run only ONCE here
|
|
81
|
+
* });
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @param fn - A function performing multiple signal updates
|
|
85
|
+
* @returns The return value of `fn`
|
|
86
|
+
*/
|
|
87
|
+
export function batch<T>(fn: () => T): T {
|
|
88
|
+
batchDepth++;
|
|
89
|
+
try {
|
|
90
|
+
return fn();
|
|
91
|
+
} finally {
|
|
92
|
+
batchDepth--;
|
|
93
|
+
if (batchDepth === 0) {
|
|
94
|
+
flushEffects();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Read reactive state without subscribing the current active effect or computed computation.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```tsx
|
|
104
|
+
* effect(() => {
|
|
105
|
+
* const id = currentId(); // Subscribed: effect re-runs when currentId changes
|
|
106
|
+
*
|
|
107
|
+
* // Not subscribed: effect does NOT re-run when analyticsCounter changes
|
|
108
|
+
* const count = untrack(() => analyticsCounter());
|
|
109
|
+
* sendMetric(id, count);
|
|
110
|
+
* });
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* @param fn - A function reading reactive signals without tracking
|
|
114
|
+
* @returns The return value of `fn`
|
|
115
|
+
*/
|
|
116
|
+
export function untrack<T>(fn: () => T): T {
|
|
117
|
+
const prev = currentSubscriber;
|
|
118
|
+
currentSubscriber = null;
|
|
119
|
+
try {
|
|
120
|
+
return fn();
|
|
121
|
+
} finally {
|
|
122
|
+
currentSubscriber = prev;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visage Fine-Grained Reactive Engine Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type CleanupFn = () => void;
|
|
6
|
+
|
|
7
|
+
// State flags for subscriber status
|
|
8
|
+
export enum SubscriberState {
|
|
9
|
+
CLEAN = 0,
|
|
10
|
+
CHECK_DIRTY = 1,
|
|
11
|
+
DIRTY = 2,
|
|
12
|
+
DISPOSED = 3
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const SIGNAL_BRAND = Symbol("visage.signal");
|
|
16
|
+
export const COMPUTED_BRAND = Symbol("visage.computed");
|
|
17
|
+
|
|
18
|
+
export interface ReadonlySignal<T> {
|
|
19
|
+
get(): T;
|
|
20
|
+
readonly value: T;
|
|
21
|
+
(): T;
|
|
22
|
+
readonly [SIGNAL_BRAND]: true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WritableSignal<T> extends ReadonlySignal<T> {
|
|
26
|
+
set(value: T): void;
|
|
27
|
+
update(fn: (prev: T) => T): void;
|
|
28
|
+
(value?: T): T;
|
|
29
|
+
value: T;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SignalOptions<T> {
|
|
33
|
+
name?: string;
|
|
34
|
+
equals?: (a: T, b: T) => boolean;
|
|
35
|
+
internal?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ComputedOptions {
|
|
39
|
+
name?: string;
|
|
40
|
+
internal?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if a value is a Visage signal or computed
|
|
45
|
+
*/
|
|
46
|
+
export function isSignal<T = unknown>(value: unknown): value is ReadonlySignal<T> {
|
|
47
|
+
return typeof value === "function" && (SIGNAL_BRAND in value || COMPUTED_BRAND in value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ISubscriber {
|
|
51
|
+
state: SubscriberState;
|
|
52
|
+
sources: Set<IDependency>;
|
|
53
|
+
sourceVersions: Map<IDependency, number>;
|
|
54
|
+
markDirty(targetState?: SubscriberState): void;
|
|
55
|
+
dispose(): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface IDependency {
|
|
59
|
+
version: number;
|
|
60
|
+
removeSubscriber(sub: ISubscriber): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type Subscriber = ISubscriber;
|
|
64
|
+
export type Dependency = IDependency;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { Child } from "../dom/render";
|
|
2
|
+
import { effect, onCleanup } from "../signals";
|
|
3
|
+
import { objectToCSS, type StyleDefinition } from "./create-style";
|
|
4
|
+
import { getScopeId } from "./hash";
|
|
5
|
+
import { styleManager } from "./manager";
|
|
6
|
+
import { scopeCSS } from "./parser";
|
|
7
|
+
|
|
8
|
+
export interface StyleProps {
|
|
9
|
+
children?: string | StyleDefinition | (() => string | StyleDefinition);
|
|
10
|
+
classes?: StyleDefinition | (() => StyleDefinition);
|
|
11
|
+
passToChildren?: boolean;
|
|
12
|
+
id?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Scoped Component Style Block
|
|
17
|
+
*
|
|
18
|
+
* Automatically hashes and scopes CSS rules to the parent component/container,
|
|
19
|
+
* preventing style leaks and collisions. Supports CSS strings or JavaScript style objects.
|
|
20
|
+
*
|
|
21
|
+
* Examples:
|
|
22
|
+
*
|
|
23
|
+
* 1. Object syntax:
|
|
24
|
+
* ```tsx
|
|
25
|
+
* export function Card(props) {
|
|
26
|
+
* return (
|
|
27
|
+
* <div class="card">
|
|
28
|
+
* <h2>{props.title}</h2>
|
|
29
|
+
* <Style classes={{
|
|
30
|
+
* card: { background: "var(--card-bg)", padding: "20px", borderRadius: "8px" },
|
|
31
|
+
* "& h2": { fontSize: "1.2rem", color: "var(--text)" }
|
|
32
|
+
* }} />
|
|
33
|
+
* </div>
|
|
34
|
+
* );
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* 2. Component Scoped CSS string:
|
|
39
|
+
* ```tsx
|
|
40
|
+
* export function StepItem(props) {
|
|
41
|
+
* return (
|
|
42
|
+
* <div class="step-node">
|
|
43
|
+
* <span class="step-num">{props.step}</span>
|
|
44
|
+
* <span>{props.title}</span>
|
|
45
|
+
*
|
|
46
|
+
* <Style>{`
|
|
47
|
+
* .step-node { display: flex; align-items: center; gap: 8px; }
|
|
48
|
+
* .step-num { font-weight: 600; color: var(--primary); }
|
|
49
|
+
* `}</Style>
|
|
50
|
+
* </div>
|
|
51
|
+
* );
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function Style(props: StyleProps): Child {
|
|
56
|
+
const getRawCSS = (): string => {
|
|
57
|
+
if (props.classes) {
|
|
58
|
+
const cls = typeof props.classes === "function" ? props.classes() : props.classes;
|
|
59
|
+
return objectToCSS(cls);
|
|
60
|
+
}
|
|
61
|
+
if (props.children) {
|
|
62
|
+
const ch = typeof props.children === "function" ? props.children() : props.children;
|
|
63
|
+
return typeof ch === "object" ? objectToCSS(ch) : ch;
|
|
64
|
+
}
|
|
65
|
+
return "";
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const rawCSS = getRawCSS();
|
|
69
|
+
const scopeId = props.id ?? getScopeId(rawCSS);
|
|
70
|
+
const scopeAttr = `data-${scopeId}`;
|
|
71
|
+
|
|
72
|
+
// In browser, create a marker node to locate parent element and tag it
|
|
73
|
+
if (typeof document !== "undefined") {
|
|
74
|
+
const marker = document.createTextNode("");
|
|
75
|
+
let unsubscribe: (() => void) | null = null;
|
|
76
|
+
|
|
77
|
+
effect(() => {
|
|
78
|
+
const currentCSS = getRawCSS();
|
|
79
|
+
const currentScopeId = props.id ?? getScopeId(currentCSS);
|
|
80
|
+
const scoped = scopeCSS(currentCSS, {
|
|
81
|
+
scopeId: currentScopeId,
|
|
82
|
+
passToChildren: props.passToChildren ?? true
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (unsubscribe) {
|
|
86
|
+
unsubscribe();
|
|
87
|
+
}
|
|
88
|
+
unsubscribe = styleManager.inject(currentScopeId, scoped);
|
|
89
|
+
|
|
90
|
+
// Attach data-v-xyz attribute to parent container
|
|
91
|
+
const attachScope = () => {
|
|
92
|
+
const parent = (marker.parentElement || marker.parentNode) as HTMLElement | null;
|
|
93
|
+
if (parent && typeof parent.setAttribute === "function") {
|
|
94
|
+
parent.setAttribute(`data-${currentScopeId}`, "");
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
attachScope();
|
|
98
|
+
queueMicrotask(attachScope);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
onCleanup(() => {
|
|
102
|
+
if (unsubscribe) {
|
|
103
|
+
unsubscribe();
|
|
104
|
+
}
|
|
105
|
+
const parent = (marker.parentElement || marker.parentNode) as HTMLElement | null;
|
|
106
|
+
if (parent && typeof parent.removeAttribute === "function") {
|
|
107
|
+
parent.removeAttribute(scopeAttr);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return marker;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// SSR environment: inject CSS into SSR registry
|
|
115
|
+
const scoped = scopeCSS(rawCSS, {
|
|
116
|
+
scopeId,
|
|
117
|
+
passToChildren: props.passToChildren ?? true
|
|
118
|
+
});
|
|
119
|
+
styleManager.inject(scopeId, scoped);
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
}
|