@ozsarman/clarityjs 0.6.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 +178 -0
- package/package.json +168 -0
- package/src/analyze.js +534 -0
- package/src/async-state.js +555 -0
- package/src/bundle-runtime.js +35 -0
- package/src/clarity-bundle.js +332 -0
- package/src/clarity-test.js +622 -0
- package/src/cli.js +453 -0
- package/src/codegen.js +1934 -0
- package/src/dev-server.js +362 -0
- package/src/devtools.js +765 -0
- package/src/edge.js +606 -0
- package/src/error-overlay.js +535 -0
- package/src/file-conventions.js +472 -0
- package/src/font.js +513 -0
- package/src/game-loop.js +106 -0
- package/src/head.js +393 -0
- package/src/hydrate.js +292 -0
- package/src/i18n.js +403 -0
- package/src/image.js +352 -0
- package/src/index.js +193 -0
- package/src/islands.js +284 -0
- package/src/isr.js +306 -0
- package/src/layout.js +342 -0
- package/src/lexer.js +572 -0
- package/src/linter.js +547 -0
- package/src/pages-router.js +229 -0
- package/src/parser.js +1108 -0
- package/src/router.js +732 -0
- package/src/runtime.js +1465 -0
- package/src/scoped-css.js +641 -0
- package/src/server-actions.js +439 -0
- package/src/server-data.js +225 -0
- package/src/sourcemap.js +130 -0
- package/src/ssg.js +310 -0
- package/src/ssr.js +621 -0
- package/src/store.js +276 -0
- package/src/transitions.js +438 -0
- package/src/ts-plugin.js +613 -0
- package/src/typegen.js +240 -0
- package/src/vite-plugin.js +447 -0
- package/types/index.d.ts +366 -0
package/src/store.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clarity.js — Global Store
|
|
3
|
+
*
|
|
4
|
+
* Signal-based cross-component state management.
|
|
5
|
+
* Inspired by Pinia (Vue) and Zustand (React), built natively on Clarity signals.
|
|
6
|
+
*
|
|
7
|
+
* Quick start:
|
|
8
|
+
*
|
|
9
|
+
* // stores/counter.js
|
|
10
|
+
* import { createStore } from '@ozsarman/clarityjs/store';
|
|
11
|
+
*
|
|
12
|
+
* export const useCounterStore = createStore('counter', {
|
|
13
|
+
* state: () => ({ count: 0, step: 1 }),
|
|
14
|
+
*
|
|
15
|
+
* getters: {
|
|
16
|
+
* doubled: (state) => state.count * 2,
|
|
17
|
+
* label: (state) => `Count is ${state.count}`,
|
|
18
|
+
* },
|
|
19
|
+
*
|
|
20
|
+
* actions: {
|
|
21
|
+
* increment() { this.count += this.step; },
|
|
22
|
+
* decrement() { this.count -= this.step; },
|
|
23
|
+
* async loadFromAPI() {
|
|
24
|
+
* const data = await fetch('/api/count').then(r => r.json());
|
|
25
|
+
* this.count = data.value;
|
|
26
|
+
* },
|
|
27
|
+
* },
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // In any component:
|
|
31
|
+
* const store = useCounterStore();
|
|
32
|
+
* store.count // reactive read (current signal value)
|
|
33
|
+
* store.doubled // reactive getter
|
|
34
|
+
* store.increment() // action
|
|
35
|
+
* store.$patch({ count: 10, step: 2 }) // batch update
|
|
36
|
+
* store.$reset() // restore initial state
|
|
37
|
+
* store.$subscribe(state => console.log(state)) // watch all changes
|
|
38
|
+
*
|
|
39
|
+
* Devtools integration:
|
|
40
|
+
* Each store self-registers with __clarity_stores__ on globalThis so that
|
|
41
|
+
* the Clarity DevTools panel can enumerate stores and inspect their state.
|
|
42
|
+
*
|
|
43
|
+
* Author: Claude (Anthropic) + Özdemir Sarman
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { signal, computed, effect, batch } from './runtime.js';
|
|
47
|
+
|
|
48
|
+
// ─── Global store registry (for devtools) ────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
if (typeof globalThis.__clarity_stores__ === 'undefined') {
|
|
51
|
+
globalThis.__clarity_stores__ = new Map();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── createStore ─────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Define a global reactive store.
|
|
58
|
+
*
|
|
59
|
+
* Returns a factory function (useXxxStore) that can be called anywhere to get
|
|
60
|
+
* the singleton store instance — all callers share the same reactive signals.
|
|
61
|
+
*
|
|
62
|
+
* @template {object} S
|
|
63
|
+
* @template {object} G
|
|
64
|
+
* @template {object} A
|
|
65
|
+
*
|
|
66
|
+
* @param {string} id — Unique store identifier (used by DevTools)
|
|
67
|
+
* @param {object} definition
|
|
68
|
+
* @param {() => S} definition.state — Factory that returns initial state
|
|
69
|
+
* @param {G} [definition.getters] — Derived values (computed signals)
|
|
70
|
+
* @param {A} [definition.actions] — Methods that mutate state
|
|
71
|
+
*
|
|
72
|
+
* @returns {() => StoreInstance<S, G, A>}
|
|
73
|
+
*/
|
|
74
|
+
export function createStore(id, { state: stateFn, getters = {}, actions = {} }) {
|
|
75
|
+
if (typeof id !== 'string' || !id) {
|
|
76
|
+
throw new Error('[Clarity createStore] id must be a non-empty string');
|
|
77
|
+
}
|
|
78
|
+
if (typeof stateFn !== 'function') {
|
|
79
|
+
throw new Error('[Clarity createStore] state must be a function that returns an object');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Singleton: created once, shared across all callers ─────────────────────
|
|
83
|
+
let _instance = null;
|
|
84
|
+
|
|
85
|
+
function _createInstance() {
|
|
86
|
+
const initialState = stateFn();
|
|
87
|
+
if (typeof initialState !== 'object' || initialState === null) {
|
|
88
|
+
throw new Error(`[Clarity createStore] "${id}" state() must return a plain object`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── 1. Turn every state key into a signal ───────────────────────────────
|
|
92
|
+
const _signals = {};
|
|
93
|
+
for (const [key, val] of Object.entries(initialState)) {
|
|
94
|
+
_signals[key] = signal(val);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── 2. Subscribers list (for $subscribe) ────────────────────────────────
|
|
98
|
+
const _subscribers = new Set();
|
|
99
|
+
|
|
100
|
+
// ── 3. Build the public store proxy ─────────────────────────────────────
|
|
101
|
+
const _stateProxy = new Proxy({}, {
|
|
102
|
+
get(_, key) {
|
|
103
|
+
if (key in _signals) return _signals[key].get();
|
|
104
|
+
return undefined;
|
|
105
|
+
},
|
|
106
|
+
set(_, key, value) {
|
|
107
|
+
if (key in _signals) {
|
|
108
|
+
_signals[key].set(value);
|
|
109
|
+
} else {
|
|
110
|
+
// Dynamically add new state key
|
|
111
|
+
_signals[key] = signal(value);
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
},
|
|
115
|
+
has(_, key) { return key in _signals; },
|
|
116
|
+
ownKeys() { return Object.keys(_signals); },
|
|
117
|
+
getOwnPropertyDescriptor(_, key) {
|
|
118
|
+
if (key in _signals) return { enumerable: true, configurable: true, writable: true };
|
|
119
|
+
return undefined;
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ── 4. Build computed getters ────────────────────────────────────────────
|
|
124
|
+
const _computed = {};
|
|
125
|
+
for (const [key, fn] of Object.entries(getters)) {
|
|
126
|
+
_computed[key] = computed(() => fn(_stateProxy));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── 5. Build bound action methods ────────────────────────────────────────
|
|
130
|
+
// Actions run with `this` pointing at the proxy so they can do:
|
|
131
|
+
// this.count += 1 or await this.loadData()
|
|
132
|
+
const _actions = {};
|
|
133
|
+
for (const [key, fn] of Object.entries(actions)) {
|
|
134
|
+
_actions[key] = function (...args) {
|
|
135
|
+
return fn.apply(_stateProxy, args);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── 6. Effect to notify $subscribe listeners ─────────────────────────────
|
|
140
|
+
effect(() => {
|
|
141
|
+
// Read all signals to subscribe to all of them
|
|
142
|
+
const snapshot = {};
|
|
143
|
+
for (const [k, sig] of Object.entries(_signals)) {
|
|
144
|
+
snapshot[k] = sig.get();
|
|
145
|
+
}
|
|
146
|
+
// Notify subscribers (skip on first run before any listener is added)
|
|
147
|
+
if (_subscribers.size > 0) {
|
|
148
|
+
_subscribers.forEach(fn => {
|
|
149
|
+
try { fn(snapshot); } catch {}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ── 7. Assemble public API ───────────────────────────────────────────────
|
|
155
|
+
const store = new Proxy({}, {
|
|
156
|
+
get(_, key) {
|
|
157
|
+
// State reads — reactive
|
|
158
|
+
if (key in _signals) return _signals[key].get();
|
|
159
|
+
// Getter reads — computed
|
|
160
|
+
if (key in _computed) return _computed[key].get();
|
|
161
|
+
// Actions
|
|
162
|
+
if (key in _actions) return _actions[key];
|
|
163
|
+
// $ helpers
|
|
164
|
+
if (key === '$id') return id;
|
|
165
|
+
if (key === '$state') return _stateProxy;
|
|
166
|
+
if (key === '$signals') return _signals; // for devtools
|
|
167
|
+
if (key === '$patch') return _patch;
|
|
168
|
+
if (key === '$reset') return _reset;
|
|
169
|
+
if (key === '$subscribe') return _subscribe;
|
|
170
|
+
if (key === '$dispose') return _dispose;
|
|
171
|
+
return undefined;
|
|
172
|
+
},
|
|
173
|
+
set(_, key, value) {
|
|
174
|
+
if (key in _signals) {
|
|
175
|
+
_signals[key].set(value);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
},
|
|
180
|
+
has(_, key) {
|
|
181
|
+
return (key in _signals) || (key in _computed) || (key in _actions) ||
|
|
182
|
+
['$id','$state','$signals','$patch','$reset','$subscribe','$dispose'].includes(key);
|
|
183
|
+
},
|
|
184
|
+
ownKeys() {
|
|
185
|
+
return [
|
|
186
|
+
...Object.keys(_signals),
|
|
187
|
+
...Object.keys(_computed),
|
|
188
|
+
...Object.keys(_actions),
|
|
189
|
+
'$id', '$patch', '$reset', '$subscribe', '$dispose',
|
|
190
|
+
];
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ── $patch — batch update multiple state keys ────────────────────────────
|
|
195
|
+
function _patch(partial) {
|
|
196
|
+
if (typeof partial === 'function') {
|
|
197
|
+
// Patcher receives the proxy and mutates directly
|
|
198
|
+
batch(() => partial(_stateProxy));
|
|
199
|
+
} else {
|
|
200
|
+
batch(() => {
|
|
201
|
+
for (const [k, v] of Object.entries(partial)) {
|
|
202
|
+
if (k in _signals) _signals[k].set(v);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── $reset — restore initial state ───────────────────────────────────────
|
|
209
|
+
function _reset() {
|
|
210
|
+
const fresh = stateFn();
|
|
211
|
+
batch(() => {
|
|
212
|
+
for (const [k, v] of Object.entries(fresh)) {
|
|
213
|
+
if (k in _signals) _signals[k].set(v);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── $subscribe — watch all state changes ─────────────────────────────────
|
|
219
|
+
/**
|
|
220
|
+
* @param {(state: S) => void} fn Called with a snapshot on any change
|
|
221
|
+
* @returns {() => void} Unsubscribe function
|
|
222
|
+
*/
|
|
223
|
+
function _subscribe(fn) {
|
|
224
|
+
_subscribers.add(fn);
|
|
225
|
+
return () => _subscribers.delete(fn);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── $dispose — remove from global registry (tests / SSR) ─────────────────
|
|
229
|
+
function _dispose() {
|
|
230
|
+
globalThis.__clarity_stores__.delete(id);
|
|
231
|
+
_instance = null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Register in global devtools registry
|
|
235
|
+
globalThis.__clarity_stores__.set(id, store);
|
|
236
|
+
|
|
237
|
+
return store;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Returned hook — always returns the same singleton ────────────────────
|
|
241
|
+
return function useStore() {
|
|
242
|
+
if (!_instance) _instance = _createInstance();
|
|
243
|
+
return _instance;
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─── Store utilities ─────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get the registry of all active stores (for DevTools).
|
|
251
|
+
* @returns {Map<string, object>}
|
|
252
|
+
*/
|
|
253
|
+
export function getStoreRegistry() {
|
|
254
|
+
return globalThis.__clarity_stores__;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Reset all registered stores to their initial state.
|
|
259
|
+
* Useful in tests: call between each test case.
|
|
260
|
+
*/
|
|
261
|
+
export function resetAllStores() {
|
|
262
|
+
for (const store of globalThis.__clarity_stores__.values()) {
|
|
263
|
+
store.$reset?.();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Dispose (destroy) all registered stores.
|
|
269
|
+
* Used in SSR per-request isolation to avoid state leaking between requests.
|
|
270
|
+
*/
|
|
271
|
+
export function disposeAllStores() {
|
|
272
|
+
for (const store of globalThis.__clarity_stores__.values()) {
|
|
273
|
+
store.$dispose?.();
|
|
274
|
+
}
|
|
275
|
+
globalThis.__clarity_stores__.clear();
|
|
276
|
+
}
|