fusee-framework 1.0.0 → 1.0.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 +6 -7
- package/app/main.js +2 -0
- package/app/routes/TestFor.js +43 -0
- package/framework/core/directives.js +56 -22
- package/framework/core/evaluator.js +7 -4
- package/index.html +1 -0
- package/package.json +1 -1
- package/vite.config.js +0 -5
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Fusée Framework 🚀
|
|
2
|
-
**v1.0.
|
|
2
|
+
**v1.0.1 — Signals-First JS Framework | Atomic Reactivity | Peak Performance**
|
|
3
3
|
|
|
4
|
-
Fusée is a custom, high-performance reactive JavaScript framework built for speed and simplicity. It features a recursive, non-greedy compiler, a signals-based reactivity engine, and a comprehensive CLI for instant application scaffolding.
|
|
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, and a comprehensive CLI for instant application scaffolding.
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -10,8 +10,8 @@ Fusée is a custom, high-performance reactive JavaScript framework built for spe
|
|
|
10
10
|
The fastest way to get started with Fusée is via the CLI:
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
# 1. Install globally
|
|
14
|
-
npm
|
|
13
|
+
# 1. Install Fusée globally
|
|
14
|
+
npm install -g fusee-framework
|
|
15
15
|
|
|
16
16
|
# 2. Scaffold your first project
|
|
17
17
|
create-fusee-app .
|
|
@@ -70,7 +70,6 @@ export const Counter = defineComponent({
|
|
|
70
70
|
## 🚀 Future Roadmap
|
|
71
71
|
|
|
72
72
|
- [ ] Global Store Management
|
|
73
|
-
- [ ]
|
|
74
|
-
- [ ] Advanced router transitions support
|
|
73
|
+
- [ ] Advanced file-based routing support
|
|
75
74
|
|
|
76
|
-
Built with ❤️ by a forward-thinking JavaScript Architect.
|
|
75
|
+
Built with ❤️ by me, Sebi Somu, a forward-thinking JavaScript Architect.
|
package/app/main.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createRouter, mountOutlet } from '../framework/index.js'
|
|
2
2
|
import { Home } from './routes/Home.js'
|
|
3
3
|
import { About } from './routes/About.js'
|
|
4
|
+
import { TestFor } from './routes/TestFor.js'
|
|
4
5
|
|
|
5
6
|
createRouter([
|
|
6
7
|
{ path: '/', component: Home },
|
|
7
8
|
{ path: '/about', component: About },
|
|
9
|
+
{ path: '/test-for', component: TestFor },
|
|
8
10
|
])
|
|
9
11
|
|
|
10
12
|
const outlet = document.getElementById('app')
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { defineComponent, signal } from '../../framework/index.js'
|
|
2
|
+
|
|
3
|
+
export const TestFor = defineComponent({
|
|
4
|
+
setup() {
|
|
5
|
+
const items = signal([
|
|
6
|
+
{ id: 1, name: 'Item 1' },
|
|
7
|
+
{ id: 2, name: 'Item 2' },
|
|
8
|
+
{ id: 3, name: 'Item 3' }
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
items,
|
|
13
|
+
addItem: () => {
|
|
14
|
+
const nextId = Math.max(0, ...items().map(i => i.id)) + 1
|
|
15
|
+
items([...items(), { id: nextId, name: `Item ${nextId}` }])
|
|
16
|
+
},
|
|
17
|
+
shuffleItems: () => {
|
|
18
|
+
const shuffled = [...items()].sort(() => Math.random() - 0.5)
|
|
19
|
+
items(shuffled)
|
|
20
|
+
},
|
|
21
|
+
template: `
|
|
22
|
+
<div class="page">
|
|
23
|
+
<h1>Keyed f-for Test</h1>
|
|
24
|
+
<p>Focus an input below, then click "Shuffle" or "Add". The focus should stay!</p>
|
|
25
|
+
|
|
26
|
+
<div style="margin-bottom: 20px; display: flex; gap: 10px;">
|
|
27
|
+
<button @click="addItem" class="primary-btn">Add Item</button>
|
|
28
|
+
<button @click="shuffleItems" class="primary-btn">Shuffle Items</button>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div style="display: flex; flex-direction: column; gap: 10px;">
|
|
32
|
+
<div f-for="item in items" :key="item.id"
|
|
33
|
+
style="padding: 10px; border: 1px solid #333; display: flex; gap: 10px; align-items: center;">
|
|
34
|
+
<span>ID: {{ item.id }}</span>
|
|
35
|
+
<input type="text" :value="item.name" placeholder="Type here..."
|
|
36
|
+
style="background: #111; color: #fff; border: 1px solid #444; padding: 5px;" />
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
`
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
})
|
|
@@ -82,6 +82,12 @@ export function processFor(root, context, components, effects) {
|
|
|
82
82
|
const indexName = match[3]
|
|
83
83
|
const arrayExpr = match[5]
|
|
84
84
|
|
|
85
|
+
const keyExpr = el.getAttribute('data-bind-key') || el.getAttribute(':key')
|
|
86
|
+
if (keyExpr) {
|
|
87
|
+
el.removeAttribute('data-bind-key')
|
|
88
|
+
el.removeAttribute(':key')
|
|
89
|
+
}
|
|
90
|
+
|
|
85
91
|
const anchor = document.createComment('f-for')
|
|
86
92
|
const parent = el.parentNode
|
|
87
93
|
parent.insertBefore(anchor, el)
|
|
@@ -89,45 +95,73 @@ export function processFor(root, context, components, effects) {
|
|
|
89
95
|
const templateNode = el.cloneNode(true)
|
|
90
96
|
parent.removeChild(el)
|
|
91
97
|
|
|
92
|
-
|
|
93
|
-
let
|
|
98
|
+
// Cache of currently rendered items: Map<key, { node, effects, context }>
|
|
99
|
+
let cache = new Map()
|
|
100
|
+
let previousOrder = []
|
|
94
101
|
|
|
95
102
|
effects.push(effect(() => {
|
|
96
103
|
const rawItems = evaluateExpression(arrayExpr, context)
|
|
97
104
|
let items = []
|
|
98
105
|
|
|
99
106
|
if (Array.isArray(rawItems)) {
|
|
100
|
-
items = rawItems.map((item, index) => ({ value: item,
|
|
107
|
+
items = rawItems.map((item, index) => ({ value: item, rawIndex: index }))
|
|
101
108
|
} else if (typeof rawItems === 'number' && !isNaN(rawItems)) {
|
|
102
|
-
items = Array.from({ length: Math.max(0, Math.floor(rawItems)) }, (_, i) => ({ value: i + 1,
|
|
109
|
+
items = Array.from({ length: Math.max(0, Math.floor(rawItems)) }, (_, i) => ({ value: i + 1, rawIndex: i }))
|
|
103
110
|
} else if (rawItems && typeof rawItems === 'object') {
|
|
104
|
-
items = Object.entries(rawItems).map(([key, value], index) => ({ value, key, index }))
|
|
111
|
+
items = Object.entries(rawItems).map(([key, value], index) => ({ value, key, rawIndex: index }))
|
|
105
112
|
}
|
|
106
113
|
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
const nextCache = new Map()
|
|
115
|
+
const nextOrder = []
|
|
116
|
+
|
|
117
|
+
const newItemsWithKeys = items.map((item, index) => {
|
|
118
|
+
const itemContext = Object.create(context)
|
|
119
|
+
itemContext[itemName] = item.value
|
|
120
|
+
if (keyName) itemContext[keyName] = item.hasOwnProperty('key') ? item.key : item.rawIndex
|
|
121
|
+
if (indexName) itemContext[indexName] = index
|
|
109
122
|
|
|
110
|
-
|
|
111
|
-
|
|
123
|
+
const key = keyExpr ? evaluateExpression(keyExpr, itemContext) : item.rawIndex
|
|
124
|
+
return { item, key, itemContext }
|
|
125
|
+
})
|
|
112
126
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
const clone = templateNode.cloneNode(true)
|
|
116
|
-
const childContext = Object.create(context)
|
|
127
|
+
const parentNode = anchor.parentNode
|
|
128
|
+
let currentCursor = anchor.nextSibling
|
|
117
129
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (indexName) childContext[indexName] = item.index
|
|
130
|
+
newItemsWithKeys.forEach(({ item, key, itemContext }, index) => {
|
|
131
|
+
let entry = cache.get(key)
|
|
121
132
|
|
|
122
|
-
|
|
123
|
-
|
|
133
|
+
if (entry) {
|
|
134
|
+
entry.context[itemName] = item.value
|
|
135
|
+
if (keyName) entry.context[keyName] = item.hasOwnProperty('key') ? item.key : item.rawIndex
|
|
136
|
+
if (indexName) entry.context[indexName] = index
|
|
124
137
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
138
|
+
if (entry.node !== currentCursor) {
|
|
139
|
+
parentNode.insertBefore(entry.node, currentCursor)
|
|
140
|
+
} else {
|
|
141
|
+
currentCursor = currentCursor.nextSibling
|
|
142
|
+
}
|
|
143
|
+
cache.delete(key)
|
|
144
|
+
} else {
|
|
145
|
+
// Create new node
|
|
146
|
+
const clone = templateNode.cloneNode(true)
|
|
147
|
+
const childEffects = []
|
|
148
|
+
compileNode(clone, itemContext, components, childEffects)
|
|
149
|
+
|
|
150
|
+
parentNode.insertBefore(clone, currentCursor)
|
|
151
|
+
entry = { node: clone, effects: childEffects, context: itemContext }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
nextCache.set(key, entry)
|
|
155
|
+
nextOrder.push(entry.node)
|
|
128
156
|
})
|
|
129
157
|
|
|
130
|
-
|
|
158
|
+
for (const [key, entry] of cache.entries()) {
|
|
159
|
+
for (const e of entry.effects) if (typeof e === 'function') e()
|
|
160
|
+
entry.node.parentNode?.removeChild(entry.node)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
cache = nextCache
|
|
164
|
+
previousOrder = nextOrder
|
|
131
165
|
}))
|
|
132
166
|
}
|
|
133
167
|
}
|
|
@@ -6,12 +6,15 @@ const DANGEROUS_SCHEMES = /^(javascript|data|vbscript|file):/i
|
|
|
6
6
|
export function evaluateExpression(expr, context, extraContext = {}, unwrapSignals = true) {
|
|
7
7
|
const keys = []
|
|
8
8
|
const values = []
|
|
9
|
+
const identifiers = new Set(expr.match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g) || [])
|
|
9
10
|
|
|
10
|
-
// Map global context
|
|
11
|
+
// Map global context (only what is used in expression)
|
|
11
12
|
for (const k in context) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
if (identifiers.has(k)) {
|
|
14
|
+
keys.push(k)
|
|
15
|
+
const val = context[k]
|
|
16
|
+
values.push(unwrapSignals && typeof val === 'function' && val.isSignal ? val() : val)
|
|
17
|
+
}
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
// Map extra context (like $event)
|
package/index.html
CHANGED
package/package.json
CHANGED
package/vite.config.js
CHANGED
|
@@ -2,11 +2,8 @@ import { defineConfig } from 'vite'
|
|
|
2
2
|
import path from 'path'
|
|
3
3
|
|
|
4
4
|
export default defineConfig({
|
|
5
|
-
// Directorul rădăcină al proiectului
|
|
6
5
|
root: '.',
|
|
7
6
|
|
|
8
|
-
// Alias-uri pentru import-uri mai curate
|
|
9
|
-
// În loc de '../../framework/index.js' poți scrie '@fusee'
|
|
10
7
|
resolve: {
|
|
11
8
|
alias: {
|
|
12
9
|
'@fusee': path.resolve(__dirname, './framework'),
|
|
@@ -14,14 +11,12 @@ export default defineConfig({
|
|
|
14
11
|
}
|
|
15
12
|
},
|
|
16
13
|
|
|
17
|
-
// Config pentru server-ul de development
|
|
18
14
|
server: {
|
|
19
15
|
port: 3000,
|
|
20
16
|
open: true, // Deschide browserul automat
|
|
21
17
|
hmr: true, // Hot Module Replacement
|
|
22
18
|
},
|
|
23
19
|
|
|
24
|
-
// Config pentru build de producție
|
|
25
20
|
build: {
|
|
26
21
|
outDir: 'dist',
|
|
27
22
|
emptyOutDir: true,
|