@getforma/core
Advanced tools
+3
-1
| { | ||
| "name": "@getforma/core", | ||
| "author": "Forma <victor@getforma.dev>", | ||
| "version": "0.2.0", | ||
| "version": "0.3.0", | ||
| "description": "Real DOM reactive library — fine-grained signals, islands architecture, SSR hydration. No virtual DOM, no diffing. ~15KB gzipped.", | ||
@@ -80,2 +80,3 @@ "type": "module", | ||
| "test:watch": "vitest", | ||
| "test:coverage": "vitest run --coverage", | ||
| "prepublishOnly": "npm run build" | ||
@@ -117,2 +118,3 @@ }, | ||
| "devDependencies": { | ||
| "@vitest/coverage-v8": "^4.0.18", | ||
| "fake-indexeddb": "^6.2.3", | ||
@@ -119,0 +121,0 @@ "happy-dom": "^20.8.3", |
+251
-13
@@ -7,3 +7,3 @@ # FormaJS | ||
| Reactive DOM library with fine-grained signals, islands architecture, and SSR hydration. ~15KB gzipped. | ||
| Reactive DOM library with fine-grained signals. No virtual DOM — `h()` creates real elements, signals update only what changed. ~15KB gzipped. | ||
@@ -16,4 +16,68 @@ ## Install | ||
| ## Quick Start | ||
| Or use the CDN (no build step required): | ||
| ```html | ||
| <script src="https://unpkg.com/@getforma/core/dist/formajs-runtime.global.js"></script> | ||
| ``` | ||
| ## Why FormaJS? | ||
| 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. | ||
| **Design principles:** | ||
| - **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/). | ||
| - **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. | ||
| - **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. | ||
| - **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. | ||
| - **Islands over SPAs.** `activateIslands()` hydrates independent regions of server-rendered HTML. Each island is self-contained. Ship less JavaScript, keep server-rendered content instant. | ||
| **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. | ||
| ## Three Ways to Use FormaJS | ||
| ### 1. HTML Runtime (no build step) | ||
| Drop a script tag, write `data-*` attributes. Zero config, zero tooling. | ||
| ```html | ||
| <script src="https://unpkg.com/@getforma/core/dist/formajs-runtime.global.js"></script> | ||
| <div data-forma-state='{"count": 0}'> | ||
| <p data-text="{count}"></p> | ||
| <button data-on:click="{count++}">+1</button> | ||
| <button data-on:click="{count = 0}">Reset</button> | ||
| </div> | ||
| ``` | ||
| #### Supported Directives | ||
| | Directive | Description | Example | | ||
| |-----------|-------------|---------| | ||
| | `data-forma-state` | Declare reactive state (JSON) | `data-forma-state='{"count": 0}'` | | ||
| | `data-text` | Bind text content | `data-text="{count}"` | | ||
| | `data-show` | Toggle visibility (display) | `data-show="{isOpen}"` | | ||
| | `data-if` | Conditional render (add/remove from DOM) | `data-if="{loggedIn}"` | | ||
| | `data-model` | Two-way binding (inputs) | `data-model="{email}"` | | ||
| | `data-on:event` | Event handler | `data-on:click="{count++}"` | | ||
| | `data-class:name` | Conditional CSS class | `data-class:active="{isActive}"` | | ||
| | `data-bind:attr` | Dynamic attribute | `data-bind:href="{url}"` | | ||
| | `data-list` | List rendering with keyed reconciliation | `data-list="{items}"` | | ||
| | `data-computed` | Computed value | `data-computed="doubled = count * 2"` | | ||
| | `data-persist` | Persist state to localStorage | `data-persist="{count}"` | | ||
| | `data-fetch` | Fetch data from URL | `data-fetch="GET /api/items → items"` | | ||
| | `data-transition:*` | Enter/leave CSS transitions | `data-transition:enter="fade-in"` | | ||
| CSP-safe expression parser — no `eval()` or `new Function()` by default. For strict CSP environments, use the hardened build: | ||
| ```html | ||
| <script src="https://unpkg.com/@getforma/core/dist/formajs-runtime-hardened.global.js"></script> | ||
| ``` | ||
| ### 2. Hyperscript — `h()` | ||
| ```bash | ||
| npm install @getforma/core | ||
| ``` | ||
| ```typescript | ||
@@ -32,17 +96,191 @@ import { createSignal, h, mount } from '@getforma/core'; | ||
| ## Features | ||
| ### 3. JSX | ||
| - **Real DOM** — `h()` creates actual DOM elements with reactive bindings. No virtual DOM, no diffing overhead. | ||
| - **Fine-grained reactivity** — signals, effects, computed values via alien-signals. Only what changed updates. | ||
| - **Islands architecture** — `activateIslands()` for independent hydration of server-rendered HTML regions. | ||
| - **SSR hydration** — `adoptNode()` walks server-rendered DOM and attaches reactive bindings without re-creating elements. | ||
| - **Conditional rendering** — `createShow()`, `createSwitch()` with branch caching for O(1) toggle. | ||
| - **List rendering** — `createList()` with LIS-based keyed reconciliation, handles 50K+ rows. | ||
| - **State management** — `createStore()` with deep reactivity, history, persistence. | ||
| Same `h()` function, JSX syntax. Configure your bundler: | ||
| ```json | ||
| // tsconfig.json | ||
| { | ||
| "compilerOptions": { | ||
| "jsx": "react", | ||
| "jsxFactory": "h", | ||
| "jsxFragmentFactory": "Fragment" | ||
| } | ||
| } | ||
| ``` | ||
| ```tsx | ||
| import { createSignal, h, Fragment, mount } from '@getforma/core'; | ||
| const [count, setCount] = createSignal(0); | ||
| function Counter() { | ||
| return ( | ||
| <> | ||
| <p>{() => `Count: ${count()}`}</p> | ||
| <button onClick={() => setCount(count() + 1)}>+1</button> | ||
| </> | ||
| ); | ||
| } | ||
| mount(() => <Counter />, '#app'); | ||
| ``` | ||
| If you use `@getforma/build`, JSX is preconfigured — just write `.tsx` files. | ||
| ## Core API | ||
| ### Signals | ||
| ```typescript | ||
| import { createSignal, createEffect, createComputed, batch } from '@getforma/core'; | ||
| const [count, setCount] = createSignal(0); | ||
| const doubled = createComputed(() => count() * 2); | ||
| createEffect(() => console.log('count:', count())); | ||
| batch(() => { | ||
| setCount(1); | ||
| setCount(2); // effect fires once with value 2 | ||
| }); | ||
| ``` | ||
| ### Conditional Rendering | ||
| ```typescript | ||
| import { createSignal, createShow, createSwitch, h } from '@getforma/core'; | ||
| const [loggedIn, setLoggedIn] = createSignal(false); | ||
| // createShow — toggle between two branches | ||
| createShow(loggedIn, | ||
| () => h('p', null, 'Welcome back'), | ||
| () => h('p', null, 'Please sign in'), | ||
| ); | ||
| // createSwitch — multi-branch with caching | ||
| const [view, setView] = createSignal('home'); | ||
| createSwitch(view, [ | ||
| { match: 'home', render: () => h('div', null, 'Home') }, | ||
| { match: 'settings', render: () => h('div', null, 'Settings') }, | ||
| ], () => h('div', null, '404 Not Found')); | ||
| ``` | ||
| ### List Rendering | ||
| ```typescript | ||
| import { createSignal, createList, h } from '@getforma/core'; | ||
| const [items, setItems] = createSignal([ | ||
| { id: 1, name: 'Alice' }, | ||
| { id: 2, name: 'Bob' }, | ||
| ]); | ||
| createList( | ||
| items, | ||
| (item) => item.id, // key function | ||
| (item) => h('li', null, item.name), | ||
| ); | ||
| ``` | ||
| ### Store (deep reactivity) | ||
| ```typescript | ||
| import { createStore } from '@getforma/core'; | ||
| const [state, setState] = createStore({ | ||
| user: { name: 'Alice', prefs: { theme: 'dark' } }, | ||
| items: [1, 2, 3], | ||
| }); | ||
| // Read reactively | ||
| state.user.name; // 'Alice' | ||
| // Mutate — only affected subscribers update | ||
| setState('user', 'name', 'Bob'); | ||
| setState('items', items => [...items, 4]); | ||
| ``` | ||
| ### Components | ||
| ```typescript | ||
| import { defineComponent, onMount, onUnmount, h } from '@getforma/core'; | ||
| const Timer = defineComponent(() => { | ||
| const [seconds, setSeconds] = createSignal(0); | ||
| onMount(() => { | ||
| const id = setInterval(() => setSeconds(s => s + 1), 1000); | ||
| return () => clearInterval(id); // cleanup on unmount | ||
| }); | ||
| return h('span', null, () => `${seconds()}s`); | ||
| }); | ||
| document.body.appendChild(Timer()); | ||
| ``` | ||
| ### Context (Dependency Injection) | ||
| ```typescript | ||
| import { createContext, provide, inject } from '@getforma/core'; | ||
| const ThemeCtx = createContext('light'); | ||
| provide(ThemeCtx, 'dark'); | ||
| const theme = inject(ThemeCtx); // 'dark' | ||
| ``` | ||
| ## Islands Architecture | ||
| For server-rendered HTML, activate independent interactive regions: | ||
| ```typescript | ||
| import { activateIslands, createSignal, h } from '@getforma/core'; | ||
| activateIslands({ | ||
| Counter: (el, props) => { | ||
| const [count, setCount] = createSignal(props.initial ?? 0); | ||
| // Hydrate: attach reactivity to existing server-rendered DOM | ||
| }, | ||
| }); | ||
| ``` | ||
| ```html | ||
| <!-- Server-rendered HTML --> | ||
| <div data-forma-island="Counter" data-forma-props='{"initial": 5}'> | ||
| <span>5</span> | ||
| <button>+1</button> | ||
| </div> | ||
| ``` | ||
| ## Subpath Exports | ||
| | Import | Description | | ||
| |--------|-------------| | ||
| | `@getforma/core` | Signals, `h()`, `mount()`, lists, stores, components | | ||
| | `@getforma/core/runtime` | HTML Runtime — `initRuntime()`, `mount()`, `unmount()` | | ||
| | `@getforma/core/runtime-hardened` | Runtime with `new Function()` locked off (strict CSP) | | ||
| | `@getforma/core/ssr` | Server-side rendering — `renderToString()`, `renderToStream()` | | ||
| | `@getforma/core/tc39` | TC39-compatible `Signal.State` and `Signal.Computed` classes | | ||
| ## Examples | ||
| See the [`examples/`](./examples) directory: | ||
| - **counter** — minimal `h()` counter | ||
| - **counter-jsx** — same counter with JSX syntax | ||
| - **todo** — todo list with `createList` and keyed reconciliation | ||
| - **data-table** — sortable table with `createList` | ||
| ## Ecosystem | ||
| - [forma](https://github.com/getforma-dev/forma) — Rust server framework (forma-ir + forma-server) | ||
| - [forma-tools](https://github.com/getforma-dev/forma-tools) — Build tooling (@getforma/compiler + @getforma/build) | ||
| - [create-forma-app](https://github.com/getforma-dev/create-forma-app) — CLI scaffolder | ||
| | Package | Description | | ||
| |---------|-------------| | ||
| | [@getforma/core](https://www.npmjs.com/package/@getforma/core) | This library — `npm install @getforma/core` | | ||
| | [@getforma/compiler](https://www.npmjs.com/package/@getforma/compiler) | SSR compiler — `.tsx` to FMIR binary | | ||
| | [@getforma/build](https://www.npmjs.com/package/@getforma/build) | esbuild wrapper with JSX + SSR preconfigured | | ||
| | [create-forma-app](https://www.npmjs.com/package/@getforma/create-app) | `npx @getforma/create-app` project scaffolder | | ||
| | [forma](https://github.com/getforma-dev/forma) | Rust server framework (forma-ir + forma-server) | | ||
@@ -49,0 +287,0 @@ ## License |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
3221675
0.23%287
485.71%6
20%