
Research
TeamPCP Compromises Telnyx Python SDK to Deliver Credential-Stealing Malware
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.
@getforma/core
Advanced tools
Real DOM reactive library — fine-grained signals, islands architecture, SSR hydration. No virtual DOM, no diffing. ~15KB gzipped.
Reactive DOM library with fine-grained signals. No virtual DOM — h() creates real elements, signals update only what changed. ~15KB gzipped.
npm install @getforma/core
Or use the CDN (no build step required):
<script src="https://unpkg.com/@getforma/core/dist/formajs-runtime.global.js"></script>
After npm install, you need a bundler to resolve the ES module imports. Here's a minimal Vite setup:
npm install @getforma/core
npm install -D vite
<!-- index.html -->
<div id="app"></div>
<script type="module" src="./main.ts"></script>
// main.ts
import { createSignal, h, mount } from '@getforma/core';
const [count, setCount] = createSignal(0);
mount(() =>
h('button', { onClick: () => setCount(count() + 1) },
() => `Clicked ${count()} times`
),
'#app'
);
npx vite
For esbuild, tsup, or other bundlers — no special config is needed. FormaJS ships standard ESM and CJS via package.json exports.
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:
h('div') returns an actual HTMLDivElement. Signals mutate it directly. No diffing pass, no reconciliation overhead for simple updates. Inspired by Solid.h() hyperscript (like Preact), or JSX. All share the same signal graph. Pick the right tool for the job, upgrade without rewriting.new Function() as a fallback for complex expressions; the hardened build (forma.hardened.js) locks it off entirely for strict CSP environments.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.
Drop a script tag, write data-* attributes. Zero config, zero tooling.
<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>
| 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" |
data-ref | Register element for $refs access | data-ref="myInput" |
$el | Reference to the current DOM element | data-on:click="{$el.classList.toggle('active')}" |
$dispatch | Fire a CustomEvent (bubbles, crosses Shadow DOM) | data-on:click="{$dispatch('selected', {id: itemId})}" |
$refs | Named element references | data-on:click="{$refs.myInput.focus()}" |
CSP-safe expression parser — no eval() or new Function() by default. For strict CSP environments, use the hardened build:
<script src="https://unpkg.com/@getforma/core/dist/formajs-runtime-hardened.global.js"></script>
h()npm install @getforma/core
import { createSignal, h, mount } from '@getforma/core';
const [count, setCount] = createSignal(0);
mount(() =>
h('button', { onClick: () => setCount(count() + 1) },
() => `Clicked ${count()} times`
),
'#app'
);
Same h() function, JSX syntax. Configure your bundler:
// tsconfig.json
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}
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');
<!-- unpkg -->
<script src="https://unpkg.com/@getforma/core@1.0.0/dist/formajs-runtime.global.js"></script>
<!-- jsDelivr (faster globally) -->
<script src="https://cdn.jsdelivr.net/npm/@getforma/core@1.0.0/dist/formajs-runtime.global.js"></script>
<script type="module">
import { createSignal, h, mount } from 'https://cdn.jsdelivr.net/npm/@getforma/core@1.0.0/dist/index.js';
const [count, setCount] = createSignal(0);
mount(() => h('button', { onClick: () => setCount(count() + 1) }, () => `${count()}`), '#app');
</script>
| Build | Filename |
|---|---|
| Standard (recommended) | formajs-runtime.global.js |
CSP-safe (no new Function) | formajs-runtime-hardened.global.js |
| Standard (short alias) | forma-runtime.js |
| CSP-safe (short alias) | forma-runtime-csp.js |
Available from both unpkg.com/@getforma/core@VERSION/dist/ and cdn.jsdelivr.net/npm/@getforma/core@VERSION/dist/.
For production, always pin the version (e.g., @1.0.0). Unversioned URLs resolve to latest.
The CSP build uses a hand-written expression parser and never calls
new Function. It supports most common patterns. See examples/csp for a working demo.
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
});
Skip updates when the new value is equal to the current value:
const [pos, setPos] = createSignal(
{ x: 0, y: 0 },
{ equals: (a, b) => a.x === b.x && a.y === b.y },
);
setPos({ x: 0, y: 0 }); // skipped — values are equal
setPos({ x: 1, y: 0 }); // applied — values differ
The computed getter receives the previous value for efficient diffing:
const changes = createComputed((prev) => {
const current = items();
if (prev) console.log(`${prev.length} → ${current.length} items`);
return current;
});
Type guards and utilities from alien-signals 3.x:
import { isSignal, isComputed, getBatchDepth, trigger } from '@getforma/core';
isSignal(count); // true — is this a signal getter?
isComputed(doubled); // true — is this a computed value?
getBatchDepth(); // 0 outside batch, 1+ inside
trigger(doubled); // force recomputation even if deps unchanged
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'));
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),
);
import { createStore } from '@getforma/core';
const [state, setState] = createStore({
user: { name: 'Alice', prefs: { theme: 'dark' } },
items: [1, 2, 3],
});
// Read reactively — tracked at the exact property path
state.user.name; // 'Alice'
state.items[0]; // 1
// Setter API — partial object merge (batched)
setState({ user: { ...state.user, name: 'Bob' } });
setState(prev => ({ items: [...prev.items, 4] }));
// Or mutate directly via proxy — only affected subscribers update
state.user.name = 'Bob'; // only "user.name" subscribers notified
state.items.push(4); // array mutation batched automatically
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());
onMount vs onUnmountonMount(fn) — runs after the component's DOM is created. If fn returns a function, that function is automatically registered as an unmount callback.onUnmount(fn) — explicitly registers a cleanup function that runs when the component is disposed.Both mechanisms feed into the same cleanup queue — the onMount return shorthand is convenience for the common pattern of setting up and tearing down in one place:
// These are equivalent:
onMount(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
});
// vs.
onMount(() => {
const id = setInterval(tick, 1000);
onUnmount(() => clearInterval(id));
});
mount() fails fast. If the container selector doesn't match any element, it throws:
mount(() => h('p', null, 'hello'), '#nonexistent');
// Error: mount: container not found — "#nonexistent"
Global error handler. Register a handler for errors in effects and lifecycle callbacks:
import { onError } from '@getforma/core';
onError((error, info) => {
console.error(`[${info?.source}]`, error);
});
Error boundaries. Catch rendering errors and display fallback UI with a retry option:
import { createErrorBoundary, h } from '@getforma/core';
createErrorBoundary(
() => h(UnstableComponent),
(error, retry) => h('div', null,
h('p', null, `Something went wrong: ${error.message}`),
h('button', { onClick: retry }, 'Retry'),
),
);
import { createContext, provide, inject } from '@getforma/core';
const ThemeCtx = createContext('light');
provide(ThemeCtx, 'dark');
const theme = inject(ThemeCtx); // 'dark'
import { createHistory } from '@getforma/core';
const [state, setState, { undo, redo, canUndo, canRedo }] = createHistory({ text: '' });
setState({ text: 'hello' });
setState({ text: 'hello world' });
undo(); // state.text === 'hello'
canUndo(); // true
redo(); // state.text === 'hello world'
import { createReducer } from '@getforma/core';
const [state, dispatch] = createReducer(
(state, action) => {
switch (action.type) {
case 'INCREMENT': return { count: state.count + 1 };
case 'DECREMENT': return { count: state.count - 1 };
default: return state;
}
},
{ count: 0 },
);
dispatch({ type: 'INCREMENT' }); // state() === { count: 1 }
For server-rendered HTML, activate independent interactive regions. Each island callback receives the root DOM element and parsed props, then returns a component tree — the same h() calls you'd use for client-side rendering. The hydration system walks the descriptor tree against the existing SSR DOM, attaching event handlers and reactive bindings without recreating elements.
import { activateIslands, createSignal, h } from '@getforma/core';
activateIslands({
Counter: (el, props) => {
const [count, setCount] = createSignal(props?.initial ?? 0);
// el is the island's root HTMLElement — useful for layout measurement,
// focus management, CSS classes, or reading extra data-* attributes.
el.classList.add('is-hydrated');
// Return the same tree shape as the SSR output.
// Hydration matches this against existing DOM — no elements are created.
return h('div', null,
h('span', null, () => String(count())),
h('button', { onClick: () => setCount(c => c + 1) }, '+1'),
);
},
});
<!-- Server-rendered HTML -->
<div data-forma-island="0" data-forma-component="Counter" data-forma-props='{"initial": 5}'>
<span>5</span>
<button>+1</button>
</div>
Each island is activated inside its own createRoot scope with error isolation — a broken island never takes down its siblings.
Control when an island hydrates via data-forma-hydrate:
| Trigger | When it hydrates | Use case |
|---|---|---|
load (default) | Immediately on page load | Above-the-fold interactive content |
visible | When island enters viewport | Below-the-fold components |
idle | During browser idle time (requestIdleCallback) | Non-critical functionality |
interaction | On first pointerdown or focusin | Skeleton+skin pattern |
<div data-forma-island="1" data-forma-component="Comments" data-forma-hydrate="visible">
<!-- Only loads JS when scrolled into view -->
</div>
When swapping module content (e.g., inside <forma-stage> Shadow DOM), dispose islands to prevent leaked effects and listeners:
import { deactivateIsland, deactivateAllIslands } from '@getforma/core';
// Dispose all active islands under a root
deactivateAllIslands(shadowRoot);
// Or dispose a single island
deactivateIsland(islandElement);
The main entry point (@getforma/core) has zero network code — no fetch, no WebSocket, no process.env. Network-capable modules are separate subpath imports.
| Import | Description |
|---|---|
@getforma/core | Signals, h(), mount(), lists, stores, components, islands, events, DOM utils |
@getforma/core/http | createFetch, fetchJSON, createSSE, createWebSocket |
@getforma/core/storage | createLocalStorage, createSessionStorage, createIndexedDB |
@getforma/core/server | createAction, $$serverFunction, handleRPC, createRPCMiddleware |
@getforma/core/runtime | HTML Runtime — initRuntime(), mount(), unmount() |
@getforma/core/runtime/global | HTML Runtime global build (IIFE, for <script> tags) |
@getforma/core/runtime-hardened | Runtime with new Function() locked off (strict CSP) |
@getforma/core/runtime-csp | Alias for runtime-hardened (CSP-safe build) |
@getforma/core/runtime-csp/global | CSP-safe global build (IIFE, for <script> tags) |
@getforma/core/ssr | Server-side rendering — renderToString(), renderToStream() |
@getforma/core/tc39 | TC39-compatible Signal.State and Signal.Computed classes |
// Core (zero network code)
import { createSignal, h, mount, createStore } from '@getforma/core';
// HTTP (only if needed)
import { createFetch, createSSE } from '@getforma/core/http';
// Storage (only if needed)
import { createLocalStorage } from '@getforma/core/storage';
// Server (only if needed)
import { createAction, $$serverFunction } from '@getforma/core/server';
See the examples/ directory:
h() countercreateList and keyed reconciliationcreateListFormaJS shares Solid's core insight — fine-grained signals updating the real DOM without a virtual DOM. If you know Solid, you'll feel at home. The differences are in scope and delivery:
| Solid | FormaJS | |
|---|---|---|
| Build requirement | Always needs a compiler (JSX transform) | CDN runtime works with zero build step; bundler is optional |
| Entry points | JSX-first | HTML Runtime (data-* attributes), h() hyperscript, or JSX |
| CSP | Relies on compiler output | Hand-written expression parser; hardened build has no new Function() |
| Islands | Via solid-start meta-framework | Built-in activateIslands() — no meta-framework needed |
| Ecosystem | Mature (router, meta-framework, devtools) | Minimal — reactive core only, you bring the architecture |
| SSR runtime | Node.js required | Node.js via renderToString, or Rust walker (no JS runtime on the server) |
| Size | ~7KB | ~15KB (includes runtime parser, stores, SSR) |
When to choose FormaJS: You want islands hydration built into the library, not bolted on through a meta-framework. You need CSP compliance without a build step. You want three entry points (CDN, hyperscript, JSX) sharing one signal graph. Or you're building on a Rust backend and want your frontend reactive layer to integrate natively with the server stack.
When to choose Solid: You want a mature JavaScript ecosystem with routing, SSR meta-framework (SolidStart), devtools, and community-built component libraries. Your backend is Node.js and you want SSR in the same language as your frontend.
FormaJS is the reactive layer of the Forma stack. The full pipeline compiles components to a binary IR (FMIR), renders them in Rust via
forma-ir, and serves pages throughforma-server— SSR without Node.js, binary IR over the wire, deployed for ~$18/month.
Some features are more battle-tested than others:
| Feature | Status | Notes |
|---|---|---|
Signals (createSignal, createEffect, createComputed, batch) | Stable | Core primitive, well-tested. Custom equals option supported. |
Reactive introspection (isSignal, isComputed, trigger, getBatchDepth) | Stable | alien-signals 3.x type guards |
h() / JSX rendering | Stable | |
mount(), createShow, createSwitch, createList | Stable | |
HTML Runtime (data-* directives) | Stable | Expression parser covers common patterns |
| CSP-hardened runtime | Stable | No new Function(), tested with strict CSP headers |
createStore (deep reactivity) | Stable | |
Components (defineComponent, lifecycle) | Stable | |
Context (createContext, provide, inject) | Stable | |
Islands (activateIslands, disposal, triggers) | Stable | 10 activation + 88 hydration + 10 trigger tests |
createHistory (undo/redo) | Stable | |
createReducer | Stable | 8 tests |
data-fetch, data-transition:* | Stable | Fully implemented in HTML Runtime |
SSR (renderToString, renderToStream) | Beta | Functional, API may evolve |
TC39 Signals compat (Signal.State, Signal.Computed) | Beta | 9 tests, but tracks an evolving TC39 proposal |
FormaJS is the reactive frontend layer of a full-stack Rust + TypeScript framework. The pipeline flows: TypeScript components → @getforma/compiler → FMIR binary → forma-ir (parse) → forma-server (render) → Axum HTTP response.
| Package | Language | Description |
|---|---|---|
| @getforma/core | TypeScript | This library — reactive DOM, signals, islands, SSR hydration |
| @getforma/compiler | TypeScript | TypeScript-to-FMIR compiler, Vite plugin, esbuild SSR plugin |
| @getforma/build | TypeScript | esbuild pipeline with content hashing, compression, manifest |
| @getforma/create-app | TypeScript | npx @getforma/create-app — scaffold a new Forma project |
| forma-ir | Rust | FMIR binary format: parser, walker, WASM exports |
| forma-server | Rust | Axum middleware for SSR page rendering, asset serving, CSP |
MIT
FAQs
Real DOM reactive library — fine-grained signals, islands architecture, SSR hydration. No virtual DOM, no diffing. ~15KB gzipped.
The npm package @getforma/core receives a total of 88 weekly downloads. As such, @getforma/core popularity was classified as not popular.
We found that @getforma/core demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

Security News
TeamPCP is partnering with ransomware group Vect to turn open source supply chain attacks on tools like Trivy and LiteLLM into large-scale ransomware operations.

Security News
/Research
Widespread GitHub phishing campaign uses fake Visual Studio Code security alerts in Discussions to trick developers into visiting malicious website.