fusee-framework 1.8.0 → 1.9.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 (49) hide show
  1. package/README.md +64 -3
  2. package/app/benchmark/vite.config.js +23 -0
  3. package/auto-imports.d.ts +4 -0
  4. package/framework/auto-import-preset.js +3 -1
  5. package/framework/core/async-context.js +27 -0
  6. package/framework/core/compiler/ast.js +167 -0
  7. package/framework/core/compiler/errors-list.js +47 -0
  8. package/framework/core/compiler/errors.js +107 -0
  9. package/framework/core/compiler/generator.js +363 -0
  10. package/framework/core/compiler/lexer.js +347 -0
  11. package/framework/core/compiler/main-compiler.js +115 -0
  12. package/framework/core/compiler/parser.js +414 -0
  13. package/framework/core/compiler/plugins/actions-plugin.js +263 -0
  14. package/framework/core/compiler/plugins/compiler-plugin.d.ts +5 -0
  15. package/framework/core/compiler/plugins/compiler-plugin.js +45 -0
  16. package/framework/core/compiler/plugins/file-router-plugin.js +301 -0
  17. package/framework/core/compiler/plugins/router-plugin.js +196 -0
  18. package/framework/core/compiler/transformer.js +398 -0
  19. package/framework/core/component.js +34 -10
  20. package/framework/core/evaluator.js +1 -1
  21. package/framework/core/h.js +430 -0
  22. package/framework/core/hydration.js +83 -0
  23. package/framework/core/signal.js +17 -15
  24. package/framework/server/actions.js +173 -0
  25. package/framework/server/actions.server.js +88 -0
  26. package/framework/server/index.js +18 -0
  27. package/framework/types/actions.d.ts +50 -0
  28. package/framework/types/actions.server.d.ts +36 -0
  29. package/framework/types/compiler/ast.d.ts +166 -0
  30. package/framework/types/compiler/errors-codes.d.ts +23 -0
  31. package/framework/types/compiler/errors.d.ts +43 -0
  32. package/framework/types/compiler/generator.d.ts +8 -0
  33. package/framework/types/compiler/h.d.ts +46 -0
  34. package/framework/types/compiler/lexer.d.ts +27 -0
  35. package/framework/types/compiler/main-compiler.d.ts +60 -0
  36. package/framework/types/compiler/parser.d.ts +4 -0
  37. package/framework/types/compiler/plugins/actions.d.ts +30 -0
  38. package/framework/types/compiler/plugins/compiler-plugin.d.ts +5 -0
  39. package/framework/types/compiler/plugins/file-router-plugins.d.ts +32 -0
  40. package/framework/types/compiler/plugins/router-plugin.d.ts +24 -0
  41. package/framework/types/compiler/transformer.d.ts +28 -0
  42. package/framework/types/hydration.d.ts +30 -0
  43. package/framework/types/index.d.ts +6 -0
  44. package/framework/types/server.d.ts +11 -0
  45. package/framework/types/signal.d.ts +2 -7
  46. package/ghid_go_fusee_integrat_go1_26.txt +276 -0
  47. package/ghid_rust_fusee_rust_1_95_0.txt +1118 -0
  48. package/package.json +44 -36
  49. package/vite.config.js +65 -53
@@ -0,0 +1,430 @@
1
+ import { effect, batch, signal } from '../core/signal.js'
2
+ import { createEventHandler, registerDelegatedEvent, isDelegatedEvent } from '../core/event-delegation.js'
3
+
4
+ export { effect as _effect, batch as _batch }
5
+
6
+ function isGetter(v) {
7
+ return typeof v === 'function' && !v.isSignal
8
+ }
9
+
10
+ const SENSITIVE_ATTRS = new Set(['href', 'src', 'srcset', 'formaction', 'xlink:href', 'data'])
11
+ const DANGEROUS_SCHEME = /^(javascript|data|vbscript|file):/i
12
+
13
+ function sanitize(name, value) {
14
+ if (SENSITIVE_ATTRS.has(name.toLowerCase())) {
15
+ const trimmed = String(value).trim()
16
+ if (DANGEROUS_SCHEME.test(trimmed)) {
17
+ console.warn(`[fusée] Blocked potential XSS on "${name}": "${value}"`)
18
+ return 'about:blank'
19
+ }
20
+ }
21
+ return value
22
+ }
23
+
24
+ export function h(tag, props = {}, children = [], isStatic = false) {
25
+ const el = document.createElement(tag)
26
+ const effects = []
27
+
28
+ if (!isStatic) {
29
+ _applyProps(el, props, effects)
30
+ } else {
31
+ _applyStaticProps(el, props)
32
+ }
33
+
34
+ const childNodes = _mountChildren(el, children, effects)
35
+
36
+ return {
37
+ node: el,
38
+ effects,
39
+ props,
40
+ children: childNodes,
41
+ _tag: tag,
42
+ }
43
+ }
44
+
45
+ export function hText(value) {
46
+ const node = document.createTextNode('')
47
+ const effects = []
48
+
49
+ if (typeof value === 'function') {
50
+ const cleanup = effect(() => {
51
+ node.textContent = String(value() ?? '')
52
+ })
53
+ effects.push(cleanup)
54
+ } else {
55
+ node.textContent = String(value ?? '')
56
+ }
57
+
58
+ return { node, effects }
59
+ }
60
+
61
+ export function hIf(branches) {
62
+ const anchor = document.createComment('f-if')
63
+ const effects = []
64
+
65
+ let currentNodes = []
66
+ let currentBranch = -1
67
+
68
+ const cleanup = effect(() => {
69
+ let matchIdx = -1
70
+ for (let i = 0; i < branches.length; i++) {
71
+ const [cond] = branches[i]
72
+ if (cond === null || cond()) { matchIdx = i; break }
73
+ }
74
+
75
+ if (matchIdx === currentBranch) return
76
+
77
+ _unmountNodes(currentNodes)
78
+ currentNodes = []
79
+ currentBranch = matchIdx
80
+
81
+ if (matchIdx === -1) return
82
+
83
+ const [, getChildren] = branches[matchIdx]
84
+ const children = getChildren()
85
+ const parent = anchor.parentNode
86
+
87
+ if (parent) {
88
+ for (const fnode of children) {
89
+ parent.insertBefore(fnode.node, anchor)
90
+ currentNodes.push(fnode)
91
+ }
92
+ }
93
+ })
94
+
95
+ effects.push(cleanup)
96
+
97
+ return {
98
+ node: anchor,
99
+ effects,
100
+ _ifBranchNodes: () => currentNodes,
101
+ _isIf: true,
102
+ }
103
+ }
104
+
105
+ export function hFor(sourceGetter, renderItem, keyFn) {
106
+ const anchor = document.createComment('f-for')
107
+ const effects = []
108
+
109
+ let keyedMap = new Map()
110
+ const keyEvaluatorSignal = signal(null)
111
+
112
+ const cleanup = effect(() => {
113
+ const list = sourceGetter() ?? []
114
+ const parent = anchor.parentNode
115
+
116
+ if (!parent) {
117
+ return
118
+ }
119
+
120
+ const newMap = new Map()
121
+ const newKeys = []
122
+
123
+ for (let i = 0; i < list.length; i++) {
124
+ const item = list[i]
125
+
126
+ keyEvaluatorSignal(item)
127
+ const key = keyFn(keyEvaluatorSignal, i)
128
+ newKeys.push(key)
129
+
130
+ const exists = keyedMap.get(key)
131
+ if (exists) {
132
+ if (exists.itemSignal() !== item) {
133
+ exists.itemSignal(item)
134
+ }
135
+ newMap.set(key, exists)
136
+ } else {
137
+ const itemSignal = signal(item)
138
+ const fnode = renderItem(itemSignal, i)
139
+ newMap.set(key, { fnode, key, itemSignal })
140
+ }
141
+ }
142
+
143
+ for (const [key, entry] of keyedMap) {
144
+ if (!newMap.has(key)) {
145
+ _unmountNodes([entry.fnode])
146
+ }
147
+ }
148
+
149
+ for (let i = newKeys.length - 1; i >= 0; i--) {
150
+ const key = newKeys[i]
151
+ const entry = newMap.get(key)
152
+ const ref = i === newKeys.length - 1 ? anchor : newMap.get(newKeys[i + 1])?.fnode?.node
153
+
154
+ if (ref && entry.fnode.node.nextSibling !== ref) {
155
+ parent.insertBefore(entry.fnode.node, ref ?? anchor)
156
+ } else if (!entry.fnode.node.parentNode) {
157
+ parent.insertBefore(entry.fnode.node, anchor)
158
+ }
159
+ }
160
+
161
+ keyedMap = newMap
162
+ })
163
+
164
+ effects.push(cleanup)
165
+
166
+ return {
167
+ node: anchor,
168
+ effects,
169
+ _isFor: true,
170
+ _getNodes: () => [...keyedMap.values()].map(e => e.fnode),
171
+ }
172
+ }
173
+
174
+ export function hSlot(slots, name, fallback = []) {
175
+ const anchor = document.createComment(`slot:${name}`)
176
+ const effects = []
177
+ const slotFn = slots?.[name]
178
+ const nodes = typeof slotFn === 'function' ? slotFn() : fallback
179
+ return {
180
+ node: anchor,
181
+ effects,
182
+ _slotNodes: nodes,
183
+ _isSlot: true,
184
+ }
185
+ }
186
+
187
+ export function createComponent(name, ComponentFn, rawProps = {}, rawSlots = {}) {
188
+ if (!ComponentFn) {
189
+ console.warn(`[fusée] Component "${name}" is not registered`)
190
+ const placeholder = document.createComment(`missing:${name}`)
191
+ return { node: placeholder, effects: [] }
192
+ }
193
+
194
+ const resolvedProps = _resolveComponentProps(rawProps)
195
+ const listeners = {}
196
+ const slots = {}
197
+
198
+ for (const [key, value] of Object.entries(rawProps)) {
199
+ if (key.startsWith('on:')) {
200
+ listeners[key.slice(3)] = value
201
+ }
202
+ }
203
+
204
+ for (const [slotName, slotFn] of Object.entries(rawSlots)) {
205
+ slots[slotName] = slotFn
206
+ }
207
+
208
+ const container = document.createElement('div')
209
+ container.setAttribute('data-fusee-component', name)
210
+
211
+ const api = ComponentFn(resolvedProps, { listeners, slots })
212
+ api.render(container)
213
+ const root = container.firstElementChild ?? container
214
+
215
+ const effects = []
216
+ effects.push(() => api.unmount())
217
+
218
+ return { node: root, effects, _component: api }
219
+ }
220
+
221
+ function _applyProps(el, props, effects) {
222
+ for (const [key, value] of Object.entries(props)) {
223
+ if (key.startsWith('@')) {
224
+ const eventName = key.slice(1)
225
+ const { handler, modifiers = [] } = value
226
+ const handlerState = { timeoutId: null, throttleTimeoutId: null, lastRun: 0 }
227
+ const wrappedHandler = createEventHandler(handler, modifiers, null, null, handlerState)
228
+
229
+ let cleanup
230
+ if (isDelegatedEvent(eventName)) {
231
+ cleanup = registerDelegatedEvent(el, eventName, wrappedHandler, { modifiers, handlerState })
232
+ } else {
233
+ el.addEventListener(eventName, wrappedHandler)
234
+ cleanup = () => el.removeEventListener(eventName, wrappedHandler)
235
+ }
236
+ effects.push(cleanup)
237
+ continue
238
+ }
239
+
240
+ if (key === 'f-show') {
241
+ const cleanup = effect(() => {
242
+ el.style.display = value() ? '' : 'none'
243
+ })
244
+ effects.push(cleanup)
245
+ continue
246
+ }
247
+
248
+ if (key === 'f-html') {
249
+ const cleanup = effect(() => {
250
+ el.innerHTML = String(value() ?? '')
251
+ })
252
+ effects.push(cleanup)
253
+ continue
254
+ }
255
+
256
+ if (key === 'f-model') {
257
+ const sig = value()
258
+ const cleanup = effect(() => {
259
+ const v = typeof sig === 'function' ? sig() : sig
260
+ if (el.value !== String(v ?? '')) el.value = String(v ?? '')
261
+ })
262
+ effects.push(cleanup)
263
+ continue
264
+ }
265
+
266
+ if (key === 'f-ref') {
267
+ el.dispatchEvent(new CustomEvent('fusee:ref', {
268
+ detail: { name: value },
269
+ bubbles: true,
270
+ }))
271
+ continue
272
+ }
273
+
274
+ if (key === 'f-once') continue
275
+
276
+ if (typeof value !== 'function') {
277
+ _applyAttr(el, key, value)
278
+ continue
279
+ }
280
+
281
+ const attrName = key
282
+ const getter = value
283
+ const cleanup = effect(() => {
284
+ _applyAttr(el, attrName, getter())
285
+ })
286
+ effects.push(cleanup)
287
+ }
288
+ }
289
+
290
+ function _applyStaticProps(el, props) {
291
+ for (const [key, value] of Object.entries(props)) {
292
+ if (key.startsWith('@') || key.startsWith('f-'))
293
+ continue
294
+ if (typeof value !== 'function')
295
+ _applyAttr(el, key, value)
296
+ }
297
+ }
298
+
299
+ function _applyAttr(el, name, value) {
300
+ if (name === 'class') {
301
+ const cls = _resolveClass(value)
302
+ if (cls) el.className = cls
303
+ else el.removeAttribute('class')
304
+ return
305
+ }
306
+
307
+ if (name === 'style') {
308
+ if (value !== null && typeof value === 'object') {
309
+ for (const k in value) el.style[k] = value[k]
310
+ } else {
311
+ el.style.cssText = String(value ?? '')
312
+ }
313
+ return
314
+ }
315
+
316
+ if (name in el && typeof el[name] !== 'undefined' && name !== 'type') {
317
+ try {
318
+ el[name] = value
319
+ return
320
+ } catch (_) {
321
+ return
322
+ }
323
+ }
324
+
325
+ if (typeof value === 'boolean') {
326
+ value ? el.setAttribute(name, '') : el.removeAttribute(name)
327
+ return
328
+ }
329
+
330
+ if (value == null) {
331
+ el.removeAttribute(name)
332
+ return
333
+ }
334
+
335
+ el.setAttribute(name, sanitize(name, String(value)))
336
+ }
337
+
338
+ function _resolveClass(value) {
339
+ if (typeof value === 'string')
340
+ return value
341
+ if (Array.isArray(value))
342
+ return value.filter(Boolean).join(' ')
343
+ if (value && typeof value === 'object')
344
+ return Object.entries(value).filter(([, v]) => !!v).map(([k]) => k).join(' ')
345
+ return String(value ?? '')
346
+ }
347
+
348
+ function _mountChildren(parent, children, parentEffects) {
349
+ const mounted = []
350
+
351
+ for (const fnode of children) {
352
+ if (!fnode || !fnode.node) continue
353
+
354
+ parent.appendChild(fnode.node)
355
+
356
+ if (fnode._isSlot && fnode._slotNodes) {
357
+ for (const sn of fnode._slotNodes) {
358
+ if (sn?.node) parent.insertBefore(sn.node, null)
359
+ }
360
+ }
361
+
362
+ if (fnode.effects) parentEffects.push(...fnode.effects)
363
+ mounted.push(fnode)
364
+ }
365
+
366
+ return mounted
367
+ }
368
+
369
+ function _unmountNodes(fnodes) {
370
+ for (const fnode of fnodes) {
371
+ if (!fnode) continue
372
+
373
+ if (fnode.effects) {
374
+ for (let i = fnode.effects.length - 1; i >= 0; i--) {
375
+ const cleanup = fnode.effects[i]
376
+ if (typeof cleanup === 'function') cleanup()
377
+ }
378
+ }
379
+
380
+ if (fnode.node?.parentNode) {
381
+ fnode.node.parentNode.removeChild(fnode.node)
382
+ }
383
+
384
+ if (fnode._ifBranchNodes) {
385
+ _unmountNodes(fnode._ifBranchNodes())
386
+ }
387
+
388
+ if (fnode._getNodes) {
389
+ _unmountNodes(fnode._getNodes())
390
+ }
391
+ }
392
+ }
393
+
394
+ function _resolveComponentProps(rawProps) {
395
+ const resolved = {}
396
+
397
+ for (const [key, value] of Object.entries(rawProps)) {
398
+ if (key.startsWith('on:')) continue
399
+
400
+ if (isGetter(value)) {
401
+ Object.defineProperty(resolved, key, {
402
+ get: value,
403
+ enumerable: true,
404
+ configurable: true,
405
+ })
406
+ } else {
407
+ resolved[key] = value
408
+ }
409
+ }
410
+
411
+ return resolved
412
+ }
413
+
414
+ export function mount(renderFn, ctx, components, container) {
415
+ const fnodes = renderFn(ctx, components)
416
+ const effects = []
417
+
418
+ for (const fnode of fnodes) {
419
+ if (!fnode?.node) continue
420
+ container.appendChild(fnode.node)
421
+ if (fnode.effects) effects.push(...fnode.effects)
422
+ }
423
+
424
+ return {
425
+ unmount() {
426
+ _unmountNodes(fnodes)
427
+ container.innerHTML = ''
428
+ }
429
+ }
430
+ }
@@ -0,0 +1,83 @@
1
+ const _hydrationRegistry = new Map()
2
+ const _registeredCaches = []
3
+
4
+ export function _registerResourceCache(resourceKey, cache) {
5
+ _registeredCaches.push({ resourceKey, cache })
6
+ }
7
+
8
+ export function extractHydrationData() {
9
+ const snapshot = {}
10
+
11
+ for (const { resourceKey, cache } of _registeredCaches) {
12
+ const entries = {}
13
+ for (const [cacheKey, entry] of cache) {
14
+ entries[cacheKey] = { data: entry.data, updatedAt: entry.updatedAt }
15
+ }
16
+ if (Object.keys(entries).length > 0) {
17
+ snapshot[resourceKey] = entries
18
+ }
19
+ }
20
+
21
+ return snapshot
22
+ }
23
+
24
+ export function dehydrate(snapshot = extractHydrationData()) {
25
+ try {
26
+ const json = JSON.stringify(snapshot)
27
+ return json.replace(/<\/script>/gi, '<\\/script>')
28
+ } catch (err) {
29
+ console.error('[fusée] dehydrate() failed to serialize hydration data:', err)
30
+ return '{}'
31
+ }
32
+ }
33
+
34
+ export function loadHydration(snapshot) {
35
+ if (!snapshot || typeof snapshot !== 'object') {
36
+ console.warn('[fusée] loadHydration() received invalid snapshot')
37
+ return
38
+ }
39
+
40
+ for (const [resourceKey, entries] of Object.entries(snapshot)) {
41
+ if (!_hydrationRegistry.has(resourceKey)) {
42
+ _hydrationRegistry.set(resourceKey, new Map())
43
+ }
44
+ const map = _hydrationRegistry.get(resourceKey)
45
+ for (const [cacheKey, entry] of Object.entries(entries)) {
46
+ map.set(cacheKey, { data: entry.data, updatedAt: entry.updatedAt })
47
+ }
48
+ }
49
+ }
50
+
51
+ export function hydrateFromWindow() {
52
+ if (typeof window === 'undefined') return
53
+ const raw = window.__FUSEE_HYDRATION__
54
+ if (raw) loadHydration(raw)
55
+ }
56
+
57
+ export function getHydratedEntries(resourceKey) {
58
+ return _hydrationRegistry.get(resourceKey) ?? null
59
+ }
60
+
61
+ export function isHydrationFresh(resourceKey, cacheKey, staleTime = 0) {
62
+ const entries = _hydrationRegistry.get(resourceKey)
63
+ if (!entries) return false
64
+ const entry = entries.get(cacheKey)
65
+ if (!entry) return false
66
+ return (Date.now() - entry.updatedAt) <= staleTime
67
+ }
68
+
69
+ export function clearHydration() {
70
+ _hydrationRegistry.clear()
71
+ _registeredCaches.length = 0
72
+ }
73
+
74
+ export function getHydrationSnapshot() {
75
+ const out = {}
76
+ for (const [resourceKey, map] of _hydrationRegistry) {
77
+ out[resourceKey] = {}
78
+ for (const [cacheKey, entry] of map) {
79
+ out[resourceKey][cacheKey] = { ...entry }
80
+ }
81
+ }
82
+ return out
83
+ }
@@ -1,3 +1,6 @@
1
+ import { getInFlightStore } from './async-context.js'
2
+ import { _registerResourceCache, getHydratedEntries } from './hydration.js'
3
+
1
4
  let currentEffect = null
2
5
  let currentOwner = null
3
6
  let onEffectCreated = null
@@ -431,9 +434,6 @@ function hasWatchChanged(newValue, oldValue, isMultiSource, equals) {
431
434
  return !equals(newValue, oldValue)
432
435
  }
433
436
 
434
- const globalInFlightByKey = new Map()
435
- const globalInFlightByFetcher = new WeakMap()
436
-
437
437
  export function resource(sourceOrFetcher, fetcherOrOptions, optionsObj) {
438
438
  let source = null
439
439
  let actualFetcher = null
@@ -462,9 +462,13 @@ export function resource(sourceOrFetcher, fetcherOrOptions, optionsObj) {
462
462
 
463
463
  let currentPromiseId = 0
464
464
  let currentPromise = null
465
- const cache = new Map()
466
465
  const staleTime = typeof options.staleTime === 'number' ? options.staleTime : 0
467
466
 
467
+ const resourceKey = options.key !== undefined ? String(options.key) : (actualFetcher.name || 'anonymous')
468
+ const hydratedEntries = getHydratedEntries(resourceKey)
469
+ const cache = hydratedEntries ? new Map(hydratedEntries) : new Map()
470
+ _registerResourceCache(resourceKey, cache)
471
+
468
472
  function serializeKey(input) {
469
473
  if (input === undefined) return 'undefined'
470
474
  if (input === null) return 'null'
@@ -509,32 +513,30 @@ export function resource(sourceOrFetcher, fetcherOrOptions, optionsObj) {
509
513
 
510
514
  let promise
511
515
 
512
- if (options.key !== undefined) {
513
- const dedupeKey = options.key ? `key_${options.key}_${key}` : `fn_${actualFetcher.name}_${key}`;
514
- promise = globalInFlightByKey.get(dedupeKey)
516
+ const { byKey, byFetcher } = getInFlightStore()
515
517
 
518
+ if (options.key !== undefined) {
519
+ const dedupeKey = `key_${options.key}_${key}`
520
+ promise = byKey.get(dedupeKey)
516
521
  if (!promise) {
517
522
  promise = makeScheduledPromise(() => actualFetcher(input))
518
- globalInFlightByKey.set(dedupeKey, promise)
519
-
523
+ byKey.set(dedupeKey, promise)
520
524
  promise.finally(() => {
521
- if (globalInFlightByKey.get(dedupeKey) === promise) {
522
- globalInFlightByKey.delete(dedupeKey)
525
+ if (byKey.get(dedupeKey) === promise) {
526
+ byKey.delete(dedupeKey)
523
527
  }
524
528
  }).catch(() => {})
525
529
  }
526
530
  } else {
527
- let inFlightMap = globalInFlightByFetcher.get(actualFetcher)
531
+ let inFlightMap = byFetcher.get(actualFetcher)
528
532
  if (!inFlightMap) {
529
533
  inFlightMap = new Map()
530
- globalInFlightByFetcher.set(actualFetcher, inFlightMap)
534
+ byFetcher.set(actualFetcher, inFlightMap)
531
535
  }
532
-
533
536
  promise = inFlightMap.get(key)
534
537
  if (!promise) {
535
538
  promise = makeScheduledPromise(() => actualFetcher(input))
536
539
  inFlightMap.set(key, promise)
537
-
538
540
  promise.finally(() => {
539
541
  if (inFlightMap.get(key) === promise) {
540
542
  inFlightMap.delete(key)