fusee-framework 1.5.0 → 1.5.2

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.5.0 — Signals-First JS Framework | Atomic Reactivity | Peak Performance**
2
+ **v1.5.2 — 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
 
@@ -80,23 +80,42 @@ export const Welcome = defineComponent({
80
80
 
81
81
  Fusée is built with a test-driven mindset to ensure the reliability of its reactivity engine and component lifecycle.
82
82
 
83
- - **Total Tests:** `200`
83
+ - **Total Tests:** `360`
84
84
  - **Status:** `All Passed`
85
85
  - **Framework:** [Vitest](https://vitest.dev/)
86
86
  - **Environment:** JSDOM (Browser simulation)
87
87
 
88
+ ### Running Tests
89
+
90
+ ```bash
91
+ # Run all tests (easiest)
92
+ npm test
93
+
94
+ # Run all tests with verbose output
95
+ npx vitest --reporter=verbose
96
+
97
+ # Run type tests only
98
+ npx vitest run --typecheck
99
+
100
+ # Run tests in watch mode
101
+ npx vitest
102
+ ```
103
+
88
104
  ### Test Coverage includes:
89
105
  * **Reactivity Engine:** 100% (Signals, Batching, Computed, Watchers).
90
106
  * **Component System:** Props, Slots, Async Components, and Lifecycle Hooks.
91
107
  * **Dependency Injection:** Nested Provide/Inject and Shadowing.
92
108
  * **Directives:** `f-if`, `f-for`, `f-model`, and Event Modifiers.
93
109
  * **Memory Safety:** Explicit checks for memory leaks and automatic effect disposal.
110
+ * **Type Safety:** Comprehensive TypeScript definitions and type tests.
111
+ * **Integration Tests:** Advanced module combinations and edge cases.
94
112
 
95
113
  ---
96
114
 
97
115
  ## 🚀 Future Roadmap
98
116
 
99
117
  - [ ] Advanced file-based routing support and state management
118
+ - [ ] SSR support
100
119
  - [ ] Lightweight compiler which transforms declarative HTML into optimized runtime instructions
101
120
  - [ ] Integrated backend development support
102
121
 
package/auto-imports.d.ts CHANGED
@@ -23,5 +23,4 @@ declare global {
23
23
  const signal: typeof import('fusee-framework').signal
24
24
  const untrack: typeof import('fusee-framework').untrack
25
25
  const watch: typeof import('fusee-framework').watch
26
- const watchEffect: typeof import('fusee-framework').watchEffect
27
26
  }
@@ -1,6 +1,6 @@
1
1
  export const FuseePreset = {
2
2
  'fusee-framework': [
3
- 'signal', 'computed', 'effect', 'watch', 'watchEffect',
3
+ 'signal', 'computed', 'effect', 'watch',
4
4
  'batch', 'untrack', 'inspect',
5
5
  'defineComponent', 'onMount', 'onUnmount',
6
6
  'directive',
@@ -37,7 +37,6 @@ export function mountTemplate(template, container, context, components) {
37
37
  return { effects }
38
38
  }
39
39
 
40
- // Parses props string from component mustache into HTML attr string
41
40
  function parseComponentAttrs(propsStr) {
42
41
  let attrs = ''
43
42
  if (!propsStr || !propsStr.trim()) return attrs
@@ -49,13 +48,10 @@ function parseComponentAttrs(propsStr) {
49
48
  const attrValue = attrMatch[2]
50
49
 
51
50
  if (attrName.startsWith('@')) {
52
- // Event listener: @change="handler" → data-on-change="handler"
53
51
  attrs += ` data-on-${attrName.slice(1).toLowerCase()}="${attrValue}"`
54
52
  } else if (attrName.startsWith(':')) {
55
- // Dynamic prop
56
53
  attrs += ` data-bind-prop-${attrName.slice(1).toLowerCase()}="${attrValue}"`
57
54
  } else {
58
- // Static prop
59
55
  attrs += ` data-prop-${attrName.toLowerCase()}="${attrValue}"`
60
56
  }
61
57
  }
@@ -196,7 +192,6 @@ function bindComponents(el, components, context, effects) {
196
192
  const ComponentFn = components[name]
197
193
  if (!ComponentFn) return
198
194
 
199
- // ── Props ────────────────────────────────────────────────────────────────
200
195
  const props = {}
201
196
  const listeners = {}
202
197
 
@@ -228,10 +223,7 @@ function bindComponents(el, components, context, effects) {
228
223
  }
229
224
 
230
225
  } else if (attrName.startsWith('data-on-')) {
231
- // ── Emit listeners ───────────────────────────────────────────────
232
- // @change="handler" → data-on-change="handler"
233
- // When child emits 'change', calls context.handler(value)
234
- const eventName = attrName.slice(8) // "data-on-" → eventName
226
+ const eventName = attrName.slice(8)
235
227
  const handlerExpr = attr.value.trim()
236
228
 
237
229
  listeners[eventName] = (...args) => {
@@ -239,15 +231,12 @@ function bindComponents(el, components, context, effects) {
239
231
  if (typeof handler === 'function') {
240
232
  handler(...args)
241
233
  } else {
242
- // Support inline expressions: @change="count(count() + 1)"
243
234
  evaluateExpression(handlerExpr, context)
244
235
  }
245
236
  }
246
237
  }
247
238
  }
248
239
 
249
- // ── Slots ────────────────────────────────────────────────────────────────
250
- // data-slot attribute contains URI-encoded slot HTML from parent
251
240
  const rawSlot = el.getAttribute('data-slot')
252
241
  const slots = rawSlot ? parseSlots(decodeURIComponent(rawSlot)) : {}
253
242
  el.removeAttribute('data-slot')
@@ -161,7 +161,6 @@ export function processFor(el, context, components, effects) {
161
161
  const templateNode = el.cloneNode(true)
162
162
  parent.removeChild(el)
163
163
 
164
- // Cache of currently rendered items: Map<key, { node, effects, context }>
165
164
  let cache = new Map()
166
165
  let previousOrder = []
167
166
 
@@ -197,7 +196,6 @@ export function processFor(el, context, components, effects) {
197
196
  let entry = cache.get(key)
198
197
 
199
198
  if (entry) {
200
- // Update existing reactive signals for reused items
201
199
  batch(() => {
202
200
  if (typeof entry.context[itemName] === 'function' && entry.context[itemName].isSignal) {
203
201
  entry.context[itemName](item.value)
@@ -217,7 +215,6 @@ export function processFor(el, context, components, effects) {
217
215
  }
218
216
  cache.delete(key)
219
217
  } else {
220
- // Create new item context with signals for local state
221
218
  const itemContext = Object.create(context)
222
219
  itemContext[itemName] = signal(item.value)
223
220
  if (keyName) itemContext[keyName] = signal(item.hasOwnProperty('key') ? item.key : item.rawIndex)
@@ -347,7 +344,6 @@ export function processIf(el, context, components, effects) {
347
344
  export function processIs(el, context, components, effects) {
348
345
  if (el.nodeType !== 1 || !el.hasAttribute('f-is')) return false
349
346
 
350
- // 1. Prepare directive state
351
347
  const expr = el.getAttribute('f-is')
352
348
  const keepAlive = processKeepAlive(el)
353
349
 
@@ -355,8 +351,6 @@ export function processIs(el, context, components, effects) {
355
351
 
356
352
  const anchor = document.createComment('f-keepAlive-anchor')
357
353
  el.parentNode.insertBefore(anchor, el)
358
-
359
- // Keep a clone to reuse attributes (props/@events) for every dynamic instance
360
354
  const templateNode = el.cloneNode(true)
361
355
  el.remove()
362
356
 
@@ -364,14 +358,11 @@ export function processIs(el, context, components, effects) {
364
358
  let activeKey = null
365
359
  let activeCleanup = []
366
360
 
367
- // 2. Reactive loop
368
361
  effects.push(effect(() => {
369
362
  const rawValue = evaluateExpression(expr, context)
370
363
 
371
- // Skip if same component is already active
372
364
  if (rawValue === activeKey) return
373
365
 
374
- // 3. Handle exiting component
375
366
  if (activeKey !== null) {
376
367
  const currentEntry = cache.get(activeKey)
377
368
  if (currentEntry) {
@@ -391,15 +382,12 @@ export function processIs(el, context, components, effects) {
391
382
  return
392
383
  }
393
384
 
394
- // 4. Handle entering component
395
385
  let entry = cache.get(rawValue)
396
386
 
397
387
  if (entry && keepAlive) {
398
- // Restore from cache
399
388
  anchor.parentNode.insertBefore(entry.node, anchor.nextSibling)
400
389
  activeCleanup = entry.cleanup
401
390
  } else {
402
- // Create new instance
403
391
  let ComponentFn = typeof rawValue === 'string' ? components[rawValue] : rawValue
404
392
 
405
393
  if (typeof ComponentFn !== 'function') {
@@ -418,7 +406,6 @@ export function processIs(el, context, components, effects) {
418
406
  const childCleanup = []
419
407
  const dynamicComponents = { ...components, [tempName]: ComponentFn }
420
408
 
421
- // Trigger the internal compiler to bind props and events
422
409
  compileNode(compEl, context, dynamicComponents, childCleanup)
423
410
 
424
411
  entry = { node: compEl, cleanup: childCleanup }
@@ -429,7 +416,6 @@ export function processIs(el, context, components, effects) {
429
416
  activeKey = rawValue
430
417
  }))
431
418
 
432
- // Cleanup all cached components when this directive is destroyed
433
419
  effects.push(() => {
434
420
  for (const entry of cache.values()) {
435
421
  for (const c of entry.cleanup) if (typeof c === 'function') c()
@@ -504,7 +490,7 @@ export function processEvents(el, context, effects) {
504
490
  if (next.endsWith('ms')) return parseInt(next) || 250
505
491
  if (next.endsWith('s')) return (parseFloat(next) || 0.25) * 1000
506
492
  }
507
- return 250 // Default
493
+ return 250
508
494
  }
509
495
 
510
496
  const debounceIdx = modifiers.indexOf('debounce')
@@ -529,7 +515,6 @@ export function processEvents(el, context, effects) {
529
515
  if (modifiers.includes('stop')) e.stopPropagation()
530
516
  if (modifiers.includes('self') && e.target !== el) return
531
517
 
532
- // Specific handling for common modifiers like .enter, .escape etc on keyboard events
533
518
  if (e instanceof KeyboardEvent) {
534
519
  const key = e.key.toLowerCase()
535
520
  const keyModifiers = modifiers.filter(m => ['enter', 'escape', 'tab', 'space', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(m))
@@ -543,7 +528,6 @@ export function processEvents(el, context, effects) {
543
528
  }
544
529
  }
545
530
 
546
- // Throttle logic (ensures execution at most once every X ms)
547
531
  if (throttleMs > 0) {
548
532
  const now = Date.now()
549
533
  const remaining = throttleMs - (now - lastRun)
@@ -553,7 +537,6 @@ export function processEvents(el, context, effects) {
553
537
  lastRun = now
554
538
  executeLogic(e)
555
539
  } else {
556
- // Standard throttle with trailing edge support
557
540
  clearTimeout(throttleTimeoutId)
558
541
  throttleTimeoutId = setTimeout(() => {
559
542
  lastRun = Date.now()
@@ -563,7 +546,6 @@ export function processEvents(el, context, effects) {
563
546
  return
564
547
  }
565
548
 
566
- // Debounce logic (waits for X ms of inactivity)
567
549
  if (debounceMs > 0) {
568
550
  clearTimeout(timeoutId)
569
551
  timeoutId = setTimeout(() => executeLogic(e), debounceMs)
@@ -7,11 +7,9 @@ export function evaluateExpression(expr, context, extraContext = {}, unwrapSigna
7
7
  const keys = []
8
8
  const values = []
9
9
 
10
- // Remove string literals before extracting identifiers to avoid matching words inside strings
11
10
  const exprWithoutStrings = expr.replace(/'[^']*'|"[^"]*"/g, ' ')
12
11
  const identifiers = new Set(exprWithoutStrings.match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g) || [])
13
12
 
14
- // Map global context (only what is used in expression)
15
13
  for (const k in context) {
16
14
  if (identifiers.has(k)) {
17
15
  keys.push(k)
@@ -20,7 +18,6 @@ export function evaluateExpression(expr, context, extraContext = {}, unwrapSigna
20
18
  }
21
19
  }
22
20
 
23
- // Map extra context (like $event)
24
21
  for (const k in extraContext) {
25
22
  if (!keys.includes(k)) {
26
23
  keys.push(k)
@@ -238,32 +238,6 @@ export function watch(source, callback, options = {}) {
238
238
  }
239
239
  }
240
240
 
241
- export function watchEffect(fn) {
242
- if (typeof fn !== 'function') {
243
- console.warn('[framework] watchEffect() expects a function')
244
- return () => { }
245
- }
246
-
247
- const cleanupRef = { fn: null }
248
- const onCleanup = (cleanup) => {
249
- if (typeof cleanup !== 'function') {
250
- console.warn('[framework] onCleanup() expects a function')
251
- return
252
- }
253
- cleanupRef.fn = cleanup
254
- }
255
-
256
- const stopEffect = effect(() => {
257
- runWatchCleanup(cleanupRef)
258
- fn(onCleanup)
259
- })
260
-
261
- return () => {
262
- stopEffect()
263
- runWatchCleanup(cleanupRef)
264
- }
265
- }
266
-
267
241
  export function inspect(...args) {
268
242
  const isDev = typeof import.meta.env !== 'undefined' ? !!import.meta.env.DEV : true
269
243
  if (!isDev) return
@@ -364,7 +338,6 @@ function addMutatingArrayMethods(accessor) {
364
338
  }
365
339
  }
366
340
 
367
- // Helpers for watch/watchEffect
368
341
  function normalizeWatchSource(source) {
369
342
  if (Array.isArray(source)) {
370
343
  const getters = source.map(normalizeSingleWatchSource)
@@ -1,5 +1,5 @@
1
1
  // ─── Framework Public API ─────────────────────────────────────────────────────
2
- export { signal, computed, effect, batch, untrack, inspect, watch, watchEffect } from './core/signal.js'
2
+ export { signal, computed, effect, batch, untrack, inspect, watch } from './core/signal.js'
3
3
  export { defineComponent, defineAsyncComponent, onMount, onUnmount, parseSlots, provide, inject, getCurrentInstance } from './core/component.js'
4
4
  export { createRouter, navigate, mountOutlet } from './router/router.js'
5
5
  export { mountTemplate } from './core/compiler.js'
@@ -12,16 +12,10 @@ export type PropConfig = {
12
12
  required?: boolean
13
13
  }
14
14
 
15
- // ── Emit ──────────────────────────────────────────────────────────────────────
16
- // emit('change', value) — calls parent listener registered with @change="handler"
17
15
  export type EmitFn = (eventName: string, ...args: any[]) => void
18
16
 
19
- // ── Slots ─────────────────────────────────────────────────────────────────────
20
- // slots.default — default slot HTML string
21
- // slots.header — named slot HTML string
22
17
  export type Slots = Record<string, string>
23
18
 
24
- // ── Setup context ─────────────────────────────────────────────────────────────
25
19
  export type SetupContext = {
26
20
  emit: EmitFn
27
21
  slots: Slots
@@ -42,8 +36,8 @@ export type ComponentResult = {
42
36
  [key: string]: any
43
37
  }
44
38
 
45
- export type ComponentOptions<TProps = ComponentProps> = {
46
- props?: PropSchema
39
+ export type ComponentOptions<TProps = ComponentProps, P = PropSchema> = {
40
+ props?: P
47
41
  components?: Record<string, ComponentFactory<any>>
48
42
  setup: (props: TProps, ctx: SetupContext) => ComponentResult
49
43
  }
@@ -62,13 +56,28 @@ export type ComponentApi = {
62
56
  export declare function onMount(fn: () => void): void
63
57
  export declare function onUnmount(fn: () => void): void
64
58
  export declare function parseSlots(slotHTML: string): Slots
59
+ export declare function getCurrentInstance(): ComponentInstance | null
65
60
 
66
- // ── Provide / Inject ───────────────────────────────────────────────────────────
67
61
  export declare function provide<T>(key: string, value: T): void
68
62
  export declare function inject<T>(key: string): T | null
69
63
 
70
- export declare function defineComponent<TProps = EmptyProps>(
71
- options: ComponentOptions<TProps>
64
+ type InferPropType<T> = T extends StringConstructor ? string :
65
+ T extends NumberConstructor ? number :
66
+ T extends BooleanConstructor ? boolean :
67
+ any;
68
+
69
+ type InferProps<P> = P extends string[]
70
+ ? Record<P[number], any>
71
+ : P extends Record<string, PropConfig>
72
+ ? {
73
+ [K in keyof P]: P[K]['required'] extends true
74
+ ? InferPropType<P[K]['type']>
75
+ : InferPropType<P[K]['type']> | (P[K] extends { default: any } ? never : undefined)
76
+ }
77
+ : Record<string, any>;
78
+
79
+ export declare function defineComponent<P extends PropSchema = any, TProps = InferProps<P>>(
80
+ options: ComponentOptions<TProps, P>
72
81
  ): ComponentFactory<TProps>
73
82
 
74
83
  export type AsyncComponentLoader<TProps = ComponentProps> = () => Promise<ComponentFactory<TProps> | { default: ComponentFactory<TProps> } | Record<string, any>>
@@ -10,5 +10,5 @@ declare global {
10
10
  }
11
11
  }
12
12
 
13
- export function defineComposable<T>(fn: (...args: any[]) => T): Composable<T>
14
- export function assertSetupContext(name?: string): any
13
+ export declare function defineComposable<T extends (...args: any[]) => any>(fn: T): T;
14
+ export declare function assertSetupContext(name?: string): void;
@@ -9,7 +9,5 @@ export type InterpolationPart = {
9
9
  export const MUSTACHE_RE: RegExp
10
10
 
11
11
  export function evaluateExpression(expr: string, context: Record<string, any>, extraContext?: Record<string, any>): any
12
-
13
12
  export function parseInterpolation(str: string): InterpolationPart[]
14
-
15
13
  export function sanitizeAttr(name: string, value: string): string
@@ -1,5 +1,3 @@
1
- // ─── Router Types ─────────────────────────────────────────────────────────────
2
-
3
1
  import type { ComponentApi } from './component'
4
2
 
5
3
  export type Route = {
@@ -1,5 +1,3 @@
1
- // ─── Signal Types ────────────────────────────────────────────────────────────
2
-
3
1
  export type SignalAccessor<T> = {
4
2
  (): T;
5
3
  (newValue: T): void;
@@ -46,8 +44,8 @@ export type ComputedAccessor<T> = {
46
44
  readonly: true;
47
45
  } & (T extends any[] ? ReactiveArrayMethods<T[number]> : {})
48
46
 
49
- export type Signal<T = any> = T extends any[] ? ArraySignalAccessor<T> : SignalAccessor<T>
50
- export type Computed<T = any> = ComputedAccessor<T>
47
+ export type Signal<T = any> = SignalAccessor<T> & (T extends any[] ? ReactiveArrayMethods<T[number]> : {})
48
+ export type Computed<T = any> = ComputedAccessor<T> & (T extends any[] ? ReactiveArrayMethods<T[number]> : {})
51
49
 
52
50
  type EffectRunner = {
53
51
  (): void;
@@ -74,7 +72,6 @@ export declare function watch<T extends any[]>(
74
72
  callback: (newValue: T, oldValue: T | undefined, onCleanup: (fn: () => void) => void) => void,
75
73
  options?: WatchOptions
76
74
  ): () => void
77
- export declare function watchEffect(fn: (onCleanup: (fn: () => void) => void) => void): () => void
78
75
  export declare function inspect(...args: any[]): (() => void) | void
79
76
 
80
77
  declare function setEffectHook(fn: (eff: any) => void): void
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fusee-framework",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
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/tsconfig.json CHANGED
@@ -24,6 +24,8 @@
24
24
  "declarationDir": "./dist/types",
25
25
 
26
26
  "resolveJsonModule": true,
27
+ "allowImportingTsExtensions": true,
28
+ "noEmit": true,
27
29
  "paths": {
28
30
  "fusee-framework": ["./framework/types/index.d.ts"],
29
31
  "fusee-framework/*": ["./framework/*"]
package/vite.config.js CHANGED
@@ -38,6 +38,10 @@ export default defineConfig({
38
38
 
39
39
  test: {
40
40
  environment: 'jsdom',
41
- globals: true
41
+ globals: true,
42
+ typecheck: {
43
+ enabled: true,
44
+ include: ['**/*.test-d.ts'],
45
+ }
42
46
  }
43
47
  })