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