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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # Fusée Framework 🚀
2
- **v1.0.0 — Signals-First JS Framework | Atomic Reactivity | Peak Performance**
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 (linking the engine)
14
- npm link
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
- - [ ] Keyed `f-for` for list reconciliation
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
- let previousNodes = []
93
- let previousEffects = []
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, key: index, index }))
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, key: i, index: i }))
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
- for (const e of previousEffects) if (typeof e === 'function') e()
108
- for (const node of previousNodes) node.parentNode?.removeChild(node)
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
- previousNodes = []
111
- previousEffects = []
123
+ const key = keyExpr ? evaluateExpression(keyExpr, itemContext) : item.rawIndex
124
+ return { item, key, itemContext }
125
+ })
112
126
 
113
- const fragment = document.createDocumentFragment()
114
- items.forEach((item) => {
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
- childContext[itemName] = item.value
119
- if (keyName) childContext[keyName] = item.key
120
- if (indexName) childContext[indexName] = item.index
130
+ newItemsWithKeys.forEach(({ item, key, itemContext }, index) => {
131
+ let entry = cache.get(key)
121
132
 
122
- const childEffects = []
123
- compileNode(clone, childContext, components, childEffects)
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
- fragment.appendChild(clone)
126
- previousNodes.push(clone)
127
- previousEffects.push(...childEffects)
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
- anchor.parentNode.insertBefore(fragment, anchor)
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
- keys.push(k)
13
- const val = context[k]
14
- values.push(unwrapSignals && typeof val === 'function' && val.isSignal ? val() : val)
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
@@ -159,6 +159,7 @@
159
159
  <nav>
160
160
  <a href="#/">Home</a>
161
161
  <a href="#/about">About</a>
162
+ <a href="#/test-for">Test f-for</a>
162
163
  </nav>
163
164
  <div id="app"></div>
164
165
  <script type="module" src="./app/main.js"></script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fusee-framework",
3
- "version": "1.0.0",
3
+ "version": "1.0.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",
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,