fusee-framework 1.2.1 → 1.4.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.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Fusée Framework 🚀
2
- **v1.2.1 — Signals-First JS Framework | Atomic Reactivity | Peak Performance**
2
+ **v1.4.0 — 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
 
@@ -44,20 +44,29 @@ A typical Fusée project looks like this:
44
44
  ## 📝 Example Component
45
45
 
46
46
  ```javascript
47
- import { defineComponent, signal } from './framework/core/index.js';
48
-
49
- export const Counter = defineComponent({
47
+ export const Welcome = defineComponent({
50
48
  setup() {
49
+ const name = signal('World');
51
50
  const count = signal(0);
51
+ const double = computed(() => count() * 2);
52
+
52
53
  return {
54
+ name,
53
55
  count,
56
+ double,
54
57
  inc: () => count(count() + 1),
55
- dec: () => count(count() - 1),
56
58
  template: `
57
- <div class="counter-card">
58
- <h2>Counter: {{ count }}</h2>
59
- <button @click="dec">-</button>
60
- <button @click="inc">+</button>
59
+ <div class="card">
60
+ <h2>Hello, {{ name }}!</h2>
61
+ <input f-model="name" placeholder="Name" />
62
+
63
+ <button @click.debounce.300ms="inc">
64
+ Clicked {{ count }} times
65
+ </button>
66
+
67
+ <p f-if="count() > 0">
68
+ Double: <strong>{{ double }}</strong>
69
+ </p>
61
70
  </div>
62
71
  `
63
72
  };
@@ -71,5 +80,6 @@ export const Counter = defineComponent({
71
80
 
72
81
  - [ ] Global Store Management
73
82
  - [ ] Advanced file-based routing support
83
+ - [ ] Integrated lightweight compiler which transforms declarative HTML into optimized runtime instructions
74
84
 
75
85
  Built with ❤️ by me, Sebi Somu, a forward-thinking JavaScript Architect.
@@ -0,0 +1,24 @@
1
+ export const DynamicA = defineComponent({
2
+ props: ['title'],
3
+ setup(props) {
4
+ onMount(() => console.log('✅ DynamicA mounted'))
5
+ onUnmount(() => console.log('❌ DynamicA unmounted'))
6
+
7
+ const localValue = signal('')
8
+
9
+ return {
10
+ title: props.title,
11
+ localValue,
12
+ template: `
13
+ <div style="padding: 20px; background: #1e293b; border-radius: 8px; border: 2px solid #3b82f6;">
14
+ <h3 style="color: #3b82f6;">Component A</h3>
15
+ <p>Prop from parent: <strong>{{ title }}</strong></p>
16
+ <div style="margin-top: 10px;">
17
+ <label>Local state (persists with f-keepAlive):</label><br>
18
+ <input f-model="localValue" placeholder="Type something here..." style="background: #0f172a; color: white; border: 1px solid #3b82f6; padding: 5px; width: 100%;">
19
+ </div>
20
+ </div>
21
+ `
22
+ }
23
+ }
24
+ })
@@ -0,0 +1,24 @@
1
+ export const DynamicA = defineComponent({
2
+ props: ['title'],
3
+ setup(props: { title: string }) {
4
+ onMount(() => console.log('✅ DynamicA mounted'))
5
+ onUnmount(() => console.log('❌ DynamicA unmounted'))
6
+
7
+ const localValue = signal('')
8
+
9
+ return {
10
+ title: props.title,
11
+ localValue,
12
+ template: `
13
+ <div style="padding: 20px; background: #1e293b; border-radius: 8px; border: 2px solid #3b82f6;">
14
+ <h3 style="color: #3b82f6;">Component A</h3>
15
+ <p>Prop from parent: <strong>{{ title }}</strong></p>
16
+ <div style="margin-top: 10px;">
17
+ <label>Local state (persists with f-keepAlive):</label><br>
18
+ <input f-model="localValue" placeholder="Type something here..." style="background: #0f172a; color: white; border: 1px solid #3b82f6; padding: 5px; width: 100%;">
19
+ </div>
20
+ </div>
21
+ `
22
+ }
23
+ }
24
+ })
@@ -0,0 +1,24 @@
1
+ export const DynamicB = defineComponent({
2
+ props: ['title'],
3
+ setup(props) {
4
+ onMount(() => console.log('✅ DynamicB mounted'))
5
+ onUnmount(() => console.log('❌ DynamicB unmounted'))
6
+
7
+ const localInfo = signal('')
8
+
9
+ return {
10
+ title: props.title,
11
+ localInfo,
12
+ template: `
13
+ <div style="padding: 20px; background: #1e293b; border-radius: 8px; border: 2px solid #ec4899;">
14
+ <h3 style="color: #ec4899;">Component B</h3>
15
+ <p>Prop from parent: <strong>{{ title }}</strong></p>
16
+ <div style="margin-top: 10px;">
17
+ <label>Local info (B):</label><br>
18
+ <input f-model="localInfo" placeholder="B settings..." style="background: #1a1a1a; color: white; border: 1px solid #ec4899; padding: 5px; width: 100%;">
19
+ </div>
20
+ </div>
21
+ `
22
+ }
23
+ }
24
+ })
@@ -0,0 +1,24 @@
1
+ export const DynamicB = defineComponent({
2
+ props: ['title'],
3
+ setup(props: { title: string }) {
4
+ onMount(() => console.log('✅ DynamicB mounted'))
5
+ onUnmount(() => console.log('❌ DynamicB unmounted'))
6
+
7
+ const localInfo = signal('')
8
+
9
+ return {
10
+ title: props.title,
11
+ localInfo,
12
+ template: `
13
+ <div style="padding: 20px; background: #1e293b; border-radius: 8px; border: 2px solid #ec4899;">
14
+ <h3 style="color: #ec4899;">Component B</h3>
15
+ <p>Prop from parent: <strong>{{ title }}</strong></p>
16
+ <div style="margin-top: 10px;">
17
+ <label>Local info (B):</label><br>
18
+ <input f-model="localInfo" placeholder="B settings..." style="background: #1a1a1a; color: white; border: 1px solid #ec4899; padding: 5px; width: 100%;">
19
+ </div>
20
+ </div>
21
+ `
22
+ }
23
+ }
24
+ })
@@ -2,14 +2,13 @@ export const EmitTest = defineComponent({
2
2
  setup(props, { emit }) {
3
3
  const clickCount = signal(0)
4
4
  const lastEvent = signal('None')
5
-
5
+
6
6
  inspect(clickCount, lastEvent)
7
-
7
+
8
8
  return {
9
9
  clickCount,
10
10
  lastEvent,
11
-
12
- // Butoane pentru test emit
11
+
13
12
  emitSimple: () => {
14
13
  clickCount(clickCount() + 1)
15
14
  emit('simple-click')
@@ -22,7 +21,7 @@ export const EmitTest = defineComponent({
22
21
  lastEvent(`click-with-data: ${count}`)
23
22
  },
24
23
  emitObject: () => {
25
- const data = {
24
+ const data = {
26
25
  timestamp: Date.now(),
27
26
  message: 'Hello from child!',
28
27
  counter: clickCount() + 1
@@ -31,7 +30,7 @@ export const EmitTest = defineComponent({
31
30
  emit('object-event', data)
32
31
  lastEvent(`object-event: ${data.message}`)
33
32
  },
34
-
33
+
35
34
  template: `
36
35
  <div style="background: linear-gradient(135deg, #1e3a5f 0%, #0d1b2a 100%); border: 2px solid #4da6ff; border-radius: 12px; padding: 24px; margin: 20px 0; color: #fff;">
37
36
  <h3 style="margin: 0 0 16px 0; color: #4da6ff;">🧪 EMIT EVENTS TEST LAB</h3>
@@ -0,0 +1,26 @@
1
+ import { defineComponent } from '../../framework/index.js'
2
+
3
+ export const Loading = defineComponent({
4
+ setup() {
5
+ return {
6
+ template: `
7
+ <div style="display: flex; justify-content: center; align-items: center; padding: 2rem;">
8
+ <div style="
9
+ width: 40px;
10
+ height: 40px;
11
+ border: 4px solid #f3f3f3;
12
+ border-top: 4px solid #3b82f6;
13
+ border-radius: 50%;
14
+ animation: spin 1s linear infinite;
15
+ "></div>
16
+ <style>
17
+ @keyframes spin {
18
+ 0% { transform: rotate(0deg); }
19
+ 100% { transform: rotate(360deg); }
20
+ }
21
+ </style>
22
+ </div>
23
+ `
24
+ }
25
+ }
26
+ })
@@ -0,0 +1,26 @@
1
+ import { defineComponent } from '../../framework/index.js'
2
+
3
+ export const Loading = defineComponent({
4
+ setup() {
5
+ return {
6
+ template: `
7
+ <div style="display: flex; justify-content: center; align-items: center; padding: 2rem;">
8
+ <div style="
9
+ width: 40px;
10
+ height: 40px;
11
+ border: 4px solid #f3f3f3;
12
+ border-top: 4px solid #3b82f6;
13
+ border-radius: 50%;
14
+ animation: spin 1s linear infinite;
15
+ "></div>
16
+ <style>
17
+ @keyframes spin {
18
+ 0% { transform: rotate(0deg); }
19
+ 100% { transform: rotate(360deg); }
20
+ }
21
+ </style>
22
+ </div>
23
+ `
24
+ }
25
+ }
26
+ })
@@ -0,0 +1,29 @@
1
+ import { defineComposable, assertSetupContext, signal, computed, onMount, onUnmount } from '../../framework/index.js'
2
+
3
+ // ─── counterLogic ─────────────────────────────────────────────────────────────
4
+ export const counterLogic = defineComposable((initial = 0, step = 1) => {
5
+ const count = signal(initial)
6
+ const doubled = computed(() => count() * 2)
7
+ const isEven = computed(() => count() % 2 === 0)
8
+
9
+ const increment = () => count(count() + step)
10
+ const decrement = () => count(count() - step)
11
+ const reset = () => count(initial)
12
+
13
+ return { count, doubled, isEven, increment, decrement, reset }
14
+ })
15
+
16
+ // ─── mouseTracker ─────────────────────────────────────────────────────────────
17
+ export const mouseTracker = defineComposable(() => {
18
+ assertSetupContext('mouseTracker')
19
+
20
+ const x = signal(0)
21
+ const y = signal(0)
22
+
23
+ const onMove = (e) => { x(e.clientX); y(e.clientY) }
24
+
25
+ onMount(() => window.addEventListener('mousemove', onMove))
26
+ onUnmount(() => window.removeEventListener('mousemove', onMove))
27
+
28
+ return { x, y }
29
+ })
@@ -0,0 +1,30 @@
1
+ import { defineComposable, assertSetupContext, signal, computed, onMount, onUnmount } from '../../framework/index.js'
2
+
3
+ export const counterLogic = defineComposable((initial: number = 0, step: number = 1) => {
4
+ const count = signal(initial)
5
+ const doubled = computed(() => count() * 2)
6
+ const isEven = computed(() => count() % 2 === 0)
7
+
8
+ const increment = () => count(count() + step)
9
+ const decrement = () => count(count() - step)
10
+ const reset = () => count(initial)
11
+
12
+ return { count, doubled, isEven, increment, decrement, reset }
13
+ })
14
+
15
+ export const mouseTracker = defineComposable(() => {
16
+ assertSetupContext('mouseTracker')
17
+
18
+ const x = signal(0)
19
+ const y = signal(0)
20
+
21
+ const onMove = (e: MouseEvent) => {
22
+ x(e.clientX)
23
+ y(e.clientY)
24
+ }
25
+
26
+ onMount(() => window.addEventListener('mousemove', onMove))
27
+ onUnmount(() => window.removeEventListener('mousemove', onMove))
28
+
29
+ return { x, y }
30
+ })
@@ -0,0 +1,53 @@
1
+ import { defineComposable, signal, effect } from '../../framework/index.js'
2
+
3
+ /**
4
+ * @template T
5
+ * @param {string | (() => string)} urlOrSignal
6
+ * @param {{ immediate?: boolean, transform?: (raw: any) => T }} [options]
7
+ */
8
+ export const useFetch = defineComposable((urlOrSignal, options = {}) => {
9
+ const { immediate = true, transform = null } = options
10
+
11
+ const data = signal(null)
12
+ const loading = signal(false)
13
+ const error = signal(null)
14
+
15
+ let controller = null
16
+
17
+ const execute = () => {
18
+ const url = typeof urlOrSignal === 'function' ? urlOrSignal() : urlOrSignal
19
+ if (!url) return
20
+
21
+ if (controller) controller.abort()
22
+ controller = new AbortController()
23
+
24
+ loading(true)
25
+ error(null)
26
+
27
+ fetch(url, { signal: controller.signal })
28
+ .then(res => {
29
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
30
+ return res.json()
31
+ })
32
+ .then(raw => {
33
+ data(transform ? transform(raw) : raw)
34
+ loading(false)
35
+ })
36
+ .catch(err => {
37
+ if (err.name === 'AbortError') return
38
+ error(err.message ?? String(err))
39
+ loading(false)
40
+ })
41
+ }
42
+
43
+ if (typeof urlOrSignal === 'function') {
44
+ effect(() => {
45
+ urlOrSignal()
46
+ execute()
47
+ })
48
+ } else if (immediate) {
49
+ execute()
50
+ }
51
+
52
+ return { data, loading, error, refetch: execute }
53
+ })
@@ -0,0 +1,63 @@
1
+ import { defineComposable, signal, effect } from '../../framework/index.js'
2
+
3
+ export interface FetchOptions<T> {
4
+ immediate?: boolean
5
+ transform?: (raw: any) => T
6
+ }
7
+
8
+ export interface FetchResult<T> {
9
+ data: Signal<T | null>
10
+ loading: Signal<boolean>
11
+ error: Signal<string | null>
12
+ refetch: () => void
13
+ }
14
+
15
+ export const useFetch = defineComposable(<T = any>(
16
+ urlOrSignal: string | (() => string),
17
+ options: FetchOptions<T> = {}
18
+ ): FetchResult<T> => {
19
+ const { immediate = true, transform = null } = options
20
+
21
+ const data = signal<T | null>(null)
22
+ const loading = signal(false)
23
+ const error = signal<string | null>(null)
24
+
25
+ let controller: AbortController | null = null
26
+
27
+ const execute = () => {
28
+ const url = typeof urlOrSignal === 'function' ? urlOrSignal() : urlOrSignal
29
+ if (!url) return
30
+
31
+ if (controller) controller.abort()
32
+ controller = new AbortController()
33
+
34
+ loading(true)
35
+ error(null)
36
+
37
+ fetch(url, { signal: controller.signal })
38
+ .then(res => {
39
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
40
+ return res.json()
41
+ })
42
+ .then(raw => {
43
+ data(transform ? transform(raw) : raw)
44
+ loading(false)
45
+ })
46
+ .catch(err => {
47
+ if (err.name === 'AbortError') return
48
+ error(err.message ?? String(err))
49
+ loading(false)
50
+ })
51
+ }
52
+
53
+ if (typeof urlOrSignal === 'function') {
54
+ effect(() => {
55
+ urlOrSignal()
56
+ execute()
57
+ })
58
+ } else if (immediate) {
59
+ execute()
60
+ }
61
+
62
+ return { data, loading, error, refetch: execute }
63
+ })
@@ -0,0 +1,31 @@
1
+ import { defineComposable, signal, effect } from '../../framework/index.js'
2
+
3
+ /**
4
+ * @template T
5
+ * @param {string} key
6
+ * @param {T} defaultValue
7
+ */
8
+ export const useLocalStorage = defineComposable((key, defaultValue = null) => {
9
+ const stored = localStorage.getItem(key)
10
+ let initial
11
+
12
+ try {
13
+ initial = stored !== null ? JSON.parse(stored) : defaultValue
14
+ } catch {
15
+ initial = defaultValue
16
+ localStorage.removeItem(key)
17
+ }
18
+
19
+ const value = signal(initial)
20
+
21
+ effect(() => {
22
+ const current = value()
23
+ if (current === null || current === undefined) {
24
+ localStorage.removeItem(key)
25
+ } else {
26
+ localStorage.setItem(key, JSON.stringify(current))
27
+ }
28
+ })
29
+
30
+ return value
31
+ })
@@ -0,0 +1,26 @@
1
+ import { defineComposable, signal, effect } from '../../framework/index.js'
2
+
3
+ export const useLocalStorage = defineComposable(<T>(key: string, defaultValue: T | null = null): Signal<T | null> => {
4
+ const stored = localStorage.getItem(key)
5
+ let initial: T | null
6
+
7
+ try {
8
+ initial = stored !== null ? JSON.parse(stored) : defaultValue
9
+ } catch {
10
+ initial = defaultValue
11
+ localStorage.removeItem(key)
12
+ }
13
+
14
+ const value = signal<T | null>(initial)
15
+
16
+ effect(() => {
17
+ const current = value()
18
+ if (current === null || current === undefined) {
19
+ localStorage.removeItem(key)
20
+ } else {
21
+ localStorage.setItem(key, JSON.stringify(current))
22
+ }
23
+ })
24
+
25
+ return value
26
+ })
package/app/main.js CHANGED
@@ -1,13 +1,62 @@
1
- import { createRouter } from '../framework/index.js'
2
- import { Home } from './routes/Home.js'
3
- import { About } from './routes/About.js'
4
- import { TestFor } from './routes/TestFor.js'
1
+ import { createRouter, mountOutlet, defineAsyncComponent } from '../framework/index.js'
2
+ import { Loading } from './components/Loading.js'
5
3
 
6
- createRouter([
7
- { path: '/', component: Home },
8
- { path: '/about', component: About },
9
- { path: '/test-for', component: TestFor },
10
- ])
4
+ directive('focus', {
5
+ mounted(el) {
6
+ el.focus()
7
+ }
8
+ })
9
+
10
+ directive('highlight', {
11
+ mounted(el, binding) {
12
+ el.style.backgroundColor = binding.value || '#ffff0033'
13
+ el.style.transition = 'background-color 0.3s'
14
+ },
15
+ updated(el, binding) {
16
+ el.style.backgroundColor = binding.value || '#ffff0033'
17
+ }
18
+ })
19
+
20
+ const routes = [
21
+ {
22
+ path: '/',
23
+ component: defineAsyncComponent({
24
+ loader: () => import('./routes/Home.js'),
25
+ loadingComponent: Loading
26
+ })
27
+ },
28
+ {
29
+ path: '/about',
30
+ component: defineAsyncComponent({
31
+ loader: () => import('./routes/About.js'),
32
+ loadingComponent: Loading
33
+ })
34
+ },
35
+ {
36
+ path: '/test-for',
37
+ component: defineAsyncComponent({
38
+ loader: () => import('./routes/TestFor.js'),
39
+ loadingComponent: Loading
40
+ })
41
+ },
42
+ {
43
+ path: '/dynamic',
44
+ component: defineAsyncComponent({
45
+ loader: () => import('./routes/TestDynamic.js'),
46
+ loadingComponent: Loading
47
+ })
48
+ },
49
+ {
50
+ path: '/composables',
51
+ component: defineAsyncComponent({
52
+ loader: () => import('./routes/TestComposables.js'),
53
+ loadingComponent: Loading
54
+ })
55
+ },
56
+ ]
57
+
58
+ createRouter(routes)
11
59
 
12
60
  const outlet = document.getElementById('app')
61
+ if (!outlet) throw new Error('[app] #app element not found in DOM')
13
62
  mountOutlet(outlet)
package/app/main.ts CHANGED
@@ -1,13 +1,59 @@
1
- import { createRouter, provide, inject } from '../framework/index.js'
1
+ import { createRouter, mountOutlet, defineAsyncComponent } from '../framework/index.js'
2
2
  import type { Route } from '../framework/types/index.js'
3
- import { Home } from './routes/Home.js'
4
- import { About } from './routes/About.js'
5
- import { TestFor } from './routes/TestFor.js'
3
+ import { Loading } from './components/Loading.js'
4
+
5
+ directive('focus', {
6
+ mounted(el) {
7
+ el.focus()
8
+ }
9
+ })
10
+
11
+ directive('highlight', {
12
+ mounted(el, binding) {
13
+ el.style.backgroundColor = binding.value || '#ffff0033'
14
+ el.style.transition = 'background-color 0.3s'
15
+ },
16
+ updated(el, binding) {
17
+ el.style.backgroundColor = binding.value || '#ffff0033'
18
+ }
19
+ })
6
20
 
7
21
  const routes: Route[] = [
8
- { path: '/', component: Home },
9
- { path: '/about', component: About },
10
- { path: '/test-for', component: TestFor },
22
+ {
23
+ path: '/',
24
+ component: defineAsyncComponent({
25
+ loader: () => import('./routes/Home.ts'),
26
+ loadingComponent: Loading
27
+ })
28
+ },
29
+ {
30
+ path: '/about',
31
+ component: defineAsyncComponent({
32
+ loader: () => import('./routes/About.ts'),
33
+ loadingComponent: Loading
34
+ })
35
+ },
36
+ {
37
+ path: '/test-for',
38
+ component: defineAsyncComponent({
39
+ loader: () => import('./routes/TestFor.ts'),
40
+ loadingComponent: Loading
41
+ })
42
+ },
43
+ {
44
+ path: '/dynamic',
45
+ component: defineAsyncComponent({
46
+ loader: () => import('./routes/TestDynamic.ts'),
47
+ loadingComponent: Loading
48
+ })
49
+ },
50
+ {
51
+ path: '/composables',
52
+ component: defineAsyncComponent({
53
+ loader: () => import('./routes/TestComposables.ts'),
54
+ loadingComponent: Loading
55
+ })
56
+ },
11
57
  ]
12
58
 
13
59
  createRouter(routes)