@getforma/core 0.2.0 → 0.3.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 +251 -13
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@getforma/core)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-
Reactive DOM library with fine-grained signals
|
|
7
|
+
Reactive DOM library with fine-grained signals. No virtual DOM — `h()` creates real elements, signals update only what changed. ~15KB gzipped.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -12,7 +12,71 @@ Reactive DOM library with fine-grained signals, islands architecture, and SSR hy
|
|
|
12
12
|
npm install @getforma/core
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Or use the CDN (no build step required):
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<script src="https://unpkg.com/@getforma/core/dist/formajs-runtime.global.js"></script>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Why FormaJS?
|
|
22
|
+
|
|
23
|
+
Most UI libraries make you choose: simple but limited (Alpine, htmx), or powerful but complex (React, Vue, Svelte). FormaJS gives you a single reactive core that scales from a CDN script tag to a full-stack Rust SSR pipeline.
|
|
24
|
+
|
|
25
|
+
**Design principles:**
|
|
26
|
+
|
|
27
|
+
- **Real DOM, not virtual DOM.** `h('div')` returns an actual `HTMLDivElement`. Signals mutate it directly. No diffing pass, no reconciliation overhead for simple updates. Inspired by [Solid](https://www.solidjs.com/).
|
|
28
|
+
- **Fine-grained reactivity.** Powered by [alien-signals](https://github.com/nicolo-ribaudo/alien-signals). When a signal changes, only the specific DOM text node or attribute that depends on it updates — not the whole component tree.
|
|
29
|
+
- **Three entry points, one engine.** HTML Runtime (like Alpine — zero build step), `h()` hyperscript (like Preact), or JSX. All share the same signal graph. Pick the right tool for the job, upgrade without rewriting.
|
|
30
|
+
- **CSP-safe by default.** The HTML Runtime includes a hand-written expression parser. `new Function()` is an opt-in fallback, not a requirement. Ship to strict CSP environments without worry.
|
|
31
|
+
- **Islands over SPAs.** `activateIslands()` hydrates independent regions of server-rendered HTML. Each island is self-contained. Ship less JavaScript, keep server-rendered content instant.
|
|
32
|
+
|
|
33
|
+
**What FormaJS is not:** It's not a framework with opinions about routing, data fetching, or state management patterns. It's a reactive DOM library. You bring the architecture.
|
|
34
|
+
|
|
35
|
+
## Three Ways to Use FormaJS
|
|
36
|
+
|
|
37
|
+
### 1. HTML Runtime (no build step)
|
|
38
|
+
|
|
39
|
+
Drop a script tag, write `data-*` attributes. Zero config, zero tooling.
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<script src="https://unpkg.com/@getforma/core/dist/formajs-runtime.global.js"></script>
|
|
43
|
+
|
|
44
|
+
<div data-forma-state='{"count": 0}'>
|
|
45
|
+
<p data-text="{count}"></p>
|
|
46
|
+
<button data-on:click="{count++}">+1</button>
|
|
47
|
+
<button data-on:click="{count = 0}">Reset</button>
|
|
48
|
+
</div>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
#### Supported Directives
|
|
52
|
+
|
|
53
|
+
| Directive | Description | Example |
|
|
54
|
+
|-----------|-------------|---------|
|
|
55
|
+
| `data-forma-state` | Declare reactive state (JSON) | `data-forma-state='{"count": 0}'` |
|
|
56
|
+
| `data-text` | Bind text content | `data-text="{count}"` |
|
|
57
|
+
| `data-show` | Toggle visibility (display) | `data-show="{isOpen}"` |
|
|
58
|
+
| `data-if` | Conditional render (add/remove from DOM) | `data-if="{loggedIn}"` |
|
|
59
|
+
| `data-model` | Two-way binding (inputs) | `data-model="{email}"` |
|
|
60
|
+
| `data-on:event` | Event handler | `data-on:click="{count++}"` |
|
|
61
|
+
| `data-class:name` | Conditional CSS class | `data-class:active="{isActive}"` |
|
|
62
|
+
| `data-bind:attr` | Dynamic attribute | `data-bind:href="{url}"` |
|
|
63
|
+
| `data-list` | List rendering with keyed reconciliation | `data-list="{items}"` |
|
|
64
|
+
| `data-computed` | Computed value | `data-computed="doubled = count * 2"` |
|
|
65
|
+
| `data-persist` | Persist state to localStorage | `data-persist="{count}"` |
|
|
66
|
+
| `data-fetch` | Fetch data from URL | `data-fetch="GET /api/items → items"` |
|
|
67
|
+
| `data-transition:*` | Enter/leave CSS transitions | `data-transition:enter="fade-in"` |
|
|
68
|
+
|
|
69
|
+
CSP-safe expression parser — no `eval()` or `new Function()` by default. For strict CSP environments, use the hardened build:
|
|
70
|
+
|
|
71
|
+
```html
|
|
72
|
+
<script src="https://unpkg.com/@getforma/core/dist/formajs-runtime-hardened.global.js"></script>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 2. Hyperscript — `h()`
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install @getforma/core
|
|
79
|
+
```
|
|
16
80
|
|
|
17
81
|
```typescript
|
|
18
82
|
import { createSignal, h, mount } from '@getforma/core';
|
|
@@ -27,21 +91,195 @@ mount(() =>
|
|
|
27
91
|
);
|
|
28
92
|
```
|
|
29
93
|
|
|
30
|
-
|
|
94
|
+
### 3. JSX
|
|
95
|
+
|
|
96
|
+
Same `h()` function, JSX syntax. Configure your bundler:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
// tsconfig.json
|
|
100
|
+
{
|
|
101
|
+
"compilerOptions": {
|
|
102
|
+
"jsx": "react",
|
|
103
|
+
"jsxFactory": "h",
|
|
104
|
+
"jsxFragmentFactory": "Fragment"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { createSignal, h, Fragment, mount } from '@getforma/core';
|
|
111
|
+
|
|
112
|
+
const [count, setCount] = createSignal(0);
|
|
113
|
+
|
|
114
|
+
function Counter() {
|
|
115
|
+
return (
|
|
116
|
+
<>
|
|
117
|
+
<p>{() => `Count: ${count()}`}</p>
|
|
118
|
+
<button onClick={() => setCount(count() + 1)}>+1</button>
|
|
119
|
+
</>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
mount(() => <Counter />, '#app');
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
If you use `@getforma/build`, JSX is preconfigured — just write `.tsx` files.
|
|
127
|
+
|
|
128
|
+
## Core API
|
|
129
|
+
|
|
130
|
+
### Signals
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { createSignal, createEffect, createComputed, batch } from '@getforma/core';
|
|
134
|
+
|
|
135
|
+
const [count, setCount] = createSignal(0);
|
|
136
|
+
const doubled = createComputed(() => count() * 2);
|
|
137
|
+
|
|
138
|
+
createEffect(() => console.log('count:', count()));
|
|
139
|
+
|
|
140
|
+
batch(() => {
|
|
141
|
+
setCount(1);
|
|
142
|
+
setCount(2); // effect fires once with value 2
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Conditional Rendering
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { createSignal, createShow, createSwitch, h } from '@getforma/core';
|
|
150
|
+
|
|
151
|
+
const [loggedIn, setLoggedIn] = createSignal(false);
|
|
152
|
+
|
|
153
|
+
// createShow — toggle between two branches
|
|
154
|
+
createShow(loggedIn,
|
|
155
|
+
() => h('p', null, 'Welcome back'),
|
|
156
|
+
() => h('p', null, 'Please sign in'),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// createSwitch — multi-branch with caching
|
|
160
|
+
const [view, setView] = createSignal('home');
|
|
161
|
+
createSwitch(view, [
|
|
162
|
+
{ match: 'home', render: () => h('div', null, 'Home') },
|
|
163
|
+
{ match: 'settings', render: () => h('div', null, 'Settings') },
|
|
164
|
+
], () => h('div', null, '404 Not Found'));
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### List Rendering
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { createSignal, createList, h } from '@getforma/core';
|
|
171
|
+
|
|
172
|
+
const [items, setItems] = createSignal([
|
|
173
|
+
{ id: 1, name: 'Alice' },
|
|
174
|
+
{ id: 2, name: 'Bob' },
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
createList(
|
|
178
|
+
items,
|
|
179
|
+
(item) => item.id, // key function
|
|
180
|
+
(item) => h('li', null, item.name),
|
|
181
|
+
);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Store (deep reactivity)
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { createStore } from '@getforma/core';
|
|
188
|
+
|
|
189
|
+
const [state, setState] = createStore({
|
|
190
|
+
user: { name: 'Alice', prefs: { theme: 'dark' } },
|
|
191
|
+
items: [1, 2, 3],
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Read reactively
|
|
195
|
+
state.user.name; // 'Alice'
|
|
196
|
+
|
|
197
|
+
// Mutate — only affected subscribers update
|
|
198
|
+
setState('user', 'name', 'Bob');
|
|
199
|
+
setState('items', items => [...items, 4]);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Components
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import { defineComponent, onMount, onUnmount, h } from '@getforma/core';
|
|
206
|
+
|
|
207
|
+
const Timer = defineComponent(() => {
|
|
208
|
+
const [seconds, setSeconds] = createSignal(0);
|
|
209
|
+
|
|
210
|
+
onMount(() => {
|
|
211
|
+
const id = setInterval(() => setSeconds(s => s + 1), 1000);
|
|
212
|
+
return () => clearInterval(id); // cleanup on unmount
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return h('span', null, () => `${seconds()}s`);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
document.body.appendChild(Timer());
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Context (Dependency Injection)
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import { createContext, provide, inject } from '@getforma/core';
|
|
225
|
+
|
|
226
|
+
const ThemeCtx = createContext('light');
|
|
227
|
+
|
|
228
|
+
provide(ThemeCtx, 'dark');
|
|
229
|
+
const theme = inject(ThemeCtx); // 'dark'
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Islands Architecture
|
|
233
|
+
|
|
234
|
+
For server-rendered HTML, activate independent interactive regions:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import { activateIslands, createSignal, h } from '@getforma/core';
|
|
238
|
+
|
|
239
|
+
activateIslands({
|
|
240
|
+
Counter: (el, props) => {
|
|
241
|
+
const [count, setCount] = createSignal(props.initial ?? 0);
|
|
242
|
+
// Hydrate: attach reactivity to existing server-rendered DOM
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```html
|
|
248
|
+
<!-- Server-rendered HTML -->
|
|
249
|
+
<div data-forma-island="Counter" data-forma-props='{"initial": 5}'>
|
|
250
|
+
<span>5</span>
|
|
251
|
+
<button>+1</button>
|
|
252
|
+
</div>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Subpath Exports
|
|
256
|
+
|
|
257
|
+
| Import | Description |
|
|
258
|
+
|--------|-------------|
|
|
259
|
+
| `@getforma/core` | Signals, `h()`, `mount()`, lists, stores, components |
|
|
260
|
+
| `@getforma/core/runtime` | HTML Runtime — `initRuntime()`, `mount()`, `unmount()` |
|
|
261
|
+
| `@getforma/core/runtime-hardened` | Runtime with `new Function()` locked off (strict CSP) |
|
|
262
|
+
| `@getforma/core/ssr` | Server-side rendering — `renderToString()`, `renderToStream()` |
|
|
263
|
+
| `@getforma/core/tc39` | TC39-compatible `Signal.State` and `Signal.Computed` classes |
|
|
264
|
+
|
|
265
|
+
## Examples
|
|
266
|
+
|
|
267
|
+
See the [`examples/`](./examples) directory:
|
|
31
268
|
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
36
|
-
- **Conditional rendering** — `createShow()`, `createSwitch()` with branch caching for O(1) toggle.
|
|
37
|
-
- **List rendering** — `createList()` with LIS-based keyed reconciliation, handles 50K+ rows.
|
|
38
|
-
- **State management** — `createStore()` with deep reactivity, history, persistence.
|
|
269
|
+
- **counter** — minimal `h()` counter
|
|
270
|
+
- **counter-jsx** — same counter with JSX syntax
|
|
271
|
+
- **todo** — todo list with `createList` and keyed reconciliation
|
|
272
|
+
- **data-table** — sortable table with `createList`
|
|
39
273
|
|
|
40
274
|
## Ecosystem
|
|
41
275
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
276
|
+
| Package | Description |
|
|
277
|
+
|---------|-------------|
|
|
278
|
+
| [@getforma/core](https://www.npmjs.com/package/@getforma/core) | This library — `npm install @getforma/core` |
|
|
279
|
+
| [@getforma/compiler](https://www.npmjs.com/package/@getforma/compiler) | SSR compiler — `.tsx` to FMIR binary |
|
|
280
|
+
| [@getforma/build](https://www.npmjs.com/package/@getforma/build) | esbuild wrapper with JSX + SSR preconfigured |
|
|
281
|
+
| [create-forma-app](https://www.npmjs.com/package/@getforma/create-app) | `npx @getforma/create-app` project scaffolder |
|
|
282
|
+
| [forma](https://github.com/getforma-dev/forma) | Rust server framework (forma-ir + forma-server) |
|
|
45
283
|
|
|
46
284
|
## License
|
|
47
285
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getforma/core",
|
|
3
3
|
"author": "Forma <victor@getforma.dev>",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Real DOM reactive library — fine-grained signals, islands architecture, SSR hydration. No virtual DOM, no diffing. ~15KB gzipped.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.cjs",
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"typecheck": "tsc --noEmit",
|
|
78
78
|
"test": "vitest run",
|
|
79
79
|
"test:watch": "vitest",
|
|
80
|
+
"test:coverage": "vitest run --coverage",
|
|
80
81
|
"prepublishOnly": "npm run build"
|
|
81
82
|
},
|
|
82
83
|
"keywords": [
|
|
@@ -113,6 +114,7 @@
|
|
|
113
114
|
"rollup": "^4.59.0"
|
|
114
115
|
},
|
|
115
116
|
"devDependencies": {
|
|
117
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
116
118
|
"fake-indexeddb": "^6.2.3",
|
|
117
119
|
"happy-dom": "^20.8.3",
|
|
118
120
|
"tsup": "^8.5.1",
|