fusee-framework 1.4.0 → 1.4.1

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  # Fusée Framework 🚀
2
- **v1.4.0 — Signals-First JS Framework | Atomic Reactivity | Peak Performance**
2
+ **v1.4.1 — Signals-First JS Framework | Atomic Reactivity | Peak Performance**
3
3
 
4
4
  Fusée is a custom, high-performance fine-grained reactive JavaScript framework built for speed and simplicity. It features a recursive, non-greedy compiler, a signals-based reactivity engine, Dependency Injection for components and a comprehensive CLI for instant application scaffolding.
5
5
 
package/app/main.js CHANGED
@@ -18,33 +18,33 @@ directive('highlight', {
18
18
  })
19
19
 
20
20
  const routes = [
21
- {
22
- path: '/',
21
+ {
22
+ path: '/',
23
23
  component: defineAsyncComponent({
24
24
  loader: () => import('./routes/Home.js'),
25
25
  loadingComponent: Loading
26
- })
26
+ })
27
27
  },
28
- {
29
- path: '/about',
28
+ {
29
+ path: '/about',
30
30
  component: defineAsyncComponent({
31
31
  loader: () => import('./routes/About.js'),
32
32
  loadingComponent: Loading
33
- })
33
+ })
34
34
  },
35
- {
36
- path: '/test-for',
35
+ {
36
+ path: '/test-for',
37
37
  component: defineAsyncComponent({
38
38
  loader: () => import('./routes/TestFor.js'),
39
39
  loadingComponent: Loading
40
- })
40
+ })
41
41
  },
42
- {
43
- path: '/dynamic',
42
+ {
43
+ path: '/dynamic',
44
44
  component: defineAsyncComponent({
45
45
  loader: () => import('./routes/TestDynamic.js'),
46
46
  loadingComponent: Loading
47
- })
47
+ })
48
48
  },
49
49
  {
50
50
  path: '/composables',
@@ -5,10 +5,27 @@ export const TestFor = defineComponent({
5
5
  { id: 2, name: 'Item 2' },
6
6
  { id: 3, name: 'Item 3' }
7
7
  ])
8
+ const search = signal('')
9
+
10
+ // ✨ NEW: Using the reactive .filter() method on the signal!
11
+ // This returns a Computed signal that updates automatically.
12
+ const filteredItems = items.filter(item =>
13
+ item.name.toLowerCase().includes(search().toLowerCase())
14
+ )
15
+
8
16
  inspect(items)
9
17
 
10
18
  return {
11
19
  items,
20
+ search,
21
+ filteredItems,
22
+ // 🔗 Chaining test: filter -> map -> join
23
+ namesSummary: filteredItems.map(i => i.name).join(', '),
24
+
25
+ updateItem: (id, newName) => {
26
+ // We update the SOURCE signal so the .filter() can see the new values!
27
+ items(items().map(i => i.id === id ? { ...i, name: newName } : i))
28
+ },
12
29
  addItem: () => {
13
30
  const nextId = Math.max(0, ...items().map(i => i.id)) + 1
14
31
  items.push({ id: nextId, name: `Item ${nextId}` })
@@ -20,17 +37,28 @@ export const TestFor = defineComponent({
20
37
  <div class="page">
21
38
  <h1>Keyed f-for Test</h1>
22
39
  <p>Focus an input below, then click "Shuffle" or "Add". The focus should stay!</p>
40
+ <p style="color: #8b8bff; background: #8b8bff11; padding: 10px; border-radius: 4px; border-left: 3px solid #8b8bff;">
41
+ 🔗 <strong>Reactive Summary:</strong> {{ namesSummary || 'No items found' }}
42
+ </p>
23
43
 
24
- <div style="margin-bottom: 20px; display: flex; gap: 10px;">
25
- <button @click="addItem" class="primary-btn">Add Item</button>
26
- <button @click="shuffleItems" class="primary-btn">Shuffle Items</button>
44
+ <div style="margin-bottom: 20px; display: flex; flex-direction: column; gap: 10px;">
45
+ <input type="text" f-model="search" placeholder="🔍 Filter items..."
46
+ style="padding: 10px; border-radius: 4px; border: 1px solid #444; background: #111; color: #fff; width: 100%;" />
47
+
48
+ <div style="display: flex; gap: 10px;">
49
+ <button @click="addItem" class="primary-btn">Add Item</button>
50
+ <button @click="shuffleItems" class="primary-btn">Shuffle Items</button>
51
+ </div>
27
52
  </div>
28
53
 
29
54
  <div style="display: flex; flex-direction: column; gap: 10px;">
30
- <div f-for="item in items" :key="item.id"
55
+ <div f-for="item in filteredItems" :key="item.id"
31
56
  style="padding: 10px; border: 1px solid #333; display: flex; gap: 10px; align-items: center;">
32
57
  <span>ID: {{ item.id }}</span>
33
- <input type="text" :value="item.name" placeholder="Type here..."
58
+ <input type="text"
59
+ :value="item.name"
60
+ @input="updateItem(item().id, $event.target.value)"
61
+ placeholder="Type here..."
34
62
  style="background: #111; color: #fff; border: 1px solid #444; padding: 5px;" />
35
63
  </div>
36
64
  </div>
@@ -5,6 +5,10 @@ type Item = {
5
5
 
6
6
  type TestForResult = {
7
7
  items: Signal<Item[]>
8
+ search: Signal<string>
9
+ filteredItems: Computed<Item[]>
10
+ namesSummary: Computed<string>
11
+ updateItem: (id: number, name: string) => void
8
12
  addItem: () => void
9
13
  shuffleItems: () => void
10
14
  template: string
@@ -17,9 +21,20 @@ export const TestFor = defineComponent({
17
21
  { id: 2, name: 'Item 2' },
18
22
  { id: 3, name: 'Item 3' }
19
23
  ])
24
+ const search = signal('')
25
+ const filteredItems = items.filter(i =>
26
+ i.name.toLowerCase().includes(search().toLowerCase())
27
+ )
28
+ const namesSummary = filteredItems.map(i => i.name).join(', ')
20
29
 
21
30
  return {
22
31
  items,
32
+ search,
33
+ filteredItems,
34
+ namesSummary,
35
+ updateItem: (id, newName) => {
36
+ items(items().map(i => i.id === id ? { ...i, name: newName } : i))
37
+ },
23
38
  addItem: () => {
24
39
  const nextIds = items().map(i => i.id)
25
40
  const nextId = nextIds.length > 0 ? Math.max(...nextIds) + 1 : 1
@@ -32,17 +47,29 @@ export const TestFor = defineComponent({
32
47
  <div class="page">
33
48
  <h1>Keyed f-for Test (TypeScript)</h1>
34
49
  <p>Focus an input below, then click "Shuffle" or "Add". The focus should stay!</p>
50
+
51
+ <p style="color: #8b8bff; background: #8b8bff11; padding: 10px; border-radius: 4px; border-left: 3px solid #8b8bff;">
52
+ 🔗 <strong>Reactive Summary:</strong> {{ namesSummary || 'No items found' }}
53
+ </p>
35
54
 
36
- <div style="margin-bottom: 20px; display: flex; gap: 10px;">
37
- <button @click="addItem" class="primary-btn">Add Item</button>
38
- <button @click="shuffleItems" class="primary-btn">Shuffle Items</button>
55
+ <div style="margin-bottom: 20px; display: flex; flex-direction: column; gap: 10px;">
56
+ <input type="text" f-model="search" placeholder="🔍 Filter items..."
57
+ style="padding: 10px; border-radius: 4px; border: 1px solid #444; background: #111; color: #fff; width: 100%;" />
58
+
59
+ <div style="display: flex; gap: 10px;">
60
+ <button @click="addItem" class="primary-btn">Add Item</button>
61
+ <button @click="shuffleItems" class="primary-btn">Shuffle Items</button>
62
+ </div>
39
63
  </div>
40
64
 
41
65
  <div style="display: flex; flex-direction: column; gap: 10px;">
42
- <div f-for="item in items" :key="item.id"
66
+ <div f-for="item in filteredItems" :key="item.id"
43
67
  style="padding: 10px; border: 1px solid #333; display: flex; gap: 10px; align-items: center;">
44
68
  <span>ID: {{ item.id }}</span>
45
- <input type="text" :value="item.name" placeholder="Type here..."
69
+ <input type="text"
70
+ :value="item.name"
71
+ @input="updateItem(item().id, $event.target.value)"
72
+ placeholder="Type here..."
46
73
  style="background: #111; color: #fff; border: 1px solid #444; padding: 5px;" />
47
74
  </div>
48
75
  </div>
@@ -52,62 +52,8 @@ export function signal(initialValue) {
52
52
 
53
53
  accessor.isSignal = true
54
54
 
55
- if (Array.isArray(initialValue)) {
56
- accessor.push = (...items) => {
57
- const next = [...accessor(), ...items]
58
- accessor(next)
59
- return next.length
60
- }
61
-
62
- accessor.pop = () => {
63
- const arr = accessor()
64
- if (arr.length === 0) return undefined
65
- const item = arr[arr.length - 1]
66
- accessor(arr.slice(0, -1))
67
- return item
68
- }
69
-
70
- accessor.shift = () => {
71
- const arr = accessor()
72
- if (arr.length === 0) return undefined
73
- const item = arr[0]
74
- accessor(arr.slice(1))
75
- return item
76
- }
77
-
78
- accessor.unshift = (...items) => {
79
- const next = [...items, ...accessor()]
80
- accessor(next)
81
- return next.length
82
- }
83
-
84
- accessor.splice = (start, deleteCount, ...items) => {
85
- const arr = [...accessor()]
86
- const removed = arr.splice(start, deleteCount, ...items)
87
- accessor(arr)
88
- return removed
89
- }
90
-
91
- accessor.remove = (predicate) => {
92
- accessor(accessor().filter((item, i) => !predicate(item, i)))
93
- }
94
-
95
- accessor.clear = () => {
96
- accessor([])
97
- }
98
-
99
- accessor.sort = (compareFn) => {
100
- const next = [...accessor()].sort(compareFn)
101
- accessor(next)
102
- return accessor
103
- }
104
-
105
- accessor.reverse = () => {
106
- const next = [...accessor()].reverse()
107
- accessor(next)
108
- return accessor
109
- }
110
- }
55
+ if (Array.isArray(initialValue)) addMutatingArrayMethods(accessor)
56
+ addReactiveArrayMethods(accessor)
111
57
 
112
58
  return accessor
113
59
  }
@@ -183,6 +129,9 @@ export function computed(fn) {
183
129
 
184
130
  accessor.isSignal = true
185
131
  accessor.readonly = true
132
+
133
+ addReactiveArrayMethods(accessor)
134
+
186
135
  return accessor
187
136
  }
188
137
 
@@ -196,11 +145,6 @@ export function untrack(fn) {
196
145
  }
197
146
  }
198
147
 
199
- /**
200
- * Reactive log helper similar to Svelte 5's $inspect.
201
- * Watches signals or functions and logs their current values on change.
202
- * Only active in development mode.
203
- */
204
148
  export function inspect(...args) {
205
149
  const isDev = typeof import.meta.env !== 'undefined' ? !!import.meta.env.DEV : true
206
150
  if (!isDev) return
@@ -215,3 +159,88 @@ export function inspect(...args) {
215
159
  console.log('[inspect]', ...values)
216
160
  })
217
161
  }
162
+
163
+ function addReactiveArrayMethods(accessor) {
164
+ // Transformations
165
+ accessor.map = (fn) => computed(() => accessor()?.map?.(fn) ?? [])
166
+ accessor.filter = (fn) => computed(() => accessor()?.filter?.(fn) ?? [])
167
+ accessor.slice = (...args) => computed(() => accessor()?.slice?.(...args) ?? [])
168
+ accessor.concat = (...args) => computed(() => accessor()?.concat?.(...args) ?? [])
169
+ accessor.flat = (depth) => computed(() => accessor()?.flat?.(depth) ?? [])
170
+ accessor.flatMap = (fn) => computed(() => accessor()?.flatMap?.(fn) ?? [])
171
+
172
+ // Searches
173
+ accessor.find = (fn) => computed(() => accessor()?.find?.(fn))
174
+ accessor.findLast = (fn) => computed(() => accessor()?.findLast?.(fn))
175
+ accessor.findIndex = (fn) => computed(() => accessor()?.findIndex?.(fn))
176
+ accessor.findLastIndex = (fn) => computed(() => accessor()?.findLastIndex?.(fn))
177
+ accessor.indexOf = (searchElement, fromIdx) => computed(() => accessor()?.indexOf?.(searchElement, fromIdx))
178
+ accessor.lastIndexOf = (searchElement, fromIdx) => computed(() => accessor()?.lastIndexOf?.(searchElement, fromIdx))
179
+ accessor.includes = (searchElement, fromIdx) => computed(() => accessor()?.includes?.(searchElement, fromIdx))
180
+
181
+ // Validations
182
+ accessor.every = (fn) => computed(() => accessor()?.every?.(fn))
183
+ accessor.some = (fn) => computed(() => accessor()?.some?.(fn))
184
+
185
+ // Accumulators & Access
186
+ accessor.reduce = (...args) => computed(() => accessor()?.reduce?.(...args))
187
+ accessor.at = (index) => computed(() => accessor()?.at?.(index))
188
+ accessor.join = (separator) => computed(() => accessor()?.join?.(separator) ?? '')
189
+ }
190
+
191
+ function addMutatingArrayMethods(accessor) {
192
+ accessor.push = (...items) => {
193
+ const next = [...accessor(), ...items]
194
+ accessor(next)
195
+ return next.length
196
+ }
197
+
198
+ accessor.pop = () => {
199
+ const arr = accessor()
200
+ if (arr.length === 0) return undefined
201
+ const item = arr[arr.length - 1]
202
+ accessor(arr.slice(0, -1))
203
+ return item
204
+ }
205
+
206
+ accessor.shift = () => {
207
+ const arr = accessor()
208
+ if (arr.length === 0) return undefined
209
+ const item = arr[0]
210
+ accessor(arr.slice(1))
211
+ return item
212
+ }
213
+
214
+ accessor.unshift = (...items) => {
215
+ const next = [...items, ...accessor()]
216
+ accessor(next)
217
+ return next.length
218
+ }
219
+
220
+ accessor.splice = (start, deleteCount, ...items) => {
221
+ const arr = [...accessor()]
222
+ const removed = arr.splice(start, deleteCount, ...items)
223
+ accessor(arr)
224
+ return removed
225
+ }
226
+
227
+ accessor.remove = (predicate) => {
228
+ accessor(accessor().filter((item, i) => !predicate(item, i)))
229
+ }
230
+
231
+ accessor.clear = () => {
232
+ accessor([])
233
+ }
234
+
235
+ accessor.sort = (compareFn) => {
236
+ const next = [...accessor()].sort(compareFn)
237
+ accessor(next)
238
+ return accessor
239
+ }
240
+
241
+ accessor.reverse = () => {
242
+ const next = [...accessor()].reverse()
243
+ accessor(next)
244
+ return accessor
245
+ }
246
+ }
@@ -22,9 +22,9 @@ export function createRouter(routes) {
22
22
  window.removeEventListener('hashchange', handler)
23
23
  window.removeEventListener('DOMContentLoaded', handler)
24
24
  if (_currentInstance) {
25
- try {
26
- _currentInstance.unmount()
27
- } catch {}
25
+ try {
26
+ _currentInstance.unmount()
27
+ } catch { }
28
28
  }
29
29
  _routes = []
30
30
  _outlet = null
@@ -58,7 +58,7 @@ function _resolve() {
58
58
  }
59
59
 
60
60
  if (_currentInstance) {
61
- try { _currentInstance.unmount() } catch {}
61
+ try { _currentInstance.unmount() } catch { }
62
62
  }
63
63
 
64
64
  _outlet.innerHTML = ''
@@ -6,7 +6,29 @@ export type SignalAccessor<T> = {
6
6
  isSignal: boolean;
7
7
  }
8
8
 
9
- export type ArraySignalAccessor<T extends any[]> = SignalAccessor<T> & {
9
+ export interface ReactiveArrayMethods<T> {
10
+ map<U>(callbackfn: (value: T, index: number, array: T[]) => U): Computed<U[]>;
11
+ filter(predicate: (value: T, index: number, array: T[]) => boolean): Computed<T[]>;
12
+ slice(start?: number, end?: number): Computed<T[]>;
13
+ reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): Computed<U>;
14
+ reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): Computed<T>;
15
+ find(predicate: (value: T, index: number, obj: T[]) => boolean): Computed<T | undefined>;
16
+ findLast(predicate: (value: T, index: number, obj: T[]) => boolean): Computed<T | undefined>;
17
+ findIndex(predicate: (value: T, index: number, obj: T[]) => boolean): Computed<number>;
18
+ findLastIndex(predicate: (value: T, index: number, obj: T[]) => boolean): Computed<number>;
19
+ every(predicate: (value: T, index: number, array: T[]) => boolean): Computed<boolean>;
20
+ some(predicate: (value: T, index: number, array: T[]) => boolean): Computed<boolean>;
21
+ includes(searchElement: T, fromIndex?: number): Computed<boolean>;
22
+ indexOf(searchElement: T, fromIndex?: number): Computed<number>;
23
+ lastIndexOf(searchElement: T, fromIndex?: number): Computed<number>;
24
+ at(index: number): Computed<T | undefined>;
25
+ concat(...items: (T | T[])[]): Computed<T[]>;
26
+ flat<depth extends number = 1>(depth?: depth): Computed<any[]>;
27
+ flatMap<U>(callback: (value: T, index: number, array: T[]) => U | U[]): Computed<U[]>;
28
+ join(separator?: string): Computed<string>;
29
+ }
30
+
31
+ export type ArraySignalAccessor<T extends any[]> = SignalAccessor<T> & ReactiveArrayMethods<T[number]> & {
10
32
  push(...items: T[number][]): number;
11
33
  pop(): T[number] | undefined;
12
34
  shift(): T[number] | undefined;
@@ -22,7 +44,7 @@ export type ComputedAccessor<T> = {
22
44
  (): T;
23
45
  isSignal: boolean;
24
46
  readonly: true;
25
- }
47
+ } & (T extends any[] ? ReactiveArrayMethods<T[number]> : {})
26
48
 
27
49
  export type Signal<T = any> = T extends any[] ? ArraySignalAccessor<T> : SignalAccessor<T>
28
50
  export type Computed<T = any> = ComputedAccessor<T>
package/index.html CHANGED
@@ -305,7 +305,7 @@
305
305
  <a href="#/composables" style="color: #34d399;">Composables</a>
306
306
  </nav>
307
307
  <div id="app"></div>
308
- <script type="module" src="./app/main.ts"></script>
308
+ <script type="module" src="./app/main.js"></script>
309
309
  </body>
310
310
 
311
311
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fusee-framework",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Fusée - Signals-first JS Framework | Peak Performance",
5
5
  "main": "framework/index.js",
6
6
  "types": "framework/types/index.d.ts",