@mizchi/luna 0.1.4
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 +126 -0
- package/dom.d.ts +2 -0
- package/dom.js +2 -0
- package/index.d.ts +564 -0
- package/index.js +785 -0
- package/jsx-dev-runtime.d.ts +2 -0
- package/jsx-dev-runtime.js +2 -0
- package/jsx-runtime.d.ts +123 -0
- package/jsx-runtime.js +119 -0
- package/package.json +37 -0
package/index.js
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
// Re-export from MoonBit build output (api_js)
|
|
2
|
+
// This file wraps MoonBit APIs to provide SolidJS-compatible interface
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
// Signal API (internal)
|
|
6
|
+
createSignal as _createSignal,
|
|
7
|
+
get as _get,
|
|
8
|
+
set as _set,
|
|
9
|
+
update as _update,
|
|
10
|
+
peek as _peek,
|
|
11
|
+
subscribe as _subscribe,
|
|
12
|
+
map as _map,
|
|
13
|
+
createMemo as _createMemo,
|
|
14
|
+
combine as _combine,
|
|
15
|
+
effect as _effect,
|
|
16
|
+
batchStart,
|
|
17
|
+
batchEnd,
|
|
18
|
+
runUntracked,
|
|
19
|
+
batch,
|
|
20
|
+
onCleanup,
|
|
21
|
+
createRoot,
|
|
22
|
+
getOwner,
|
|
23
|
+
runWithOwner,
|
|
24
|
+
hasOwner,
|
|
25
|
+
onMount,
|
|
26
|
+
// DOM API
|
|
27
|
+
text,
|
|
28
|
+
textDyn,
|
|
29
|
+
render,
|
|
30
|
+
mount,
|
|
31
|
+
show,
|
|
32
|
+
jsx,
|
|
33
|
+
jsxs,
|
|
34
|
+
Fragment,
|
|
35
|
+
createElement,
|
|
36
|
+
events,
|
|
37
|
+
forEach,
|
|
38
|
+
// Timer utilities
|
|
39
|
+
debounced as _debounced,
|
|
40
|
+
// Route definitions
|
|
41
|
+
routePage,
|
|
42
|
+
routePageTitled,
|
|
43
|
+
routePageFull,
|
|
44
|
+
createRouter,
|
|
45
|
+
routerNavigate,
|
|
46
|
+
routerReplace,
|
|
47
|
+
routerGetPath,
|
|
48
|
+
routerGetMatch,
|
|
49
|
+
routerGetBase,
|
|
50
|
+
// Context API
|
|
51
|
+
createContext,
|
|
52
|
+
provide,
|
|
53
|
+
useContext,
|
|
54
|
+
// Resource API
|
|
55
|
+
createResource as _createResource,
|
|
56
|
+
createDeferred as _createDeferred,
|
|
57
|
+
resourceGet,
|
|
58
|
+
resourcePeek,
|
|
59
|
+
resourceRefetch,
|
|
60
|
+
resourceIsPending,
|
|
61
|
+
resourceIsSuccess,
|
|
62
|
+
resourceIsFailure,
|
|
63
|
+
resourceValue,
|
|
64
|
+
resourceError,
|
|
65
|
+
stateIsPending,
|
|
66
|
+
stateIsSuccess,
|
|
67
|
+
stateIsFailure,
|
|
68
|
+
stateValue,
|
|
69
|
+
stateError,
|
|
70
|
+
// Portal API
|
|
71
|
+
portalToBody,
|
|
72
|
+
portalToSelector,
|
|
73
|
+
portalWithShadow,
|
|
74
|
+
portalToElementWithShadow,
|
|
75
|
+
} from "../../target/js/release/build/platform/js/api/api.js";
|
|
76
|
+
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// SolidJS-compatible Signal API
|
|
79
|
+
// ============================================================================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates a reactive signal (SolidJS-style)
|
|
83
|
+
* @template T
|
|
84
|
+
* @param {T} initialValue
|
|
85
|
+
* @returns {[() => T, (value: T | ((prev: T) => T)) => void]}
|
|
86
|
+
*/
|
|
87
|
+
export function createSignal(initialValue) {
|
|
88
|
+
const signal = _createSignal(initialValue);
|
|
89
|
+
|
|
90
|
+
const getter = () => _get(signal);
|
|
91
|
+
|
|
92
|
+
const setter = (valueOrUpdater) => {
|
|
93
|
+
if (typeof valueOrUpdater === "function") {
|
|
94
|
+
_update(signal, valueOrUpdater);
|
|
95
|
+
} else {
|
|
96
|
+
_set(signal, valueOrUpdater);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return [getter, setter];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates a reactive effect (SolidJS-style alias)
|
|
105
|
+
*/
|
|
106
|
+
export function createEffect(fn) {
|
|
107
|
+
return _effect(fn);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Creates a memoized computed value (SolidJS-style)
|
|
112
|
+
* @template T
|
|
113
|
+
* @param {() => T} fn
|
|
114
|
+
* @returns {() => T}
|
|
115
|
+
*/
|
|
116
|
+
export function createMemo(fn) {
|
|
117
|
+
return _createMemo(fn);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Runs a function without tracking dependencies (SolidJS-style alias)
|
|
122
|
+
*/
|
|
123
|
+
export { runUntracked as untrack };
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Explicit dependency tracking helper (SolidJS-style)
|
|
127
|
+
* Wraps a function to explicitly specify which signals to track
|
|
128
|
+
*
|
|
129
|
+
* @template T
|
|
130
|
+
* @template U
|
|
131
|
+
* @param {(() => T) | Array<() => any>} deps - Signal accessor(s) to track
|
|
132
|
+
* @param {(input: T, prevInput?: T, prevValue?: U) => U} fn - Function to run with dependency values
|
|
133
|
+
* @param {{ defer?: boolean }} [options] - Options (defer: don't run on initial)
|
|
134
|
+
* @returns {(prevValue?: U) => U | undefined}
|
|
135
|
+
*/
|
|
136
|
+
export function on(deps, fn, options = {}) {
|
|
137
|
+
const { defer = false } = options;
|
|
138
|
+
const isArray = Array.isArray(deps);
|
|
139
|
+
|
|
140
|
+
let prevInput;
|
|
141
|
+
let prevValue;
|
|
142
|
+
let isFirst = true;
|
|
143
|
+
|
|
144
|
+
return (injectedPrevValue) => {
|
|
145
|
+
// Get current dependency values
|
|
146
|
+
const input = isArray ? deps.map((d) => d()) : deps();
|
|
147
|
+
|
|
148
|
+
// Handle deferred execution
|
|
149
|
+
if (defer && isFirst) {
|
|
150
|
+
isFirst = false;
|
|
151
|
+
prevInput = input;
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Run the function with current and previous values
|
|
156
|
+
const result = fn(input, prevInput, injectedPrevValue ?? prevValue);
|
|
157
|
+
|
|
158
|
+
// Store for next run
|
|
159
|
+
prevInput = input;
|
|
160
|
+
prevValue = result;
|
|
161
|
+
isFirst = false;
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Merge multiple props objects, with later objects taking precedence (SolidJS-style)
|
|
169
|
+
* Event handlers and refs are merged, other props are overwritten
|
|
170
|
+
*
|
|
171
|
+
* @template T
|
|
172
|
+
* @param {...T} sources - Props objects to merge
|
|
173
|
+
* @returns {T}
|
|
174
|
+
*/
|
|
175
|
+
export function mergeProps(...sources) {
|
|
176
|
+
const result = {};
|
|
177
|
+
|
|
178
|
+
for (const source of sources) {
|
|
179
|
+
if (!source) continue;
|
|
180
|
+
|
|
181
|
+
for (const key of Object.keys(source)) {
|
|
182
|
+
const value = source[key];
|
|
183
|
+
|
|
184
|
+
// Merge event handlers (on* props)
|
|
185
|
+
if (key.startsWith("on") && typeof value === "function") {
|
|
186
|
+
const existing = result[key];
|
|
187
|
+
if (typeof existing === "function") {
|
|
188
|
+
result[key] = (...args) => {
|
|
189
|
+
existing(...args);
|
|
190
|
+
value(...args);
|
|
191
|
+
};
|
|
192
|
+
} else {
|
|
193
|
+
result[key] = value;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Merge ref callbacks
|
|
197
|
+
else if (key === "ref" && typeof value === "function") {
|
|
198
|
+
const existing = result[key];
|
|
199
|
+
if (typeof existing === "function") {
|
|
200
|
+
result[key] = (el) => {
|
|
201
|
+
existing(el);
|
|
202
|
+
value(el);
|
|
203
|
+
};
|
|
204
|
+
} else {
|
|
205
|
+
result[key] = value;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Merge class/className
|
|
209
|
+
else if (key === "class" || key === "className") {
|
|
210
|
+
const existing = result[key];
|
|
211
|
+
if (existing) {
|
|
212
|
+
result[key] = `${existing} ${value}`;
|
|
213
|
+
} else {
|
|
214
|
+
result[key] = value;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Merge style objects
|
|
218
|
+
else if (key === "style" && typeof value === "object" && typeof result[key] === "object") {
|
|
219
|
+
result[key] = { ...result[key], ...value };
|
|
220
|
+
}
|
|
221
|
+
// Default: overwrite
|
|
222
|
+
else {
|
|
223
|
+
result[key] = value;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Split props into multiple objects based on key lists (SolidJS-style)
|
|
233
|
+
*
|
|
234
|
+
* @template T
|
|
235
|
+
* @template K
|
|
236
|
+
* @param {T} props - Props object to split
|
|
237
|
+
* @param {...K[]} keys - Arrays of keys to extract
|
|
238
|
+
* @returns {[Pick<T, K>, Omit<T, K>]}
|
|
239
|
+
*/
|
|
240
|
+
export function splitProps(props, ...keys) {
|
|
241
|
+
const result = [];
|
|
242
|
+
const remaining = { ...props };
|
|
243
|
+
|
|
244
|
+
for (const keyList of keys) {
|
|
245
|
+
const extracted = {};
|
|
246
|
+
for (const key of keyList) {
|
|
247
|
+
if (key in remaining) {
|
|
248
|
+
extracted[key] = remaining[key];
|
|
249
|
+
delete remaining[key];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
result.push(extracted);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
result.push(remaining);
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Creates a resource for async data (SolidJS-style)
|
|
261
|
+
* @template T
|
|
262
|
+
* @param {(resolve: (v: T) => void, reject: (e: string) => void) => void} fetcher
|
|
263
|
+
* @returns {[ResourceAccessor<T>, { refetch: () => void }]}
|
|
264
|
+
*/
|
|
265
|
+
export function createResource(fetcher) {
|
|
266
|
+
const resource = _createResource(fetcher);
|
|
267
|
+
|
|
268
|
+
// Use resourceGet for tracking dependencies, stateValue for actual value
|
|
269
|
+
const accessor = () => stateValue(resourceGet(resource));
|
|
270
|
+
Object.defineProperties(accessor, {
|
|
271
|
+
loading: { get: () => resourceIsPending(resource) },
|
|
272
|
+
error: { get: () => resourceError(resource) },
|
|
273
|
+
state: {
|
|
274
|
+
get: () => {
|
|
275
|
+
if (resourceIsPending(resource)) return "pending";
|
|
276
|
+
if (resourceIsSuccess(resource)) return "ready";
|
|
277
|
+
if (resourceIsFailure(resource)) return "errored";
|
|
278
|
+
return "unresolved";
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
latest: { get: () => resourcePeek(resource) },
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return [accessor, { refetch: () => resourceRefetch(resource) }];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Creates a deferred resource (SolidJS-style)
|
|
289
|
+
*/
|
|
290
|
+
export function createDeferred() {
|
|
291
|
+
const result = _createDeferred();
|
|
292
|
+
const resource = result._0;
|
|
293
|
+
const resolve = result._1;
|
|
294
|
+
const reject = result._2;
|
|
295
|
+
|
|
296
|
+
// Use resourceGet for tracking dependencies, stateValue for actual value
|
|
297
|
+
const accessor = () => stateValue(resourceGet(resource));
|
|
298
|
+
Object.defineProperties(accessor, {
|
|
299
|
+
loading: { get: () => resourceIsPending(resource) },
|
|
300
|
+
error: { get: () => resourceError(resource) },
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return [accessor, resolve, reject];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Debounces a signal (returns SolidJS-style signal)
|
|
308
|
+
*/
|
|
309
|
+
export function debounced(signal, delayMs) {
|
|
310
|
+
const [getter] = signal;
|
|
311
|
+
const innerSignal = _createSignal(getter());
|
|
312
|
+
const debouncedInner = _debounced(innerSignal, delayMs);
|
|
313
|
+
return [() => _get(debouncedInner), (v) => _set(innerSignal, v)];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ============================================================================
|
|
317
|
+
// SolidJS-compatible Component API
|
|
318
|
+
// ============================================================================
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* For component for list rendering (SolidJS-style)
|
|
322
|
+
* @template T
|
|
323
|
+
* @param {{ each: () => T[], fallback?: any, children: (item: T, index: () => number) => any }} props
|
|
324
|
+
* @returns {any}
|
|
325
|
+
*/
|
|
326
|
+
export function For(props) {
|
|
327
|
+
const { each, fallback, children } = props;
|
|
328
|
+
|
|
329
|
+
// If each is not provided or is falsy, show fallback
|
|
330
|
+
if (!each) {
|
|
331
|
+
return fallback ?? null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// each should be a getter function
|
|
335
|
+
const getter = typeof each === "function" ? each : () => each;
|
|
336
|
+
|
|
337
|
+
return forEach(getter, (item, index) => {
|
|
338
|
+
// Wrap index in a getter for SolidJS compatibility
|
|
339
|
+
return children(item, () => index);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Show component for conditional rendering (SolidJS-style)
|
|
345
|
+
* Note: fallback prop is not yet supported (Luna limitation)
|
|
346
|
+
* @template T
|
|
347
|
+
* @param {{ when: T | (() => T), fallback?: any, children: any | ((item: T) => any) }} props
|
|
348
|
+
* @returns {any}
|
|
349
|
+
*/
|
|
350
|
+
export function Show(props) {
|
|
351
|
+
const { when, children } = props;
|
|
352
|
+
// TODO: fallback support requires MoonBit-side changes
|
|
353
|
+
|
|
354
|
+
// Convert when to a getter if it's not already
|
|
355
|
+
const condition = typeof when === "function" ? when : () => when;
|
|
356
|
+
|
|
357
|
+
// If children is a function, we need to call it with the truthy value
|
|
358
|
+
const renderChildren =
|
|
359
|
+
typeof children === "function" ? () => children(condition()) : () => children;
|
|
360
|
+
|
|
361
|
+
return show(() => Boolean(condition()), renderChildren);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Index component for index-based list rendering (SolidJS-style)
|
|
366
|
+
* Unlike For which tracks items by reference, Index tracks by index position
|
|
367
|
+
* Item signals update in place when values change at the same index
|
|
368
|
+
*
|
|
369
|
+
* @template T
|
|
370
|
+
* @param {{ each: () => T[], fallback?: any, children: (item: () => T, index: number) => any }} props
|
|
371
|
+
* @returns {any}
|
|
372
|
+
*/
|
|
373
|
+
export function Index(props) {
|
|
374
|
+
const { each, fallback, children } = props;
|
|
375
|
+
|
|
376
|
+
if (!each) {
|
|
377
|
+
return fallback ?? null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const getter = typeof each === "function" ? each : () => each;
|
|
381
|
+
const items = getter();
|
|
382
|
+
|
|
383
|
+
if (items.length === 0 && fallback) {
|
|
384
|
+
return fallback;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Use index_each from MoonBit if available, otherwise simulate with forEach
|
|
388
|
+
// For now, we'll use forEach with index-based tracking
|
|
389
|
+
return forEach(getter, (_item, index) => {
|
|
390
|
+
// Provide item as a getter for reactivity at that index
|
|
391
|
+
const itemGetter = () => getter()[index];
|
|
392
|
+
return children(itemGetter, index);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Provider component for Context (SolidJS-style)
|
|
398
|
+
* Provides a context value to all descendants
|
|
399
|
+
*
|
|
400
|
+
* @template T
|
|
401
|
+
* @param {{ context: Context<T>, value: T, children: any | (() => any) }} props
|
|
402
|
+
* @returns {any}
|
|
403
|
+
*/
|
|
404
|
+
export function Provider(props) {
|
|
405
|
+
const { context, value, children } = props;
|
|
406
|
+
|
|
407
|
+
return provide(context, value, () => {
|
|
408
|
+
return typeof children === "function" ? children() : children;
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Switch component for conditional rendering with multiple branches (SolidJS-style)
|
|
414
|
+
* Renders the first Match that evaluates to true
|
|
415
|
+
*
|
|
416
|
+
* @param {{ fallback?: any, children: any[] }} props
|
|
417
|
+
* @returns {any}
|
|
418
|
+
*/
|
|
419
|
+
export function Switch(props) {
|
|
420
|
+
const { fallback, children } = props;
|
|
421
|
+
|
|
422
|
+
// children should be Match components, each with { when, children }
|
|
423
|
+
// Since we don't have compile-time JSX, children is an array of Match results
|
|
424
|
+
|
|
425
|
+
if (!Array.isArray(children)) {
|
|
426
|
+
return fallback ?? null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Find first truthy match
|
|
430
|
+
for (const child of children) {
|
|
431
|
+
if (child && child.__isMatch && child.when()) {
|
|
432
|
+
return typeof child.children === "function" ? child.children() : child.children;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return fallback ?? null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Match component for use inside Switch (SolidJS-style)
|
|
441
|
+
*
|
|
442
|
+
* @template T
|
|
443
|
+
* @param {{ when: T | (() => T), children: any | ((item: T) => any) }} props
|
|
444
|
+
* @returns {{ __isMatch: true, when: () => boolean, children: any }}
|
|
445
|
+
*/
|
|
446
|
+
export function Match(props) {
|
|
447
|
+
const { when, children } = props;
|
|
448
|
+
const condition = typeof when === "function" ? when : () => when;
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
__isMatch: true,
|
|
452
|
+
when: () => Boolean(condition()),
|
|
453
|
+
children:
|
|
454
|
+
typeof children === "function"
|
|
455
|
+
? () => children(condition())
|
|
456
|
+
: children,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Portal component for rendering outside the component tree (SolidJS-style)
|
|
462
|
+
* Teleports children to a different DOM location
|
|
463
|
+
*
|
|
464
|
+
* @param {{ mount?: Element | string, useShadow?: boolean, children: any | (() => any) }} props
|
|
465
|
+
* @returns {any}
|
|
466
|
+
*/
|
|
467
|
+
export function Portal(props) {
|
|
468
|
+
const { mount, useShadow = false, children } = props;
|
|
469
|
+
|
|
470
|
+
// Resolve children
|
|
471
|
+
const resolvedChildren = typeof children === "function" ? [children()] : Array.isArray(children) ? children : [children];
|
|
472
|
+
|
|
473
|
+
// Handle different mount targets
|
|
474
|
+
if (useShadow) {
|
|
475
|
+
if (typeof mount === "string") {
|
|
476
|
+
const target = document.querySelector(mount);
|
|
477
|
+
if (target) {
|
|
478
|
+
return portalToElementWithShadow(target, resolvedChildren);
|
|
479
|
+
}
|
|
480
|
+
} else if (mount) {
|
|
481
|
+
return portalToElementWithShadow(mount, resolvedChildren);
|
|
482
|
+
}
|
|
483
|
+
return portalWithShadow(resolvedChildren);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (typeof mount === "string") {
|
|
487
|
+
return portalToSelector(mount, resolvedChildren);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (mount) {
|
|
491
|
+
// For custom element mount, use selector approach
|
|
492
|
+
return portalToBody(resolvedChildren);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return portalToBody(resolvedChildren);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ============================================================================
|
|
499
|
+
// Store API (SolidJS-style)
|
|
500
|
+
// ============================================================================
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Creates a reactive store with nested property tracking (SolidJS-style)
|
|
504
|
+
* @template T
|
|
505
|
+
* @param {T} initialValue - Initial store state
|
|
506
|
+
* @returns {[T, SetStoreFunction<T>]} - [state proxy, setState function]
|
|
507
|
+
*
|
|
508
|
+
* @example
|
|
509
|
+
* const [state, setState] = createStore({ count: 0, user: { name: "John" } });
|
|
510
|
+
*
|
|
511
|
+
* // Read (reactive - tracks dependencies)
|
|
512
|
+
* state.count
|
|
513
|
+
* state.user.name
|
|
514
|
+
*
|
|
515
|
+
* // Update by path
|
|
516
|
+
* setState("count", 1);
|
|
517
|
+
* setState("user", "name", "Jane");
|
|
518
|
+
*
|
|
519
|
+
* // Functional update
|
|
520
|
+
* setState("count", c => c + 1);
|
|
521
|
+
*
|
|
522
|
+
* // Object merge at path
|
|
523
|
+
* setState("user", { name: "Jane", age: 30 });
|
|
524
|
+
*/
|
|
525
|
+
export function createStore(initialValue) {
|
|
526
|
+
// Store signals for each path
|
|
527
|
+
const signals = new Map();
|
|
528
|
+
// Deep clone the initial value to avoid mutation issues
|
|
529
|
+
const store = structuredClone(initialValue);
|
|
530
|
+
|
|
531
|
+
// Get or create a signal for a path
|
|
532
|
+
function getSignal(path) {
|
|
533
|
+
const key = path.join(".");
|
|
534
|
+
if (!signals.has(key)) {
|
|
535
|
+
const value = getValueAtPath(store, path);
|
|
536
|
+
signals.set(key, _createSignal(value));
|
|
537
|
+
}
|
|
538
|
+
return signals.get(key);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Get value at a path in an object
|
|
542
|
+
function getValueAtPath(obj, path) {
|
|
543
|
+
let current = obj;
|
|
544
|
+
for (const key of path) {
|
|
545
|
+
if (current == null) return undefined;
|
|
546
|
+
current = current[key];
|
|
547
|
+
}
|
|
548
|
+
return current;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Set value at a path in an object
|
|
552
|
+
function setValueAtPath(obj, path, value) {
|
|
553
|
+
if (path.length === 0) return;
|
|
554
|
+
let current = obj;
|
|
555
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
556
|
+
const key = path[i];
|
|
557
|
+
if (current[key] == null) {
|
|
558
|
+
// Preserve array vs object based on next key
|
|
559
|
+
const nextKey = path[i + 1];
|
|
560
|
+
current[key] = typeof nextKey === "number" || /^\d+$/.test(nextKey) ? [] : {};
|
|
561
|
+
}
|
|
562
|
+
current = current[key];
|
|
563
|
+
}
|
|
564
|
+
current[path[path.length - 1]] = value;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Notify all signals that might be affected by a path change
|
|
568
|
+
// Uses batch to ensure effects only run once even if multiple signals are updated
|
|
569
|
+
function notifyPath(path) {
|
|
570
|
+
const pathStr = path.join(".");
|
|
571
|
+
|
|
572
|
+
batchStart();
|
|
573
|
+
try {
|
|
574
|
+
for (const [key, signal] of signals.entries()) {
|
|
575
|
+
// Only notify:
|
|
576
|
+
// 1. The exact path that changed
|
|
577
|
+
// 2. Child paths (paths that start with the changed path)
|
|
578
|
+
// Do NOT notify parent paths - they didn't change (object reference is same)
|
|
579
|
+
if (key === pathStr || key.startsWith(pathStr + ".")) {
|
|
580
|
+
const signalPath = key.split(".");
|
|
581
|
+
const newValue = getValueAtPath(store, signalPath);
|
|
582
|
+
_set(signal, newValue);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
} finally {
|
|
586
|
+
batchEnd();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Create a proxy for reactive access
|
|
591
|
+
function createProxy(target, path = []) {
|
|
592
|
+
if (target === null || typeof target !== "object") {
|
|
593
|
+
return target;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return new Proxy(target, {
|
|
597
|
+
get(obj, prop) {
|
|
598
|
+
if (typeof prop === "symbol") {
|
|
599
|
+
return obj[prop];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const currentPath = [...path, prop];
|
|
603
|
+
const signal = getSignal(currentPath);
|
|
604
|
+
// Track dependency by reading the signal
|
|
605
|
+
_get(signal);
|
|
606
|
+
|
|
607
|
+
const value = obj[prop];
|
|
608
|
+
if (value !== null && typeof value === "object") {
|
|
609
|
+
return createProxy(value, currentPath);
|
|
610
|
+
}
|
|
611
|
+
return value;
|
|
612
|
+
},
|
|
613
|
+
|
|
614
|
+
set(obj, prop, value) {
|
|
615
|
+
// Direct assignment on proxy - update store and notify
|
|
616
|
+
const currentPath = [...path, prop];
|
|
617
|
+
obj[prop] = value;
|
|
618
|
+
notifyPath(currentPath);
|
|
619
|
+
return true;
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// setState function supporting path-based updates
|
|
625
|
+
function setState(...args) {
|
|
626
|
+
if (args.length === 0) return;
|
|
627
|
+
|
|
628
|
+
// Collect path segments and final value/updater
|
|
629
|
+
const path = [];
|
|
630
|
+
let i = 0;
|
|
631
|
+
|
|
632
|
+
// Collect string path segments
|
|
633
|
+
while (i < args.length - 1 && typeof args[i] === "string") {
|
|
634
|
+
path.push(args[i]);
|
|
635
|
+
i++;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const valueOrUpdater = args[i];
|
|
639
|
+
|
|
640
|
+
// If no path, treat as root update
|
|
641
|
+
if (path.length === 0 && typeof valueOrUpdater === "object" && valueOrUpdater !== null) {
|
|
642
|
+
// Merge at root
|
|
643
|
+
Object.assign(store, valueOrUpdater);
|
|
644
|
+
// Notify all signals
|
|
645
|
+
for (const [key, signal] of signals.entries()) {
|
|
646
|
+
const signalPath = key.split(".");
|
|
647
|
+
const newValue = getValueAtPath(store, signalPath);
|
|
648
|
+
_set(signal, newValue);
|
|
649
|
+
}
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Get current value at path
|
|
654
|
+
const currentValue = getValueAtPath(store, path);
|
|
655
|
+
|
|
656
|
+
// Determine new value
|
|
657
|
+
let newValue;
|
|
658
|
+
if (typeof valueOrUpdater === "function") {
|
|
659
|
+
newValue = valueOrUpdater(currentValue);
|
|
660
|
+
} else if (
|
|
661
|
+
Array.isArray(valueOrUpdater)
|
|
662
|
+
) {
|
|
663
|
+
// Arrays are replaced, not merged
|
|
664
|
+
newValue = valueOrUpdater;
|
|
665
|
+
} else if (
|
|
666
|
+
typeof valueOrUpdater === "object" &&
|
|
667
|
+
valueOrUpdater !== null &&
|
|
668
|
+
typeof currentValue === "object" &&
|
|
669
|
+
currentValue !== null &&
|
|
670
|
+
!Array.isArray(currentValue)
|
|
671
|
+
) {
|
|
672
|
+
// Merge objects (but not arrays)
|
|
673
|
+
newValue = { ...currentValue, ...valueOrUpdater };
|
|
674
|
+
} else {
|
|
675
|
+
newValue = valueOrUpdater;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Set value in store
|
|
679
|
+
setValueAtPath(store, path, newValue);
|
|
680
|
+
|
|
681
|
+
// Notify affected signals
|
|
682
|
+
notifyPath(path);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const proxy = createProxy(store);
|
|
686
|
+
return [proxy, setState];
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Produce helper for immer-style mutations (SolidJS-style)
|
|
691
|
+
* @template T
|
|
692
|
+
* @param {(draft: T) => void} fn - Mutation function
|
|
693
|
+
* @returns {(state: T) => T} - Function that applies mutations to a copy
|
|
694
|
+
*/
|
|
695
|
+
export function produce(fn) {
|
|
696
|
+
return (state) => {
|
|
697
|
+
const draft = structuredClone(state);
|
|
698
|
+
fn(draft);
|
|
699
|
+
return draft;
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Reconcile helper for efficient array/object updates (SolidJS-style)
|
|
705
|
+
* @template T
|
|
706
|
+
* @param {T} value - New value to reconcile
|
|
707
|
+
* @returns {(state: T) => T} - Function that returns the new value
|
|
708
|
+
*/
|
|
709
|
+
export function reconcile(value) {
|
|
710
|
+
return () => value;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Re-export unchanged APIs
|
|
714
|
+
export {
|
|
715
|
+
// Batch control
|
|
716
|
+
batchStart,
|
|
717
|
+
batchEnd,
|
|
718
|
+
batch,
|
|
719
|
+
// Cleanup
|
|
720
|
+
onCleanup,
|
|
721
|
+
// Owner/Root
|
|
722
|
+
createRoot,
|
|
723
|
+
getOwner,
|
|
724
|
+
runWithOwner,
|
|
725
|
+
hasOwner,
|
|
726
|
+
onMount,
|
|
727
|
+
// DOM API
|
|
728
|
+
text,
|
|
729
|
+
textDyn,
|
|
730
|
+
render,
|
|
731
|
+
mount,
|
|
732
|
+
show,
|
|
733
|
+
jsx,
|
|
734
|
+
jsxs,
|
|
735
|
+
Fragment,
|
|
736
|
+
createElement,
|
|
737
|
+
events,
|
|
738
|
+
forEach,
|
|
739
|
+
// Route definitions
|
|
740
|
+
routePage,
|
|
741
|
+
routePageTitled,
|
|
742
|
+
routePageFull,
|
|
743
|
+
createRouter,
|
|
744
|
+
routerNavigate,
|
|
745
|
+
routerReplace,
|
|
746
|
+
routerGetPath,
|
|
747
|
+
routerGetMatch,
|
|
748
|
+
routerGetBase,
|
|
749
|
+
// Context API
|
|
750
|
+
createContext,
|
|
751
|
+
provide,
|
|
752
|
+
useContext,
|
|
753
|
+
// Resource helpers (for direct access)
|
|
754
|
+
resourceGet,
|
|
755
|
+
resourcePeek,
|
|
756
|
+
resourceRefetch,
|
|
757
|
+
resourceIsPending,
|
|
758
|
+
resourceIsSuccess,
|
|
759
|
+
resourceIsFailure,
|
|
760
|
+
resourceValue,
|
|
761
|
+
resourceError,
|
|
762
|
+
stateIsPending,
|
|
763
|
+
stateIsSuccess,
|
|
764
|
+
stateIsFailure,
|
|
765
|
+
stateValue,
|
|
766
|
+
stateError,
|
|
767
|
+
// Portal API (low-level)
|
|
768
|
+
portalToBody,
|
|
769
|
+
portalToSelector,
|
|
770
|
+
portalWithShadow,
|
|
771
|
+
portalToElementWithShadow,
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
// Legacy API exports (for backwards compatibility during migration)
|
|
775
|
+
export {
|
|
776
|
+
_get as get,
|
|
777
|
+
_set as set,
|
|
778
|
+
_update as update,
|
|
779
|
+
_peek as peek,
|
|
780
|
+
_subscribe as subscribe,
|
|
781
|
+
_map as map,
|
|
782
|
+
_combine as combine,
|
|
783
|
+
_effect as effect,
|
|
784
|
+
runUntracked,
|
|
785
|
+
};
|