fusee-framework 0.1.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SebiSomu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,8 @@
1
+ \# 🚀 Fusée v0.1.0-alpha
2
+
3
+
4
+
5
+ Peak Performance JS Framework
6
+
7
+ \*\*Signals + Components + SPA\*\*
8
+
@@ -0,0 +1,38 @@
1
+ import { defineComponent, signal, onMount, onUnmount } from '../../framework/index.js'
2
+
3
+ export const Counter = defineComponent({
4
+ props: ['initialValue'],
5
+ setup(props) {
6
+ const count = signal(Number(props.initialValue) || 0)
7
+ const double = signal(0)
8
+
9
+ // Manual computed: keep double in sync
10
+ // (could use computed() from framework too)
11
+ function increment() { count(count() + 1); double(count() * 2) }
12
+ function decrement() { count(count() - 1); double(count() * 2) }
13
+ function reset() { count(0); double(0) }
14
+
15
+ onMount(() => console.log('[Counter] mounted, initial:', count()))
16
+ onUnmount(() => console.log('[Counter] unmounted'))
17
+
18
+ return {
19
+ count,
20
+ double,
21
+ increment,
22
+ decrement,
23
+ reset,
24
+ template: `
25
+ <div class="counter">
26
+ <h2>Counter</h2>
27
+ <p class="value">{{ count }}</p>
28
+ <p class="derived">double: {{ double }}</p>
29
+ <div class="actions">
30
+ <button @click="decrement">−</button>
31
+ <button @click="reset">reset</button>
32
+ <button @click="increment">+</button>
33
+ </div>
34
+ </div>
35
+ `
36
+ }
37
+ }
38
+ })
package/app/main.js ADDED
@@ -0,0 +1,11 @@
1
+ import { createRouter, mountOutlet } from '../framework/index.js'
2
+ import { Home } from './routes/Home.js'
3
+ import { About } from './routes/About.js'
4
+
5
+ createRouter([
6
+ { path: '/', component: Home },
7
+ { path: '/about', component: About },
8
+ ])
9
+
10
+ const outlet = document.getElementById('app')
11
+ mountOutlet(outlet)
@@ -0,0 +1,24 @@
1
+ import { defineComponent, signal, onMount } from '../../framework/index.js'
2
+
3
+ export const About = defineComponent({
4
+ props: [],
5
+ setup() {
6
+ const loaded = signal(false)
7
+
8
+ onMount(() => {
9
+ setTimeout(() => loaded(true), 300)
10
+ })
11
+
12
+ return {
13
+ loaded,
14
+ template: `
15
+ <div class="page">
16
+ <h1>About</h1>
17
+ <p>Framework custom — prototip SPA.</p>
18
+ <p><strong>Features:</strong> signals, defineComponent, hash router, lifecycle hooks.</p>
19
+ <p class="status">{{ loaded }}</p>
20
+ </div>
21
+ `
22
+ }
23
+ }
24
+ })
@@ -0,0 +1,22 @@
1
+ import { defineComponent, signal } from '../../framework/index.js'
2
+ import { Counter } from '../components/Counter.js'
3
+
4
+ export const Home = defineComponent({
5
+ props: [],
6
+ components: { Counter },
7
+ setup() {
8
+ const title = signal('Welcome!')
9
+
10
+ return {
11
+ title,
12
+ template: `
13
+ <div class="page">
14
+ <h1>{{ title }}</h1>
15
+ <p>This is an SPA built with my own JavaScript custom framework!</p>
16
+ <p>Made with passion by Sebastian Șomu</p>
17
+ {{ Counter }}
18
+ </div>
19
+ `
20
+ }
21
+ }
22
+ })
@@ -0,0 +1,128 @@
1
+ // ─── Component System ─────────────────────────────────────────────────────────
2
+ import { effect } from './signal.js'
3
+
4
+ // Lifecycle context — set while setup() runs
5
+ let currentInstance = null
6
+
7
+ export function onMount(fn) {
8
+ if (currentInstance) currentInstance._mountHooks.push(fn)
9
+ }
10
+
11
+ export function onUnmount(fn) {
12
+ if (currentInstance) currentInstance._unmountHooks.push(fn)
13
+ }
14
+
15
+ export function defineComponent(options) {
16
+ return function ComponentFactory(props = {}) {
17
+ const instance = {
18
+ props,
19
+ _mountHooks: [],
20
+ _unmountHooks: [],
21
+ _effects: [],
22
+ _element: null,
23
+ }
24
+
25
+ // Run setup() with lifecycle context active
26
+ currentInstance = instance
27
+ const result = options.setup(props)
28
+ currentInstance = null
29
+
30
+ // Validate props defined in component
31
+ const allowedProps = options.props || []
32
+ for (const key of Object.keys(props)) {
33
+ if (!allowedProps.includes(key)) {
34
+ console.warn(`[framework] Unknown prop "${key}" passed to component.`)
35
+ }
36
+ }
37
+
38
+ // ── Template rendering ───────────────────────────────────────────────────
39
+ function render(container) {
40
+ instance._element = container
41
+
42
+ const e = effect(() => {
43
+ const html = resolveTemplate(result.template, result, options.components || {})
44
+ container.innerHTML = html
45
+ bindEvents(container, result)
46
+ bindComponents(container, options.components || {})
47
+ })
48
+
49
+ instance._effects.push(e)
50
+
51
+ // Run mount hooks after first render
52
+ for (const hook of instance._mountHooks) hook()
53
+
54
+ return instance
55
+ }
56
+
57
+ // ── Unmount ──────────────────────────────────────────────────────────────
58
+ function unmount() {
59
+ for (const hook of instance._unmountHooks) hook()
60
+ // Clean up all reactive effects
61
+ for (const e of instance._effects) {
62
+ for (const dep of e.deps) dep.delete(e)
63
+ e.deps.clear()
64
+ }
65
+ if (instance._element) instance._element.innerHTML = ''
66
+ }
67
+
68
+ return { render, unmount, instance }
69
+ }
70
+ }
71
+
72
+ // ── Template resolver ─────────────────────────────────────────────────────────
73
+ // Replaces {{ expr }} with evaluated values from the component result object.
74
+ function resolveTemplate(template, context, components) {
75
+ return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => {
76
+ if (key in components) {
77
+ // Nested component: {{ Counter }} → render component placeholder
78
+ return `<div data-component="${key}"></div>`
79
+ }
80
+ const val = context[key]
81
+ if (typeof val === 'function') {
82
+ // Could be a signal — call it to get value
83
+ try { return val() } catch { return '' }
84
+ }
85
+ return val ?? ''
86
+ })
87
+ }
88
+
89
+ // ── Event binding ─────────────────────────────────────────────────────────────
90
+ // Finds all elements with @event attributes and attaches listeners.
91
+ // Re-runs after every template re-render (innerHTML wipe clears old listeners).
92
+ function bindEvents(container, context) {
93
+ const all = container.querySelectorAll('*')
94
+ for (const el of all) {
95
+ for (const attr of [...el.attributes]) {
96
+ if (attr.name.startsWith('@')) {
97
+ const eventName = attr.name.slice(1) // @click → click
98
+ const handlerName = attr.value // "increment"
99
+ const handler = context[handlerName]
100
+ if (typeof handler === 'function') {
101
+ el.removeAttribute(attr.name)
102
+ el.addEventListener(eventName, handler)
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ // ── Nested component mounting ─────────────────────────────────────────────────
110
+ // Finds data-component placeholders and mounts child components inside them.
111
+ function bindComponents(container, components) {
112
+ const placeholders = container.querySelectorAll('[data-component]')
113
+ for (const placeholder of placeholders) {
114
+ const name = placeholder.dataset.component
115
+ const ComponentFn = components[name]
116
+ if (ComponentFn) {
117
+ // Read props from data-prop-* attributes if present
118
+ const props = {}
119
+ for (const attr of [...placeholder.attributes]) {
120
+ if (attr.name.startsWith('data-prop-')) {
121
+ props[attr.name.slice(10)] = attr.value
122
+ }
123
+ }
124
+ const child = ComponentFn(props)
125
+ child.render(placeholder)
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,52 @@
1
+ // ─── Reactivity Engine ───────────────────────────────────────────────────────
2
+ // Solid-style signals: count() to read, count(val) to write.
3
+ // An "effect" is any function that should re-run when signals it reads change.
4
+
5
+ let currentEffect = null
6
+
7
+ export function signal(initialValue) {
8
+ let value = initialValue
9
+ const subscribers = new Set()
10
+
11
+ function accessor(newValue) {
12
+ if (newValue === undefined) {
13
+ // READ — track dependency if inside an effect
14
+ if (currentEffect) {
15
+ subscribers.add(currentEffect)
16
+ currentEffect.deps.add(subscribers)
17
+ }
18
+ return value
19
+ } else {
20
+ // WRITE — notify subscribers only if value changed
21
+ if (newValue !== value) {
22
+ value = newValue
23
+ for (const sub of [...subscribers]) sub()
24
+ }
25
+ }
26
+ }
27
+
28
+ return accessor
29
+ }
30
+
31
+ export function effect(fn) {
32
+ const run = () => {
33
+ // Clean up previous subscriptions before re-running
34
+ for (const dep of run.deps) dep.delete(run)
35
+ run.deps.clear()
36
+
37
+ currentEffect = run
38
+ try { fn() }
39
+ finally { currentEffect = null }
40
+ }
41
+
42
+ run.deps = new Set()
43
+ run() // run immediately to collect dependencies
44
+ return run
45
+ }
46
+
47
+ export function computed(fn) {
48
+ // A computed is a read-only signal derived from other signals
49
+ const result = signal(undefined)
50
+ effect(() => result(fn()))
51
+ return result
52
+ }
@@ -0,0 +1,4 @@
1
+ // ─── Framework Public API ─────────────────────────────────────────────────────
2
+ export { signal, computed, effect } from './core/signal.js'
3
+ export { defineComponent, onMount, onUnmount } from './core/component.js'
4
+ export { createRouter, navigate, mountOutlet } from './router/router.js'
@@ -0,0 +1,69 @@
1
+ // ─── Hash-based SPA Router ────────────────────────────────────────────────────
2
+ // Routes are declared as: { path: '/about', component: AboutComponent }
3
+ // Navigation via <a href="#/about"> or router.navigate('/about')
4
+
5
+ let _routes = []
6
+ let _outlet = null
7
+ let _currentInstance = null
8
+
9
+ export function createRouter(routes) {
10
+ _routes = routes
11
+
12
+ // Listen for hash changes (back/forward + manual URL edits)
13
+ window.addEventListener('hashchange', () => _resolve())
14
+
15
+ // Initial render on page load
16
+ window.addEventListener('DOMContentLoaded', () => _resolve())
17
+
18
+ return { navigate }
19
+ }
20
+
21
+ export function navigate(path) {
22
+ window.location.hash = path
23
+ }
24
+
25
+ // ── Mount outlet ──────────────────────────────────────────────────────────────
26
+ // Call this with the DOM element that acts as <router-outlet>
27
+ export function mountOutlet(el) {
28
+ _outlet = el
29
+ _resolve()
30
+ }
31
+
32
+ // ── Internal resolution ───────────────────────────────────────────────────────
33
+ function _resolve() {
34
+ if (!_outlet) return
35
+
36
+ // Parse current hash: '#/about' → '/about', fallback to '/'
37
+ const hash = window.location.hash
38
+ const path = hash.startsWith('#') ? hash.slice(1) || '/' : '/'
39
+
40
+ const matched = _routes.find(r => _matchPath(r.path, path))
41
+
42
+ if (!matched) {
43
+ _outlet.innerHTML = `<p style="color:red">[framework] No route matched for "${path}"</p>`
44
+ return
45
+ }
46
+
47
+ // Unmount previous component to run cleanup hooks
48
+ if (_currentInstance) {
49
+ try { _currentInstance.unmount() } catch {}
50
+ }
51
+
52
+ _outlet.innerHTML = ''
53
+
54
+ const componentFn = matched.component
55
+ _currentInstance = componentFn()
56
+ _currentInstance.render(_outlet)
57
+ }
58
+
59
+ // Simple path matching — supports exact paths and :param segments
60
+ function _matchPath(routePath, actualPath) {
61
+ const routeParts = routePath.split('/').filter(Boolean)
62
+ const actualParts = actualPath.split('/').filter(Boolean)
63
+
64
+ if (routeParts.length !== actualParts.length) return false
65
+
66
+ return routeParts.every((part, i) =>
67
+ part.startsWith(':') || part === actualParts[i]
68
+ )
69
+ }
package/index.html ADDED
@@ -0,0 +1,103 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ro">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>MyFramework — SPA Demo</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ body {
11
+ font-family: system-ui, sans-serif;
12
+ background: #0f0f13;
13
+ color: #e8e8f0;
14
+ min-height: 100vh;
15
+ }
16
+
17
+ nav {
18
+ display: flex;
19
+ gap: 1rem;
20
+ padding: 1rem 2rem;
21
+ background: #1a1a24;
22
+ border-bottom: 1px solid #2a2a3a;
23
+ }
24
+
25
+ nav a {
26
+ color: #8b8bff;
27
+ text-decoration: none;
28
+ font-weight: 500;
29
+ }
30
+
31
+ nav a:hover { color: #fff; }
32
+
33
+ #app {
34
+ padding: 2rem;
35
+ max-width: 600px;
36
+ margin: 0 auto;
37
+ }
38
+
39
+ .page h1 { margin-bottom: 1rem; font-size: 1.8rem; }
40
+ .page p { margin-bottom: 0.5rem; color: #aaa; line-height: 1.6; }
41
+
42
+ .counter {
43
+ margin-top: 2rem;
44
+ padding: 1.5rem;
45
+ background: #1a1a24;
46
+ border: 1px solid #2a2a3a;
47
+ border-radius: 10px;
48
+ display: inline-block;
49
+ }
50
+
51
+ .counter h2 { margin-bottom: 1rem; font-size: 1rem; color: #888; text-transform: uppercase; letter-spacing: 0.1em; }
52
+
53
+ .value {
54
+ font-size: 4rem !important;
55
+ font-weight: 700;
56
+ color: #8b8bff !important;
57
+ text-align: center;
58
+ line-height: 1;
59
+ margin-bottom: 0.25rem !important;
60
+ }
61
+
62
+ .derived {
63
+ text-align: center;
64
+ font-size: 0.85rem !important;
65
+ color: #555 !important;
66
+ margin-bottom: 1rem !important;
67
+ }
68
+
69
+ .actions {
70
+ display: flex;
71
+ gap: 0.5rem;
72
+ justify-content: center;
73
+ }
74
+
75
+ .actions button {
76
+ padding: 0.5rem 1.25rem;
77
+ border: 1px solid #3a3a5a;
78
+ background: #0f0f1a;
79
+ color: #e8e8f0;
80
+ border-radius: 6px;
81
+ cursor: pointer;
82
+ font-size: 1.1rem;
83
+ transition: background 0.15s;
84
+ }
85
+
86
+ .actions button:hover { background: #2a2a3a; }
87
+
88
+ .status {
89
+ margin-top: 1rem;
90
+ font-size: 0.8rem;
91
+ color: #4caf50 !important;
92
+ }
93
+ </style>
94
+ </head>
95
+ <body>
96
+ <nav>
97
+ <a href="#/">Home</a>
98
+ <a href="#/about">About</a>
99
+ </nav>
100
+ <div id="app"></div>
101
+ <script type="module" src="./app/main.js"></script>
102
+ </body>
103
+ </html>
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "fusee-framework",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Fusée - Signals-first JS Framework | Peak Performance",
5
+ "main": "framework/index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [],
10
+ "author": "",
11
+ "license": "MIT",
12
+ "type": "commonjs",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/SebiSomu/fusee.git"
16
+ },
17
+ "publishConfig": {
18
+ "registry": "https://registry.npmjs.org/"
19
+ }
20
+ }