fusee-framework 1.0.1 → 1.1.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,5 +1,5 @@
1
1
  # Fusée Framework 🚀
2
- **v1.0.1 — Signals-First JS Framework | Atomic Reactivity | Peak Performance**
2
+ **v1.1.1 — 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, and a comprehensive CLI for instant application scaffolding.
5
5
 
@@ -1,4 +1,3 @@
1
- import { defineComponent, signal, computed, effect, batch, untrack, onMount, onUnmount, type Signal, type Computed } from '../../framework/index.js'
2
1
 
3
2
  type CounterProps = {
4
3
  initialValue?: string
@@ -96,4 +95,3 @@ export const Counter = defineComponent({
96
95
  }
97
96
  }
98
97
  })
99
-
@@ -1,4 +1,3 @@
1
- import { defineComponent, signal, type SignalAccessor } from '../../framework/index.js'
2
1
  import { Counter } from './Counter.js'
3
2
 
4
3
  type CounterSectionProps = {
@@ -22,7 +21,7 @@ export const CounterSection = defineComponent({
22
21
 
23
22
  return {
24
23
  showCounter,
25
- get title() { return props.pageTitle },
24
+ get title() { return props.pageTitle || '' },
26
25
  toggleCounter: () => showCounter(!showCounter()),
27
26
  template: `
28
27
  <div style="margin-top: 30px; border-top: 1px solid #333; padding-top: 20px;">
@@ -1,5 +1,3 @@
1
- import { defineComponent, signal } from '../../framework/index.js'
2
-
3
1
  export const UserForm = defineComponent({
4
2
  setup() {
5
3
  const userName = signal('Guest')
package/app/main.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createRouter, mountOutlet } from '../framework/index.js'
1
+ import { createRouter } from '../framework/index.js'
2
2
  import { Home } from './routes/Home.js'
3
3
  import { About } from './routes/About.js'
4
4
  import { TestFor } from './routes/TestFor.js'
@@ -1,5 +1,3 @@
1
- import { defineComponent, signal, onMount } from '../../framework/index.js'
2
-
3
1
  export const About = defineComponent({
4
2
  props: [],
5
3
  setup() {
@@ -1,4 +1,3 @@
1
- import { defineComponent, signal } from '../../framework/index.js'
2
1
  import { UserForm } from '../components/UserForm.js'
3
2
  import { CounterSection } from '../components/CounterSection.js'
4
3
 
@@ -1,9 +1,8 @@
1
- import { defineComponent, signal, type SignalAccessor } from '../../framework/index.js'
2
1
  import { UserForm } from '../components/UserForm.js'
3
2
  import { CounterSection } from '../components/CounterSection.js'
4
3
 
5
4
  type HomeResult = {
6
- title: SignalAccessor<string>
5
+ title: Signal<string>
7
6
  changeTitle: () => void
8
7
  template: string
9
8
  }
@@ -11,15 +10,14 @@ type HomeResult = {
11
10
  export const Home = defineComponent({
12
11
  components: { UserForm, CounterSection },
13
12
  setup(): HomeResult {
14
- const title = signal('Welcome!')
15
-
13
+ const title = signal<string>('Welcome!')
16
14
  return {
17
15
  title,
18
16
  changeTitle: () => title('Changed Title!'),
19
17
  template: `
20
18
  <div class="page">
21
19
  <h1>{{ title.toUpperCase() }}</h1>
22
- <p>This is an SPA built with my own JavaScript custom framework!</p>
20
+ <p>This is an SPA built with my own (TypeScript!) custom framework!</p>
23
21
  <p>Made with passion by Sebastian Șomu</p>
24
22
 
25
23
  <button @click="changeTitle" class="primary-btn" style="margin-bottom: 20px;">Change App Title</button>
@@ -1,5 +1,3 @@
1
- import { defineComponent, signal } from '../../framework/index.js'
2
-
3
1
  export const TestFor = defineComponent({
4
2
  setup() {
5
3
  const items = signal([
@@ -0,0 +1,54 @@
1
+ type Item = {
2
+ id: number
3
+ name: string
4
+ }
5
+
6
+ type TestForResult = {
7
+ items: Signal<Item[]>
8
+ addItem: () => void
9
+ shuffleItems: () => void
10
+ template: string
11
+ }
12
+
13
+ export const TestFor = defineComponent({
14
+ setup(): TestForResult {
15
+ const items = signal<Item[]>([
16
+ { id: 1, name: 'Item 1' },
17
+ { id: 2, name: 'Item 2' },
18
+ { id: 3, name: 'Item 3' }
19
+ ])
20
+
21
+ return {
22
+ items,
23
+ addItem: () => {
24
+ const nextIds = items().map(i => i.id)
25
+ const nextId = nextIds.length > 0 ? Math.max(...nextIds) + 1 : 1
26
+ items([...items(), { id: nextId, name: `Item ${nextId}` }])
27
+ },
28
+ shuffleItems: () => {
29
+ const shuffled = [...items()].sort(() => Math.random() - 0.5)
30
+ items(shuffled)
31
+ },
32
+ template: `
33
+ <div class="page">
34
+ <h1>Keyed f-for Test (TypeScript)</h1>
35
+ <p>Focus an input below, then click "Shuffle" or "Add". The focus should stay!</p>
36
+
37
+ <div style="margin-bottom: 20px; display: flex; gap: 10px;">
38
+ <button @click="addItem" class="primary-btn">Add Item</button>
39
+ <button @click="shuffleItems" class="primary-btn">Shuffle Items</button>
40
+ </div>
41
+
42
+ <div style="display: flex; flex-direction: column; gap: 10px;">
43
+ <div f-for="item in items" :key="item.id"
44
+ style="padding: 10px; border: 1px solid #333; display: flex; gap: 10px; align-items: center;">
45
+ <span>ID: {{ item.id }}</span>
46
+ <input type="text" :value="item.name" placeholder="Type here..."
47
+ style="background: #111; color: #fff; border: 1px solid #444; padding: 5px;" />
48
+ </div>
49
+ </div>
50
+ </div>
51
+ `
52
+ }
53
+ }
54
+ })
@@ -0,0 +1,19 @@
1
+ /* eslint-disable */
2
+ /* prettier-ignore */
3
+ // @ts-nocheck
4
+ // noinspection JSUnusedGlobalSymbols
5
+ // Generated by unplugin-auto-import
6
+ // biome-ignore lint: disable
7
+ export {}
8
+ declare global {
9
+ const batch: typeof import('fusee-framework').batch
10
+ const computed: typeof import('fusee-framework').computed
11
+ const defineComponent: typeof import('fusee-framework').defineComponent
12
+ const effect: typeof import('fusee-framework').effect
13
+ const mountOutlet: typeof import('fusee-framework').mountOutlet
14
+ const mountTemplate: typeof import('fusee-framework').mountTemplate
15
+ const onMount: typeof import('fusee-framework').onMount
16
+ const onUnmount: typeof import('fusee-framework').onUnmount
17
+ const signal: typeof import('fusee-framework').signal
18
+ const untrack: typeof import('fusee-framework').untrack
19
+ }
@@ -0,0 +1,8 @@
1
+ export const FuseePreset = {
2
+ 'fusee-framework': [
3
+ 'signal', 'computed', 'effect',
4
+ 'batch', 'untrack',
5
+ 'defineComponent', 'onMount', 'onUnmount',
6
+ 'mountOutlet', 'mountTemplate'
7
+ ]
8
+ }
@@ -6,7 +6,16 @@ import { fileURLToPath } from 'url';
6
6
 
7
7
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
- const sourceFramework = path.join(__dirname, '..');
9
+
10
+ // Find the package root
11
+ let pkgRoot = __dirname;
12
+ while (pkgRoot !== path.dirname(pkgRoot) && !fs.existsSync(path.join(pkgRoot, 'package.json'))) {
13
+ pkgRoot = path.dirname(pkgRoot);
14
+ }
15
+
16
+ const sourceFramework = fs.existsSync(path.join(pkgRoot, 'framework'))
17
+ ? path.join(pkgRoot, 'framework')
18
+ : pkgRoot;
10
19
 
11
20
  const projectName = process.argv[2] || 'my-fusee-app';
12
21
  const projectPath = path.isAbsolute(projectName) ? projectName : path.join(process.cwd(), projectName);
@@ -24,9 +33,8 @@ async function scaffold() {
24
33
  function copyDir(src, dest, skipTypes = false) {
25
34
  if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
26
35
  fs.readdirSync(src).forEach(item => {
27
- // Sărim folderul types SAU orice fișier .d.ts rătăcit
28
36
  if (skipTypes && (item === 'types' || item.endsWith('.d.ts'))) return;
29
- if (item === 'bin' || item === 'node_modules' || item === '.git') return;
37
+ if (item === 'bin' || item === 'node_modules' || item === '.git' || item === 'dist') return;
30
38
 
31
39
  const s = path.join(src, item);
32
40
  const d = path.join(dest, item);
@@ -35,81 +43,125 @@ async function scaffold() {
35
43
  });
36
44
  }
37
45
 
38
- const coreFolders = ['app', 'app/components'];
46
+ const coreFolders = ['app', 'app/components', 'app/routes'];
39
47
  coreFolders.forEach(f => {
40
48
  const dir = path.join(projectPath, f);
41
49
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
42
50
  });
43
51
 
44
- console.log('📦 Injecting Fusée Core Engine (Strict Mode)...');
52
+ console.log('📦 Injecting Fusée Core Engine...');
45
53
  copyDir(sourceFramework, path.join(projectPath, 'framework'), choice === 'js');
46
54
 
55
+ const ext = choice === 'ts' ? 'ts' : 'js';
56
+
57
+ // 1. package.json
47
58
  const packageJson = {
48
59
  name: projectName === '.' ? 'fusee-project' : projectName,
49
60
  version: '1.0.0',
50
61
  type: 'module',
51
62
  scripts: { "dev": "vite", "build": "vite build" },
52
- devDependencies: { "vite": "^5.0.0" }
63
+ devDependencies: {
64
+ "vite": "^5.0.0",
65
+ "unplugin-auto-import": "^0.17.0"
66
+ }
53
67
  };
54
68
  if (choice === 'ts') packageJson.devDependencies["typescript"] = "^5.0.0";
55
69
  fs.writeFileSync(path.join(projectPath, 'package.json'), JSON.stringify(packageJson, null, 2));
56
70
 
71
+ // 2. vite.config.js
72
+ const viteConfig = `import { defineConfig } from 'vite';
73
+ import path from 'path';
74
+ import AutoImport from 'unplugin-auto-import/vite';
75
+ import { FuseePreset } from './framework/auto-import-preset.js';
76
+
77
+ export default defineConfig({
78
+ resolve: {
79
+ alias: {
80
+ 'fusee-framework': path.resolve(__dirname, './framework')
81
+ }
82
+ },
83
+ plugins: [
84
+ AutoImport({
85
+ imports: [FuseePreset],
86
+ dts: true // Generates auto-imports.d.ts
87
+ })
88
+ ]
89
+ });`;
90
+ fs.writeFileSync(path.join(projectPath, 'vite.config.js'), viteConfig);
91
+
92
+ // 3. tsconfig.json (if TS)
57
93
  if (choice === 'ts') {
58
- const tsConfig = { compilerOptions: { "target": "ESNext", "module": "ESNext", "moduleResolution": "node", "strict": true, "esModuleInterop": true, "skipLibCheck": true } };
94
+ const tsConfig = {
95
+ compilerOptions: {
96
+ "target": "ESNext",
97
+ "module": "ESNext",
98
+ "moduleResolution": "bundler",
99
+ "strict": true,
100
+ "esModuleInterop": true,
101
+ "skipLibCheck": true,
102
+ "paths": {
103
+ "fusee-framework": ["./framework/types/index.d.ts"]
104
+ }
105
+ },
106
+ "include": ["app/**/*", "framework/**/*", "auto-imports.d.ts"]
107
+ };
59
108
  fs.writeFileSync(path.join(projectPath, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2));
60
109
  }
61
110
 
62
- const ext = choice === 'ts' ? 'ts' : 'js';
63
- const indexHtml = `
64
- <!DOCTYPE html>
111
+ // 4. index.html
112
+ const indexHtml = `<!DOCTYPE html>
65
113
  <html>
66
114
  <head>
67
115
  <meta charset="UTF-8">
68
- <title>Strict Fusée App</title>
116
+ <title>Fusée App</title>
69
117
  <style>
70
- body { background: #050505; color: white; font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
71
- #app { text-align: center; background: #111; padding: 3rem; border-radius: 20px; border: 1px solid #222; box-shadow: 0 10px 40px rgba(255, 51, 51, 0.1); }
72
- h1 { color: #ff3333; text-shadow: 0 0 20px rgba(255, 51, 51, 0.3); margin: 0; font-size: 2.5rem; }
118
+ body { background: #050505; color: white; font-family: system-ui; margin: 0; }
119
+ #app { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; }
120
+ .hero { text-align: center; margin-bottom: 2rem; }
121
+ h1 { color: #ff3333; font-size: 3rem; margin: 0; }
73
122
  </style>
74
123
  </head>
75
124
  <body>
76
125
  <div id="app">
77
- <h1>Fusée 🚀</h1>
78
- <p style="color: #ff9999; opacity: 0.8; margin-bottom: 2rem;">A reactive framework built with passion.</p>
126
+ <div class="hero">
127
+ <h1>Fusée 🚀</h1>
128
+ <p style="opacity: 0.6;">Modern Reactive Framework</p>
129
+ </div>
79
130
  {{ Counter }}
80
131
  </div>
81
132
  <script type="module" src="/app/main.${ext}"></script>
82
133
  </body>
83
- </html>
84
- `;
134
+ </html>`;
85
135
  fs.writeFileSync(path.join(projectPath, 'index.html'), indexHtml);
86
136
 
87
- const btnStyle = "background:#ff3333; color:white; border:none; width:45px; height:45px; border-radius:12px; cursor:pointer; font-size:1.6rem; font-weight:bold; transition: 0.3s; box-shadow: 0 4px 15px rgba(255, 51, 51, 0.2);";
137
+ // 5. Counter component
138
+ const btnStyle = "background:#ff3333; color:white; border:none; width:50px; height:50px; border-radius:15px; cursor:pointer; font-size:1.5rem; transition: 0.2s;";
88
139
  const counterContent = choice === 'ts'
89
- ? `import { signal } from '../../framework/core/signal.js';
90
- import { defineComponent } from '../../framework/core/component.js';
140
+ ? `interface CounterResult {
141
+ count: Signal<number>
142
+ inc: () => void
143
+ dec: () => void
144
+ template: string
145
+ }
91
146
 
92
147
  export const Counter = defineComponent({
93
- setup() {
148
+ setup(): CounterResult {
94
149
  const count = signal<number>(0);
95
150
  return {
96
151
  count,
97
152
  inc: () => count(count() + 1),
98
153
  dec: () => count(count() - 1),
99
154
  template: \`
100
- <div style="display:flex; align-items:center; justify-content:center; gap:2.5rem;">
155
+ <div style="display:flex; align-items:center; gap:2rem; background:#111; padding:2rem; border-radius:20px; border:1px solid #222;">
101
156
  <button @click="dec" style="${btnStyle}">-</button>
102
- <span style="font-size:2.8rem; font-weight:bold; min-width:70px;">{{ count }}</span>
157
+ <span style="font-size:3rem; font-weight:bold; min-width:60px; text-align:center;">{{ count }}</span>
103
158
  <button @click="inc" style="${btnStyle}">+</button>
104
159
  </div>
105
160
  \`
106
161
  };
107
162
  }
108
163
  });`
109
- : `import { signal } from '../../framework/core/signal.js';
110
- import { defineComponent } from '../../framework/core/component.js';
111
-
112
- export const Counter = defineComponent({
164
+ : `export const Counter = defineComponent({
113
165
  setup() {
114
166
  const count = signal(0);
115
167
  return {
@@ -117,23 +169,23 @@ export const Counter = defineComponent({
117
169
  inc: () => count(count() + 1),
118
170
  dec: () => count(count() - 1),
119
171
  template: \`
120
- <div style="display:flex; align-items:center; justify-content:center; gap:2.5rem;">
172
+ <div style="display:flex; align-items:center; gap:2rem; background:#111; padding:2rem; border-radius:20px; border:1px solid #222;">
121
173
  <button @click="dec" style="${btnStyle}">-</button>
122
- <span style="font-size:2.8rem; font-weight:bold; min-width:70px;">{{ count }}</span>
174
+ <span style="font-size:3rem; font-weight:bold; min-width:60px; text-align:center;">{{ count }}</span>
123
175
  <button @click="inc" style="${btnStyle}">+</button>
124
176
  </div>
125
177
  \`
126
178
  };
127
179
  }
128
180
  });`;
129
-
130
181
  fs.writeFileSync(path.join(projectPath, `app/components/Counter.${ext}`), counterContent);
131
182
 
132
- const mainContent = `import { mountTemplate } from '../framework/core/compiler.js';
133
- import { Counter } from './components/Counter.${ext}';
183
+ // 6. main.js
184
+ const mainContent = `import { Counter } from './components/Counter';
134
185
 
135
186
  const root = document.getElementById('app');
136
187
  if (root) {
188
+ // mountTemplate is now auto-imported via Fusée!
137
189
  mountTemplate(root.innerHTML, root, {}, { Counter });
138
190
  }
139
191
  `;
@@ -64,7 +64,7 @@ export function compileNode(node, context, components, effects) {
64
64
  }
65
65
 
66
66
  if (node.nodeType === 1) {
67
- processDirectives(node, context, components, effects)
67
+ if (processDirectives(node, context, components, effects)) return
68
68
  processBindingAttrs(node, context, effects)
69
69
  bindComponents(node, components, context, effects)
70
70
  } else if (node.nodeType === 3) {
@@ -80,7 +80,7 @@ export function compileNode(node, context, components, effects) {
80
80
 
81
81
  function compileOnce(node, context, components, effects) {
82
82
  if (node.nodeType === 1) {
83
- processDirectives(node, context, components, effects)
83
+ if (processDirectives(node, context, components, effects)) return
84
84
  processBindingAttrs(node, context, effects)
85
85
  bindComponents(node, components, context, effects)
86
86
  } else if (node.nodeType === 3) {
@@ -167,48 +167,51 @@ function processBindingAttrs(el, context, effects) {
167
167
  }
168
168
  }
169
169
 
170
- function bindComponents(container, components, context, effects) {
171
- const placeholders = container.querySelectorAll('[data-component]')
172
- const els = container.matches && container.matches('[data-component]') ? [container, ...placeholders] : placeholders
173
- for (const placeholder of els) {
174
- const name = placeholder.dataset.component
175
- const ComponentFn = components[name]
176
- if (ComponentFn) {
177
- const props = {}
178
- for (const attr of [...placeholder.attributes]) {
179
- const attrName = attr.name.toLowerCase()
180
-
181
- if (attrName.startsWith('data-prop-')) {
182
- props[attrName.slice(10)] = attr.value
183
- } else if (attrName.startsWith('data-bind-prop-')) {
184
- const propName = attrName.slice(15)
185
- const expression = attr.value.trim()
186
-
187
- if (expression in context) {
188
- Object.defineProperty(props, propName, {
189
- get() {
190
- const val = context[expression]
191
- return typeof val === 'function' && val.isSignal ? val() : val
192
- },
193
- enumerable: true,
194
- configurable: true
195
- })
196
- } else {
197
- Object.defineProperty(props, propName, {
198
- get() {
199
- return evaluateExpression(expression, context)
200
- },
201
- enumerable: true,
202
- configurable: true
203
- })
204
- }
170
+ function bindComponents(el, components, context, effects) {
171
+ if (el.nodeType !== 1) return
172
+ const name = el.getAttribute('data-component')
173
+ if (!name) return
174
+
175
+ // Remove the attribute immediately to prevent recursive compilers from
176
+ // re-identifying this element as a component placeholder.
177
+ el.removeAttribute('data-component')
178
+
179
+ const ComponentFn = components[name]
180
+ if (ComponentFn) {
181
+ const props = {}
182
+ for (const attr of [...el.attributes]) {
183
+ const attrName = attr.name.toLowerCase()
184
+
185
+ if (attrName.startsWith('data-prop-')) {
186
+ props[attrName.slice(10)] = attr.value
187
+ } else if (attrName.startsWith('data-bind-prop-')) {
188
+ const propName = attrName.slice(15)
189
+ const expression = attr.value.trim()
190
+
191
+ if (expression in context) {
192
+ Object.defineProperty(props, propName, {
193
+ get() {
194
+ const val = context[expression]
195
+ return typeof val === 'function' && val.isSignal ? val() : val
196
+ },
197
+ enumerable: true,
198
+ configurable: true
199
+ })
200
+ } else {
201
+ Object.defineProperty(props, propName, {
202
+ get() {
203
+ return evaluateExpression(expression, context)
204
+ },
205
+ enumerable: true,
206
+ configurable: true
207
+ })
205
208
  }
206
209
  }
207
- const childComponent = ComponentFn(props)
208
- childComponent.render(placeholder)
209
- if (effects) {
210
- effects.push(() => childComponent.unmount())
211
- }
210
+ }
211
+ const childComponent = ComponentFn(props)
212
+ childComponent.render(el)
213
+ if (effects) {
214
+ effects.push(() => childComponent.unmount())
212
215
  }
213
216
  }
214
217
  }