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 +1 -1
- package/app/main.js +12 -12
- package/app/routes/TestFor.js +33 -5
- package/app/routes/TestFor.ts +32 -5
- package/framework/core/signal.js +90 -61
- package/framework/router/router.js +4 -4
- package/framework/types/signal.d.ts +24 -2
- package/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Fusée Framework 🚀
|
|
2
|
-
**v1.4.
|
|
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',
|
package/app/routes/TestFor.js
CHANGED
|
@@ -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
|
-
<
|
|
26
|
-
|
|
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
|
|
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"
|
|
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>
|
package/app/routes/TestFor.ts
CHANGED
|
@@ -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
|
-
<
|
|
38
|
-
|
|
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
|
|
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"
|
|
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>
|
package/framework/core/signal.js
CHANGED
|
@@ -52,62 +52,8 @@ export function signal(initialValue) {
|
|
|
52
52
|
|
|
53
53
|
accessor.isSignal = true
|
|
54
54
|
|
|
55
|
-
if (Array.isArray(initialValue))
|
|
56
|
-
|
|
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
|
|
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