fusee-framework 1.6.0 → 1.7.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.
Files changed (112) hide show
  1. package/README.md +29 -13
  2. package/app/benchmark/benchmark.js +633 -0
  3. package/app/benchmark/index.html +102 -0
  4. package/app/benchmark/styles.css +249 -0
  5. package/app/components/Counter.js +17 -7
  6. package/app/components/CounterSection.js +3 -4
  7. package/app/main.js +29 -62
  8. package/app/main.ts +72 -63
  9. package/app/pages/404.js +16 -0
  10. package/app/pages/[...all].js +35 -0
  11. package/app/pages/[...all].ts +35 -0
  12. package/app/pages/_layout.js +26 -0
  13. package/app/pages/_layout.ts +26 -0
  14. package/app/pages/about.js +23 -0
  15. package/app/pages/about.ts +29 -0
  16. package/app/pages/composables.js +158 -0
  17. package/app/pages/composables.ts +158 -0
  18. package/app/pages/dynamic.js +40 -0
  19. package/app/pages/dynamic.ts +40 -0
  20. package/app/pages/index.js +117 -0
  21. package/app/pages/index.ts +78 -0
  22. package/app/pages/login.ts +21 -0
  23. package/app/pages/objects/[id].js +56 -0
  24. package/app/pages/objects/[id].ts +56 -0
  25. package/app/pages/posts/[category]/[slug].js +56 -0
  26. package/app/pages/posts/[category]/[slug].ts +56 -0
  27. package/app/pages/router-example.js +56 -0
  28. package/app/pages/router-example.ts +56 -0
  29. package/app/pages/test-for.js +65 -0
  30. package/app/pages/test-for.ts +80 -0
  31. package/app/pages/users/[id].js +24 -0
  32. package/app/pages/users/[id].ts +46 -0
  33. package/app/pages/users/index.js +22 -0
  34. package/app/pages/users/index.ts +29 -0
  35. package/app/pages/users.js +43 -0
  36. package/app/pages/users.ts +78 -0
  37. package/app/routes/About.js +1 -1
  38. package/app/routes/About.ts +1 -1
  39. package/app/routes/AuthRoutes.ts +41 -0
  40. package/app/routes/Home.js +9 -0
  41. package/app/routes/NotFound.js +16 -0
  42. package/app/routes/NotFound.ts +16 -0
  43. package/app/routes/RouterExample.js +56 -0
  44. package/app/routes/RouterExample.ts +56 -0
  45. package/app/routes/WildcardDemo.js +35 -0
  46. package/app/routes/WildcardDemo.ts +35 -0
  47. package/auto-imports.d.ts +1 -0
  48. package/bin/create-fusee-linux +0 -0
  49. package/bin/create-fusee-mac-arm64 +0 -0
  50. package/bin/create-fusee-mac-intel +0 -0
  51. package/bin/create-fusee-win.exe +0 -0
  52. package/bin/create-fusee.exe +0 -0
  53. package/bin/index.js +38 -0
  54. package/create-fusee-go/cmd/generate.go +72 -0
  55. package/create-fusee-go/cmd/init.go +38 -0
  56. package/create-fusee-go/cmd/root.go +121 -0
  57. package/create-fusee-go/go.mod +10 -0
  58. package/create-fusee-go/go.sum +10 -0
  59. package/create-fusee-go/internal/assets/assets.go +78 -0
  60. package/create-fusee-go/internal/assets/embed/framework/auto-import-preset.js +9 -0
  61. package/create-fusee-go/internal/assets/embed/framework/core/compiler.js +253 -0
  62. package/create-fusee-go/internal/assets/embed/framework/core/component.js +266 -0
  63. package/create-fusee-go/internal/assets/embed/framework/core/composable.js +27 -0
  64. package/create-fusee-go/internal/assets/embed/framework/core/directives.js +875 -0
  65. package/create-fusee-go/internal/assets/embed/framework/core/evaluator.js +153 -0
  66. package/create-fusee-go/internal/assets/embed/framework/core/event-delegation.js +191 -0
  67. package/create-fusee-go/internal/assets/embed/framework/core/signal.js +412 -0
  68. package/create-fusee-go/internal/assets/embed/framework/core/store.js +81 -0
  69. package/create-fusee-go/internal/assets/embed/framework/index.d.ts +3 -0
  70. package/create-fusee-go/internal/assets/embed/framework/index.js +10 -0
  71. package/create-fusee-go/internal/assets/embed/framework/router/file-router.js +177 -0
  72. package/create-fusee-go/internal/assets/embed/framework/router/router.js +462 -0
  73. package/create-fusee-go/internal/assets/embed/framework/types/compiler.d.ts +15 -0
  74. package/create-fusee-go/internal/assets/embed/framework/types/component.d.ts +88 -0
  75. package/create-fusee-go/internal/assets/embed/framework/types/composable.d.ts +14 -0
  76. package/create-fusee-go/internal/assets/embed/framework/types/directives.d.ts +55 -0
  77. package/create-fusee-go/internal/assets/embed/framework/types/evaluator.d.ts +13 -0
  78. package/create-fusee-go/internal/assets/embed/framework/types/event-delegation.d.ts +32 -0
  79. package/create-fusee-go/internal/assets/embed/framework/types/index.d.ts +22 -0
  80. package/create-fusee-go/internal/assets/embed/framework/types/router.d.ts +32 -0
  81. package/create-fusee-go/internal/assets/embed/framework/types/signal.d.ts +79 -0
  82. package/create-fusee-go/internal/assets/embed/framework/types/store.d.ts +8 -0
  83. package/create-fusee-go/internal/assets/templates/Counter.tmpl +26 -0
  84. package/create-fusee-go/internal/assets/templates/component.tmpl +11 -0
  85. package/create-fusee-go/internal/assets/templates/components/Loading.tmpl +24 -0
  86. package/create-fusee-go/internal/assets/templates/index.html.tmpl +15 -0
  87. package/create-fusee-go/internal/assets/templates/main.tmpl +14 -0
  88. package/create-fusee-go/internal/assets/templates/package.json.tmpl +14 -0
  89. package/create-fusee-go/internal/assets/templates/page.tmpl +12 -0
  90. package/create-fusee-go/internal/assets/templates/pages/about.tmpl +20 -0
  91. package/create-fusee-go/internal/assets/templates/pages/index.tmpl +22 -0
  92. package/create-fusee-go/internal/assets/templates/pages/layout.tmpl +18 -0
  93. package/create-fusee-go/internal/assets/templates/tsconfig.json.tmpl +14 -0
  94. package/create-fusee-go/internal/assets/templates/vite.config.js.tmpl +18 -0
  95. package/create-fusee-go/main.go +7 -0
  96. package/framework/auto-import-preset.js +1 -1
  97. package/framework/core/compiler.js +252 -249
  98. package/framework/core/component.js +265 -260
  99. package/framework/core/directives.js +875 -786
  100. package/framework/core/evaluator.js +153 -70
  101. package/framework/core/event-delegation.js +33 -14
  102. package/framework/core/signal.js +412 -403
  103. package/framework/index.js +10 -11
  104. package/framework/router/file-router.js +177 -0
  105. package/framework/router/router.js +462 -80
  106. package/framework/types/event-delegation.d.ts +0 -58
  107. package/framework/types/router.d.ts +32 -15
  108. package/framework/types/signal.d.ts +1 -0
  109. package/index.html +638 -626
  110. package/package.json +5 -3
  111. package/tsconfig.json +1 -0
  112. /package/{framework → create-fusee-go/internal/assets/embed/framework}/bin/create-fusee.js +0 -0
@@ -1,403 +1,412 @@
1
- let currentEffect = null
2
- let onEffectCreated = null
3
-
4
- export function setEffectHook(fn) { onEffectCreated = fn }
5
-
6
- let batchDepth = 0
7
- const pendingEffects = new Set()
8
-
9
- export function batch(fn) {
10
- batchDepth++
11
- try {
12
- return fn()
13
- } finally {
14
- batchDepth--
15
- if (batchDepth === 0) {
16
- while (pendingEffects.size > 0) {
17
- const toRun = [...pendingEffects]
18
- pendingEffects.clear()
19
- for (const eff of toRun) eff()
20
- }
21
- }
22
- }
23
- }
24
-
25
- function scheduleEffect(effectFn) {
26
- if (batchDepth > 0) {
27
- pendingEffects.add(effectFn)
28
- } else {
29
- effectFn()
30
- }
31
- }
32
-
33
- export function signal(initialValue) {
34
- let value = initialValue
35
- const subscribers = new Set()
36
-
37
- function accessor() {
38
- if (arguments.length === 0) {
39
- if (currentEffect) {
40
- subscribers.add(currentEffect)
41
- currentEffect.deps.add(subscribers)
42
- }
43
- return value
44
- } else {
45
- const newValue = arguments[0]
46
- if (!Object.is(value, newValue)) {
47
- value = newValue
48
- for (const sub of [...subscribers]) scheduleEffect(sub)
49
- }
50
- }
51
- }
52
-
53
- accessor.isSignal = true
54
-
55
- if (Array.isArray(initialValue)) addMutatingArrayMethods(accessor)
56
- addReactiveArrayMethods(accessor)
57
-
58
- return accessor
59
- }
60
-
61
- export function effect(fn) {
62
- let active = true
63
- let running = false
64
- const nestedEffects = new Set()
65
-
66
- const run = () => {
67
- if (!active || running) {
68
- if (running) console.warn('[framework] Recursive effect detected')
69
- return
70
- }
71
-
72
- running = true
73
-
74
- for (const childCleanup of nestedEffects) {
75
- childCleanup()
76
- }
77
- nestedEffects.clear()
78
-
79
- for (const dep of run.deps) dep.delete(run)
80
- run.deps.clear()
81
-
82
- const prevEffect = currentEffect
83
- const prevOnEffectCreated = onEffectCreated
84
-
85
- onEffectCreated = (childCleanup) => {
86
- nestedEffects.add(childCleanup)
87
- if (prevOnEffectCreated) prevOnEffectCreated(childCleanup)
88
- }
89
-
90
- currentEffect = run
91
- try { fn() }
92
- finally {
93
- currentEffect = prevEffect
94
- onEffectCreated = prevOnEffectCreated
95
- running = false
96
- }
97
- }
98
-
99
- run.deps = new Set()
100
-
101
- const cleanup = () => {
102
- active = false
103
- pendingEffects.delete(run)
104
- for (const dep of run.deps) dep.delete(run)
105
- run.deps.clear()
106
-
107
- for (const childCleanup of nestedEffects) {
108
- childCleanup()
109
- }
110
- nestedEffects.clear()
111
- }
112
-
113
- if (onEffectCreated) onEffectCreated(cleanup)
114
- run()
115
-
116
- return cleanup
117
- }
118
-
119
- export function computed(fn) {
120
- let value
121
- let dirty = true
122
- let active = true
123
- const subscribers = new Set()
124
- const deps = new Set()
125
-
126
- const computedNode = () => {
127
- if (!active || dirty) return
128
-
129
- if (subscribers.size > 0) {
130
- if (!runRecompute()) return
131
- } else {
132
- dirty = true
133
- for (const d of deps) d.delete(computedNode)
134
- deps.clear()
135
- return
136
- }
137
-
138
- for (const sub of [...subscribers]) scheduleEffect(sub)
139
- }
140
-
141
- computedNode.deps = deps
142
-
143
- let computing = false
144
-
145
- function runRecompute() {
146
- if (computing) {
147
- console.warn('[framework] Circular dependency detected in computed()')
148
- return false
149
- }
150
-
151
- computing = true
152
- for (const d of deps) d.delete(computedNode)
153
- deps.clear()
154
-
155
- const prevEffect = currentEffect
156
- currentEffect = active ? computedNode : null
157
- try {
158
- const newValue = fn()
159
- const changed = !Object.is(newValue, value)
160
- value = newValue
161
- dirty = false
162
- return changed
163
- } finally {
164
- currentEffect = prevEffect
165
- computing = false
166
- }
167
- }
168
-
169
- function accessor() {
170
- if (arguments.length === 0) {
171
- if (active && currentEffect) {
172
- subscribers.add(currentEffect)
173
- currentEffect.deps.add(subscribers)
174
- }
175
-
176
- if (dirty) {
177
- runRecompute()
178
- }
179
-
180
- return value
181
- }
182
- console.warn('[framework] computed() is read-only')
183
- }
184
-
185
- accessor.dispose = () => {
186
- active = false
187
- dirty = true
188
- pendingEffects.delete(computedNode)
189
- for (const d of deps) d.delete(computedNode)
190
- deps.clear()
191
- subscribers.clear()
192
- }
193
-
194
- accessor.isSignal = true
195
- accessor.readonly = true
196
- addReactiveArrayMethods(accessor)
197
-
198
- return accessor
199
- }
200
-
201
- export function untrack(fn) {
202
- const prevEffect = currentEffect
203
- currentEffect = null
204
- try {
205
- return fn()
206
- } finally {
207
- currentEffect = prevEffect
208
- }
209
- }
210
-
211
- export function watch(source, callback, options = {}) {
212
- if (typeof callback !== 'function') {
213
- console.warn('[framework] watch() callback must be a function')
214
- return () => { }
215
- }
216
-
217
- const { immediate = false, equals = Object.is } = options
218
- const { getter, isMultiSource } = normalizeWatchSource(source)
219
-
220
- let initialized = false
221
- let oldValue
222
- const cleanupRef = { fn: null }
223
-
224
- const onCleanup = (fn) => {
225
- if (typeof fn !== 'function') {
226
- console.warn('[framework] onCleanup() expects a function')
227
- return
228
- }
229
- cleanupRef.fn = fn
230
- }
231
-
232
- const stopEffect = effect(() => {
233
- const newValue = getter()
234
-
235
- if (!initialized) {
236
- initialized = true
237
- oldValue = cloneWatchValue(newValue, isMultiSource)
238
- if (immediate) {
239
- untrack(() => callback(newValue, undefined, onCleanup))
240
- }
241
- return
242
- }
243
-
244
- if (!hasWatchChanged(newValue, oldValue, isMultiSource, equals)) {
245
- return
246
- }
247
-
248
- const previousValue = oldValue
249
- oldValue = cloneWatchValue(newValue, isMultiSource)
250
-
251
- runWatchCleanup(cleanupRef)
252
- untrack(() => callback(newValue, previousValue, onCleanup))
253
- })
254
-
255
- return () => {
256
- stopEffect()
257
- runWatchCleanup(cleanupRef)
258
- }
259
- }
260
-
261
- export function inspect(...args) {
262
- const isDev = typeof import.meta.env !== 'undefined' ? !!import.meta.env.DEV : true
263
- if (!isDev) return
264
-
265
- return effect(() => {
266
- const values = args.map(arg => {
267
- if (typeof arg === 'function') {
268
- return arg()
269
- }
270
- return arg
271
- })
272
- console.log('[inspect]', ...values)
273
- })
274
- }
275
-
276
- function addReactiveArrayMethods(accessor) {
277
- // Transformations
278
- accessor.map = (fn) => computed(() => accessor()?.map?.(fn) ?? [])
279
- accessor.filter = (fn) => computed(() => accessor()?.filter?.(fn) ?? [])
280
- accessor.slice = (...args) => computed(() => accessor()?.slice?.(...args) ?? [])
281
- accessor.concat = (...args) => computed(() => accessor()?.concat?.(...args) ?? [])
282
- accessor.flat = (depth) => computed(() => accessor()?.flat?.(depth) ?? [])
283
- accessor.flatMap = (fn) => computed(() => accessor()?.flatMap?.(fn) ?? [])
284
-
285
- // Searches
286
- accessor.find = (fn) => computed(() => accessor()?.find?.(fn))
287
- accessor.findLast = (fn) => computed(() => accessor()?.findLast?.(fn))
288
- accessor.findIndex = (fn) => computed(() => accessor()?.findIndex?.(fn))
289
- accessor.findLastIndex = (fn) => computed(() => accessor()?.findLastIndex?.(fn))
290
- accessor.indexOf = (searchElement, fromIdx) => computed(() => accessor()?.indexOf?.(searchElement, fromIdx))
291
- accessor.lastIndexOf = (searchElement, fromIdx) => computed(() => accessor()?.lastIndexOf?.(searchElement, fromIdx))
292
- accessor.includes = (searchElement, fromIdx) => computed(() => accessor()?.includes?.(searchElement, fromIdx))
293
-
294
- // Validations
295
- accessor.every = (fn) => computed(() => accessor()?.every?.(fn))
296
- accessor.some = (fn) => computed(() => accessor()?.some?.(fn))
297
-
298
- // Accumulators & Access
299
- accessor.reduce = (...args) => computed(() => accessor()?.reduce?.(...args))
300
- accessor.at = (index) => computed(() => accessor()?.at?.(index))
301
- accessor.join = (separator) => computed(() => accessor()?.join?.(separator) ?? '')
302
- }
303
-
304
- function addMutatingArrayMethods(accessor) {
305
- accessor.push = (...items) => {
306
- const next = [...accessor(), ...items]
307
- accessor(next)
308
- return next.length
309
- }
310
-
311
- accessor.pop = () => {
312
- const arr = accessor()
313
- if (arr.length === 0) return undefined
314
- const item = arr[arr.length - 1]
315
- accessor(arr.slice(0, -1))
316
- return item
317
- }
318
-
319
- accessor.shift = () => {
320
- const arr = accessor()
321
- if (arr.length === 0) return undefined
322
- const item = arr[0]
323
- accessor(arr.slice(1))
324
- return item
325
- }
326
-
327
- accessor.unshift = (...items) => {
328
- const next = [...items, ...accessor()]
329
- accessor(next)
330
- return next.length
331
- }
332
-
333
- accessor.splice = (start, deleteCount, ...items) => {
334
- const arr = [...accessor()]
335
- const removed = arr.splice(start, deleteCount, ...items)
336
- accessor(arr)
337
- return removed
338
- }
339
-
340
- accessor.remove = (predicate) => {
341
- accessor(accessor().filter((item, i) => !predicate(item, i)))
342
- }
343
-
344
- accessor.clear = () => {
345
- accessor([])
346
- }
347
-
348
- accessor.sort = (compareFn) => {
349
- const next = [...accessor()].sort(compareFn)
350
- accessor(next)
351
- return accessor
352
- }
353
-
354
- accessor.reverse = () => {
355
- const next = [...accessor()].reverse()
356
- accessor(next)
357
- return accessor
358
- }
359
- }
360
-
361
- function normalizeWatchSource(source) {
362
- if (Array.isArray(source)) {
363
- const getters = source.map(normalizeSingleWatchSource)
364
- return {
365
- getter: () => getters.map(get => get()),
366
- isMultiSource: true
367
- }
368
- }
369
- return {
370
- getter: normalizeSingleWatchSource(source),
371
- isMultiSource: false
372
- }
373
- }
374
-
375
- function normalizeSingleWatchSource(source) {
376
- if (typeof source === 'function') return source
377
- console.warn('[framework] watch() source should be a signal, getter, or an array of those')
378
- return () => source
379
- }
380
-
381
- function cloneWatchValue(value, isMultiSource) {
382
- if (isMultiSource && Array.isArray(value)) return value.slice()
383
- return value
384
- }
385
-
386
- function hasWatchChanged(newValue, oldValue, isMultiSource, equals) {
387
- if (isMultiSource) {
388
- if (!Array.isArray(oldValue) || newValue.length !== oldValue.length) return true
389
- for (let i = 0; i < newValue.length; i++) {
390
- if (!equals(newValue[i], oldValue[i])) return true
391
- }
392
- return false
393
- }
394
- return !equals(newValue, oldValue)
395
- }
396
-
397
- function runWatchCleanup(cleanupRef) {
398
- if (typeof cleanupRef.fn === 'function') {
399
- const fn = cleanupRef.fn
400
- cleanupRef.fn = null
401
- untrack(() => fn())
402
- }
403
- }
1
+ let currentEffect = null
2
+ let currentOwner = null
3
+ let onEffectCreated = null
4
+
5
+ export function setEffectHook(fn) { onEffectCreated = fn }
6
+
7
+ export function onCleanup(fn) {
8
+ if (!currentOwner) return
9
+ currentOwner.cleanups.add(fn)
10
+ }
11
+
12
+ let batchDepth = 0
13
+ const pendingEffects = new Set()
14
+
15
+ export function batch(fn) {
16
+ batchDepth++
17
+ try {
18
+ return fn()
19
+ } finally {
20
+ batchDepth--
21
+ if (batchDepth === 0) {
22
+ while (pendingEffects.size > 0) {
23
+ const toRun = [...pendingEffects]
24
+ pendingEffects.clear()
25
+ for (const eff of toRun) eff()
26
+ }
27
+ }
28
+ }
29
+ }
30
+
31
+ function scheduleEffect(effectFn) {
32
+ if (batchDepth > 0) {
33
+ pendingEffects.add(effectFn)
34
+ } else {
35
+ effectFn()
36
+ }
37
+ }
38
+
39
+ export function signal(initialValue) {
40
+ let value = initialValue
41
+ const subscribers = new Set()
42
+
43
+ function accessor() {
44
+ if (arguments.length === 0) {
45
+ if (currentEffect) {
46
+ subscribers.add(currentEffect)
47
+ currentEffect.deps.add(subscribers)
48
+ }
49
+ return value
50
+ } else {
51
+ const newValue = arguments[0]
52
+ if (!Object.is(value, newValue)) {
53
+ value = newValue
54
+ for (const sub of [...subscribers]) scheduleEffect(sub)
55
+ }
56
+ }
57
+ }
58
+
59
+ accessor.isSignal = true
60
+
61
+ if (Array.isArray(initialValue)) addMutatingArrayMethods(accessor)
62
+ addReactiveArrayMethods(accessor)
63
+
64
+ return accessor
65
+ }
66
+
67
+ export function effect(fn) {
68
+ let active = true
69
+ let running = false
70
+
71
+ const owner = {
72
+ children: new Set(),
73
+ cleanups: new Set()
74
+ }
75
+
76
+ const run = () => {
77
+ if (!active || running) {
78
+ if (running) console.warn('[framework] Recursive effect detected')
79
+ return
80
+ }
81
+
82
+ running = true
83
+
84
+ for (const dep of run.deps) dep.delete(run)
85
+ run.deps.clear()
86
+
87
+ const prevEffect = currentEffect
88
+ const prevOwner = currentOwner
89
+
90
+ currentEffect = run
91
+ currentOwner = owner
92
+
93
+ try { fn() }
94
+ finally {
95
+ currentEffect = prevEffect
96
+ currentOwner = prevOwner
97
+ running = false
98
+ }
99
+ }
100
+
101
+ run.deps = new Set()
102
+
103
+ const cleanup = () => {
104
+ if (!active) return
105
+ active = false
106
+
107
+ pendingEffects.delete(run)
108
+
109
+ for (const dep of run.deps) dep.delete(run)
110
+ run.deps.clear()
111
+
112
+ for (const child of owner.children) child()
113
+ owner.children.clear()
114
+
115
+ for (const fn of owner.cleanups) fn()
116
+ owner.cleanups.clear()
117
+ }
118
+
119
+ if (onEffectCreated) onEffectCreated(cleanup)
120
+ if (currentOwner) {
121
+ currentOwner.children.add(cleanup)
122
+ }
123
+
124
+ run()
125
+ return cleanup
126
+ }
127
+
128
+ export function computed(fn) {
129
+ let value
130
+ let dirty = true
131
+ let active = true
132
+ const subscribers = new Set()
133
+ const deps = new Set()
134
+
135
+ const computedNode = () => {
136
+ if (!active || dirty) return
137
+
138
+ if (subscribers.size > 0) {
139
+ if (!runRecompute()) return
140
+ } else {
141
+ dirty = true
142
+ for (const d of deps) d.delete(computedNode)
143
+ deps.clear()
144
+ return
145
+ }
146
+
147
+ for (const sub of [...subscribers]) scheduleEffect(sub)
148
+ }
149
+
150
+ computedNode.deps = deps
151
+
152
+ let computing = false
153
+
154
+ function runRecompute() {
155
+ if (computing) {
156
+ console.warn('[framework] Circular dependency detected in computed()')
157
+ return false
158
+ }
159
+
160
+ computing = true
161
+ for (const d of deps) d.delete(computedNode)
162
+ deps.clear()
163
+
164
+ const prevEffect = currentEffect
165
+ currentEffect = active ? computedNode : null
166
+ try {
167
+ const newValue = fn()
168
+ const changed = !Object.is(newValue, value)
169
+ value = newValue
170
+ dirty = false
171
+ return changed
172
+ } finally {
173
+ currentEffect = prevEffect
174
+ computing = false
175
+ }
176
+ }
177
+
178
+ function accessor() {
179
+ if (arguments.length === 0) {
180
+ if (active && currentEffect) {
181
+ subscribers.add(currentEffect)
182
+ currentEffect.deps.add(subscribers)
183
+ }
184
+
185
+ if (dirty) {
186
+ runRecompute()
187
+ }
188
+
189
+ return value
190
+ }
191
+ console.warn('[framework] computed() is read-only')
192
+ }
193
+
194
+ accessor.dispose = () => {
195
+ active = false
196
+ dirty = true
197
+ pendingEffects.delete(computedNode)
198
+ for (const d of deps) d.delete(computedNode)
199
+ deps.clear()
200
+ subscribers.clear()
201
+ }
202
+
203
+ accessor.isSignal = true
204
+ accessor.readonly = true
205
+ addReactiveArrayMethods(accessor)
206
+
207
+ return accessor
208
+ }
209
+
210
+ export function untrack(fn) {
211
+ const prevEffect = currentEffect
212
+ currentEffect = null
213
+ try {
214
+ return fn()
215
+ } finally {
216
+ currentEffect = prevEffect
217
+ }
218
+ }
219
+
220
+ export function watch(source, callback, options = {}) {
221
+ if (typeof callback !== 'function') {
222
+ console.warn('[framework] watch() callback must be a function')
223
+ return () => { }
224
+ }
225
+
226
+ const { immediate = false, equals = Object.is } = options
227
+ const { getter, isMultiSource } = normalizeWatchSource(source)
228
+
229
+ let initialized = false
230
+ let oldValue
231
+ const cleanupRef = { fn: null }
232
+
233
+ const onCleanup = (fn) => {
234
+ if (typeof fn !== 'function') {
235
+ console.warn('[framework] onCleanup() expects a function')
236
+ return
237
+ }
238
+ cleanupRef.fn = fn
239
+ }
240
+
241
+ const stopEffect = effect(() => {
242
+ const newValue = getter()
243
+
244
+ if (!initialized) {
245
+ initialized = true
246
+ oldValue = cloneWatchValue(newValue, isMultiSource)
247
+ if (immediate) {
248
+ untrack(() => callback(newValue, undefined, onCleanup))
249
+ }
250
+ return
251
+ }
252
+
253
+ if (!hasWatchChanged(newValue, oldValue, isMultiSource, equals)) {
254
+ return
255
+ }
256
+
257
+ const previousValue = oldValue
258
+ oldValue = cloneWatchValue(newValue, isMultiSource)
259
+
260
+ runWatchCleanup(cleanupRef)
261
+ untrack(() => callback(newValue, previousValue, onCleanup))
262
+ })
263
+
264
+ return () => {
265
+ stopEffect()
266
+ runWatchCleanup(cleanupRef)
267
+ }
268
+ }
269
+
270
+ export function inspect(...args) {
271
+ const isDev = typeof import.meta.env !== 'undefined' ? !!import.meta.env.DEV : true
272
+ if (!isDev) return
273
+
274
+ return effect(() => {
275
+ const values = args.map(arg => {
276
+ if (typeof arg === 'function') {
277
+ return arg()
278
+ }
279
+ return arg
280
+ })
281
+ console.log('[inspect]', ...values)
282
+ })
283
+ }
284
+
285
+ function addReactiveArrayMethods(accessor) {
286
+ // Transformations
287
+ accessor.map = (fn) => computed(() => accessor()?.map?.(fn) ?? [])
288
+ accessor.filter = (fn) => computed(() => accessor()?.filter?.(fn) ?? [])
289
+ accessor.slice = (...args) => computed(() => accessor()?.slice?.(...args) ?? [])
290
+ accessor.concat = (...args) => computed(() => accessor()?.concat?.(...args) ?? [])
291
+ accessor.flat = (depth) => computed(() => accessor()?.flat?.(depth) ?? [])
292
+ accessor.flatMap = (fn) => computed(() => accessor()?.flatMap?.(fn) ?? [])
293
+
294
+ // Searches
295
+ accessor.find = (fn) => computed(() => accessor()?.find?.(fn))
296
+ accessor.findLast = (fn) => computed(() => accessor()?.findLast?.(fn))
297
+ accessor.findIndex = (fn) => computed(() => accessor()?.findIndex?.(fn))
298
+ accessor.findLastIndex = (fn) => computed(() => accessor()?.findLastIndex?.(fn))
299
+ accessor.indexOf = (searchElement, fromIdx) => computed(() => accessor()?.indexOf?.(searchElement, fromIdx))
300
+ accessor.lastIndexOf = (searchElement, fromIdx) => computed(() => accessor()?.lastIndexOf?.(searchElement, fromIdx))
301
+ accessor.includes = (searchElement, fromIdx) => computed(() => accessor()?.includes?.(searchElement, fromIdx))
302
+
303
+ // Validations
304
+ accessor.every = (fn) => computed(() => accessor()?.every?.(fn))
305
+ accessor.some = (fn) => computed(() => accessor()?.some?.(fn))
306
+
307
+ // Accumulators & Access
308
+ accessor.reduce = (...args) => computed(() => accessor()?.reduce?.(...args))
309
+ accessor.at = (index) => computed(() => accessor()?.at?.(index))
310
+ accessor.join = (separator) => computed(() => accessor()?.join?.(separator) ?? '')
311
+ }
312
+
313
+ function addMutatingArrayMethods(accessor) {
314
+ accessor.push = (...items) => {
315
+ const next = [...accessor(), ...items]
316
+ accessor(next)
317
+ return next.length
318
+ }
319
+
320
+ accessor.pop = () => {
321
+ const arr = accessor()
322
+ if (arr.length === 0) return undefined
323
+ const item = arr[arr.length - 1]
324
+ accessor(arr.slice(0, -1))
325
+ return item
326
+ }
327
+
328
+ accessor.shift = () => {
329
+ const arr = accessor()
330
+ if (arr.length === 0) return undefined
331
+ const item = arr[0]
332
+ accessor(arr.slice(1))
333
+ return item
334
+ }
335
+
336
+ accessor.unshift = (...items) => {
337
+ const next = [...items, ...accessor()]
338
+ accessor(next)
339
+ return next.length
340
+ }
341
+
342
+ accessor.splice = (start, deleteCount, ...items) => {
343
+ const arr = [...accessor()]
344
+ const removed = arr.splice(start, deleteCount, ...items)
345
+ accessor(arr)
346
+ return removed
347
+ }
348
+
349
+ accessor.remove = (predicate) => {
350
+ accessor(accessor().filter((item, i) => !predicate(item, i)))
351
+ }
352
+
353
+ accessor.clear = () => {
354
+ accessor([])
355
+ }
356
+
357
+ accessor.sort = (compareFn) => {
358
+ const next = [...accessor()].sort(compareFn)
359
+ accessor(next)
360
+ return accessor
361
+ }
362
+
363
+ accessor.reverse = () => {
364
+ const next = [...accessor()].reverse()
365
+ accessor(next)
366
+ return accessor
367
+ }
368
+ }
369
+
370
+ function normalizeWatchSource(source) {
371
+ if (Array.isArray(source)) {
372
+ const getters = source.map(normalizeSingleWatchSource)
373
+ return {
374
+ getter: () => getters.map(get => get()),
375
+ isMultiSource: true
376
+ }
377
+ }
378
+ return {
379
+ getter: normalizeSingleWatchSource(source),
380
+ isMultiSource: false
381
+ }
382
+ }
383
+
384
+ function normalizeSingleWatchSource(source) {
385
+ if (typeof source === 'function') return source
386
+ console.warn('[framework] watch() source should be a signal, getter, or an array of those')
387
+ return () => source
388
+ }
389
+
390
+ function cloneWatchValue(value, isMultiSource) {
391
+ if (isMultiSource && Array.isArray(value)) return value.slice()
392
+ return value
393
+ }
394
+
395
+ function hasWatchChanged(newValue, oldValue, isMultiSource, equals) {
396
+ if (isMultiSource) {
397
+ if (!Array.isArray(oldValue) || newValue.length !== oldValue.length) return true
398
+ for (let i = 0; i < newValue.length; i++) {
399
+ if (!equals(newValue[i], oldValue[i])) return true
400
+ }
401
+ return false
402
+ }
403
+ return !equals(newValue, oldValue)
404
+ }
405
+
406
+ function runWatchCleanup(cleanupRef) {
407
+ if (typeof cleanupRef.fn === 'function') {
408
+ const fn = cleanupRef.fn
409
+ cleanupRef.fn = null
410
+ untrack(() => fn())
411
+ }
412
+ }