# @getforma/compiler

[![npm](https://img.shields.io/npm/v/@getforma/compiler)](https://www.npmjs.com/package/@getforma/compiler)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

FMIR and client JSX compiler for [Forma](https://github.com/getforma-dev/forma). Emits binary modules for native Rust SSR and lowers eligible browser JSX to reusable HTML templates.

Add the compiler for Rust SSR; browser-only applications run without it.

The Vite plugins (`formaCompiler`, `formaServer`), `compileFormaJSX` and `transformServerFunctions` were removed in 3.0.0. Use `@getforma/build` and authenticated Rust routes. [The old plugin documentation](../../docs/archive/2026-10-15-legacy-apis/packages/compiler/README.md) is archived.

## Install

These examples use the coordinated `3.2.0` package set. Select that version explicitly:

```bash
npm install -D @getforma/compiler@3.2.0
```

## esbuild SSR Plugin — `formaSsrPlugin`

Emits FMIR (Forma Module IR) binary files for Rust-based server-side rendering. Only needed with the full Forma stack (`forma-ir` + `forma-server`).

```ts
import { formaSsrPlugin } from "@getforma/compiler";

// Used by @getforma/build, not typically called directly
```

Use esbuild `write: false`. The plugin appends `<page>.ir` to the returned
`outputFiles`, alongside the browser output, and never writes files itself.
Publish these files only after the complete esbuild call succeeds. `@getforma/build`
handles publication, rollback and watched generations. `write: true` is refused.

One captured source graph produces the FMIR and browser registration metadata.
A distinct `clientEntryPoint` must resolve the same island declarations as
`entryPoint`; matching spelling alone is insufficient. Source/type edits during
compilation refuse the product and can be retried. Browser routes (`ssr: false`)
do not use this transform. The island table lives inside the binary; callers who want it as data use `generateRealIr(entry)`,
which returns `{ binary, islands }`. (Through 0.2.0 the plugin also dropped a
`<page>.islands.json` sidecar into the output directory — build metadata in the
served asset tree, read by nothing but a since-removed `@getforma/build` step.)

### FMIR v3 compatibility

The compiler emits [FMIR v3](../../docs/FMIR-FORMAT.md): a 24-byte header,
a section table, and required bytecode, strings, slots, islands and page props
schema. CRC-32 covers every byte except its own four-byte field. The writer's
base major/minor/patch comes from the compiler package version. A reader accepts
its own major and writer minors up to its own; patch numbers do not restrict
compatibility. `3.0.0-dev` and `3.0.0` both encode `3,0,0`. Upgrade the renderer
first and follow the [compatibility contract](../../docs/compatibility.md).
Use aligned compiler and Rust packages and rebuild older `.ir` files. V2 modules
are refused with a rebuild diagnostic.

`dumpFmir(binary)` exports the same lossless canonical text as Rust's `dump_ir`,
including section flags, unknown optional payloads and island extension bytes.
`IrEmitContext` exposes schema, key-path and source-position writers for tooling.
Production AST compilation carries the declared page and island contracts and the
proved list key paths. Request data is bound through those declarations, not
through guessed numeric slot IDs or anonymous slot names. See
[declared props](../../docs/DECLARED-PROPS.md) and the format document for the
current wire contract.

### Build diagnostics

The [server-renderable subset and diagnostic catalogue](docs/subset.md) define
what the compiler can publish. Unsupported native constructs fail the build with a stable
code, original source location, reason and fix. There is no warning-only SSR mode
or placeholder module. `strict: false` and `FORMA_STRICT=0` remain accepted as
compatibility inputs and cannot downgrade an error.

The direct esbuild plugin applies the same policy. `@getforma/build` collects
diagnostics across entries before refusing publication, writes an optional JSON
report, and retains the previous good output. `collectDiagnostics(() =>
generateRealIr(entry))` remains the low-level inspection API; callers must inspect
its diagnostics before publishing any returned bytes.

A callable list source also needs a native value: use a declared array prop or a
closed literal-default signal. The compiler does not evaluate arbitrary getters,
filters or maps. Retired BUILD-303/504 remain reserved; their historical guidance
is archived. The current list key and row-root proofs are described in the subset.

### Browser-only components and native admission

A component declared `const Chart = browserOnly(ChartImpl)` from
`@getforma/core` must be registered in the compiler-visible `activateIslands`
object. Its typed props must be representable by the declared contract. The
compiler emits its authenticated host and state contract without walking its
implementation. An unsupported native component does not implicitly become such
a host. Unregistered or unrepresentable boundaries fail with BUILD-610; the old
BUILD-706/708 dropping/shell behavior is retired. A route that uses only browser
rendering can select `ssr: false` in `@getforma/build`.

### Native sources and ownership

The compiler follows named source components and supported relative imports and
re-exports, including TypeScript/JSX export aliases. Package implementations,
recursive composition and composition beyond three inlining levels are refused.
The [subset catalogue](docs/subset.md) names the admitted constructs and links a
negative and a passing fixture for every page diagnostic.

Server data enters through typed page and island props. A computed server value
belongs in its own declared prop; arbitrary calls, arithmetic, filters and maps
are not evaluated at request time by the native walker. Literal-default signals
and proved prop-seeded signals can supply initial native values. Closed client
values are not addressable handler slots. Duplicate signal getter names across
scopes still report BUILD-503 and now refuse publication; use distinct getter
names or one shared declaration as appropriate.

`createList` needs a callable source proved to read a declared array prop or
closed literal-default signal, a property-path key, and one stable owned Element
per row. A plain array source is BUILD-616; callable but native-unproved data is
BUILD-610. The native writer and client adoption use the emitted key contract.

A value-bound select must own its option state through the same activation
boundary. Independently registered option/optgroup content under a different
owner refuses with BUILD-617. Ordinary unregistered helpers inside the owning
island and registered content under an unbound select remain admitted. The
independent island-root and finite parser-repair proof remains FORMA-139's
release gate, as recorded in the subset document.

### Closed values and diagnostics

The compiler can fold supported literal and module-constant expressions and
initial signal values. Folding does not turn a direct value into a reactive
binding; use a function where the browser should track it. Scalar text,
attributes and conditions obey [the shared value rules](../../docs/parity-rules.md),
including HTML boolean presence and explicit boolean conditions.

An unknown text/attribute value, unsupported props shape, unresolved component,
or literal past the FMIR string-table limit refuses publication. It does not
publish missing markup or a placeholder and leave the browser to repair it.
Historical slot-injection, warning-only and fallback guidance is preserved in
[the pre-FORMA-101 README](../../docs/archive/FORMA-101-strict-subset/packages/compiler/README.md).

## Public entrypoints

The package root exports compilation (`formaSsrPlugin`, `generateRealIr`,
`formaClientPlugin`, `transformClientTemplates`) and
explicit tooling APIs: `IrEmitContext` and its schema/key-path/source-position
types; `DIAGNOSTIC_CATALOGUE`, `collectDiagnostics`, `diagnosticEntry`,
`formatDiagnostic`, `makeDiagnostic` and their diagnostic types; and FMIR
inspection/encoding (`readFmirContainer`, `dumpFmir`, `encodeFmirSections`,
`crc32`, `baseWriterVersion`, `COMPILER_WRITER_VERSION`, `FmirContainerError`,
`WriterVersion`, `FmirSection`). The release packed-set verifier uses the same
public inspection APIs as consumers.

### Client JSX templates

`formaClientPlugin()` is an esbuild plugin for classic JSX with a named `h`
import from `@getforma/core`. Configure `jsxFactory: 'h'` and register this plugin
alongside `formaSsrPlugin()` when building a native/browser pair. `@getforma/build`
registers it only with `clientTemplates: true`; it is off by default. This option
requires a coordinated compiler/core set. It targets frequently created lists,
where a cached template can repay the helper's download and cleanup cost. Small
islands should retain ordinary `h()` unless their own measurements support opting
in. All route size budgets still apply, including to compiled builds.

The pass hoists escaped, static HTML and child-node binding paths for eligible
subtrees containing at least two elements inside a direct, synchronous inline
`createList` render callback imported from core. The two-argument indexed overload
and the three/four-argument array overload are recognized, including import
aliases. Named row helpers, nested functions and unrelated JSX remain ordinary.
Current eligible elements are ordinary
HTML containers, text formatting, lists, explicit table sections/cells, anchors,
buttons, `br` and `hr`. Attributes are limited to `class`/`className`, `id`,
`title`, `role`, and lowercase `data-*`/`aria-*`. Dynamic properties and a sole
dynamic child use the existing core writers, including reactive function values
and Node/array children. Inline `onClick` functions use delegated bubbling clicks.
Spreads, refs, styles, raw HTML, URL attributes, form controls, custom elements,
foreign namespaces, other event forms, mixed dynamic children and parser-unstable
trees retain ordinary JSX lowering; eligible descendants may still be lowered.

The initializer preserves expression and initial-reader interleaving: a node's
attribute expressions run first, then its children are constructed and initialized,
then its own props and child bindings initialize. Each expression runs once on
only the chosen construction path. Direct `await`/`yield` expressions and async
or generator render callbacks remain ordinary; deferred function bodies are not
moved out of their function. The cached static subtree already has its static
attributes and internal parent/child links during initialization. Code inspecting those links can observe
this intentional difference from nested `h()` construction, which attaches each
child to its parent later.

The factory does no DOM or trust-policy work at module initialization. Its lazy
markup callback uses the existing explicit `trustedHtml` mint for the compiler's
escaped literal; the helper validates that branded value at the template sink.
Adoption and non-HTML fallback execute the original JSX without evaluating the
markup callback.

Compiled clicks use one shared bubbling listener per document. The original
composed path supplies handler order and each handler sees its bound element as
`currentTarget`. Propagation stopped before the document listener prevents these
handlers; detached-tree and noncomposed shadow clicks do not reach it. Removing a
compiled row or disposing its owner revokes its handlers and bindings. This is
the compiled event contract; native event listener options remain available via
ordinary `h()` trees.

Native FMIR is generated from the original source. Adoption description and
non-HTML construction execute the original JSX tree; adopted live updates
and fresh CSR creation retain one ownership/reactive graph. The generated helper
comes from `@getforma/core/compiled`, so use a coordinated core/compiler set that
provides this subpath. Applications without compiled templates do not import it.
Server-adopted lists also describe rows inserted after activation through the
original JSX path. The current clone speedup therefore applies to client-created
lists; it is not an adopted-list optimization.

For bundler integrations, `transformClientTemplates(source, { filename?,
factory?, runtime? })` returns transformed JSX/TSX as a string before JSX lowering.
The defaults are `factory: 'h'` and `runtime: 'transform'`; an imported `h` alias
may be named by `factory`. Unsupported input is returned unchanged. The direct string
API does not capture files or apply namespace wrapping; integrations must retain
their source-generation and namespace guarantees. Prefer the plugin for esbuild.

`generateRealIr(entryPointPath)` drives component analysis, resolution, walking
and emission. Those implementation modules, including `ComponentAnalyzer`,
`walkHTree`, signal scopes and export resolvers, are private. Direct subpath
imports are not package exports. The runtime surface is pinned by
`supported-surface.test.ts`; package verification checks the built artifact.

## When Do You Need This?

| Scenario | Need compiler? |
|----------|---------------|
| Learning FormaJS, building prototypes | No |
| Rust SSR with `forma-server` | Yes — emits FMIR binary |
| HTML Runtime (`data-*` directives) | No — runtime handles everything |

## Peer Dependencies

- `esbuild >=0.17.0` (optional — for esbuild SSR plugin)
- `@getforma/core ^3.2.0` for the current minor. Patch differences are compatible.

## Part of the Forma Stack

### Frontend (TypeScript)

| Package | Description |
|---|---|
| [@getforma/core](https://www.npmjs.com/package/@getforma/core) | Reactive DOM library — signals, h(), islands, SSR hydration |
| [@getforma/compiler](https://www.npmjs.com/package/@getforma/compiler) | **This package** — h() optimization, declared props and islands, IR emission |
| [@getforma/build](https://www.npmjs.com/package/@getforma/build) | Production pipeline — bundling, hashing, compression, manifest |

### Backend (Rust)

| Package | Description |
|---|---|
| [forma-ir](https://crates.io/crates/forma-ir) | FMIR binary format — parser and native HTML walker |
| [forma-server](https://crates.io/crates/forma-server) | Typed SSR and Axum responses — page rendering, asset serving, CSP headers |

### Full Framework

| Package | Description |
|---|---|
| [@getforma/create-app](https://github.com/getforma-dev/forma/tree/main/packages/create-app) | `npx @getforma/create-app` — scaffolds a Rust server + TypeScript frontend project |

## License

MIT


### Paired entry product transport

`formaSsrPlugin` requires esbuild `write: false`. After a successful awaited
`build()` or `rebuild()`, call `getSsrEntryProducts(result)` on that exact result
object. Each immutable record identifies the canonical source/browser entry,
page, IR output path and SHA-256, the emitted island definitions and
`browserRequired`. The latter is based on the compiled definition set, including
conditional islands; an unused registration does not make a static page require
JavaScript. This transport does not itself remove browser code or publish files.
The islands-only entry rewrite below and build graph/manifest filtering
consume these accepted records. The shipped `@getforma/build` integration
publishes the complete product; calling the accessor alone publishes nothing.

The accessor refuses failed results or replaced/mutated IR outputs. It returns
no native records for an explicit Browser-only build; callers must require one
matching record for every configured SSR entry. A failed native compilation
cannot use placeholder IR to establish a static policy.

Never publish from an early `onEnd`: a later plugin may still refuse the result.
A build integration may read records from its final guarded publisher, after all
other validation hooks; commit them together with that cycle's graph and
manifest. Refused watch cycles retain the previously accepted records and files.
The compiler exposes no mutable `onStart` metadata callback.

If source files or their resolutions change during compilation, the compiler
refuses that generation and calls the optional `onSourcesChanged(page)` callback,
including when another compilation error already exists. A custom watch owner
can use this notification to schedule a fresh build after the current cycle
finishes. Do not await `rebuild()` inside `onEnd`; it can join the cycle whose
callback is still running. `@getforma/build` handles this recovery for both split
and non-split builds, coalesces requests, and stops after an unchanged invalid
generation. Cancellation and disposal clear pending recovery work.


### Islands-only browser entries (FORMA-105)

A native entry may select its page with a standalone named core import:

```ts
import { mount, activateIslands } from '@getforma/core';
import { HomePage } from './HomePage';
import { Counter } from './Counter';

activateIslands({ Counter });
mount(HomePage, '#app');
```

The paired compiler authenticates that import, emits the original native page,
and omits the mount statement from the browser product. Only definitions
reachable in the emitted native product remain in the literal activation
registry; conditional islands still require JavaScript. The wrapper uses the
original declared component and captured getters. Imports and module
initialization stay in their original modules; no component is copied or
signal recreated. Ordinary bundler tree shaking removes unused pure page code.

A separate `entryPoint` may supply the compile-time native mount while
`clientEntryPoint` is activation-only. If both entries mount a named page, they
must resolve to the same original declaration, including through aliases or
barrels. All registration identities and canonical contracts must still match,
even for unused registrations. Separate inline page functions cannot prove
that declaration identity; use the activation-only browser shape instead.

`BUILD-611` refuses a used/assigned/exported mount result, nested or multiple
mounts, indirect mount aliases, namespace mount access, effectful target
arguments, and ambiguous activation-only Page imports. A named import alias
(`import { mount as render }`) is supported. An actual core namespace binding
may read a statically named non-mount member, including optional access. Dynamic
members and passing/exporting/aliasing/destructuring the namespace are refused
because they cannot prove the mount is absent. Erased type-only uses remain
valid. This conservative check does not apply to arbitrary user objects.
User functions merely named
`mount` are preserved. Empty activation calls may be omitted only as standalone
top-level statements with pure remaining arguments; required effects must be
explicit statements outside them. Nonempty activation root arguments and
surrounding behavior stay intact. Activation inside the removed page mount is
refused. These errors fail builds with or without strict mode.

A zero-island product records `browserRequired: false`; the publisher must keep
its explicit SSR mode/IR while omitting final JavaScript/module-preload links.
A zero-request route additionally depends on the build manifest and server
response policy; the compiler accessor alone does not establish that behavior. Browser routes (`ssr: false`) remain normal
client mounts and do not use this native entry rewrite.

Multiple native entry plugins in one esbuild operation share a private
compilation coordinator. It waits for every current `onStart` analysis, checks
that overlapping original source snapshots agree, and composes original-range
entry edits before any file is loaded. Each contributor's source and resolution
guards still run before publication; a refused cycle cannot supply partial
transformation authority. Identical edits may be deduplicated; conflicting
ranges refuse instead of choosing the first plugin.

Ordinary sequential reuse of an esbuild options object is supported, as are
concurrent builds with independent options objects and previously completed
contexts that continue rebuilding. If two setups interleave while reusing the
same options object, the grouping is ambiguous and fails with `BUILD-611`.
Use distinct options objects for concurrent setups (and a fresh options object
after an incomplete failed setup). This coordinator does not add a public
combined-plugin API.

The coordinator collects adoption hooks from every original registration before
loading shared modules, then composes those hooks with entry edits in original
coordinates. Shared helper uses with conflicting writable codecs retain their
signal tuples but receive no setter authority. Each module receives one hygienic
hook import, and unchanged native bytes/contracts remain the authority for
browser registration. These compiler hooks feed the shipped strict adoption transaction. Browser
and packed-package checks qualify their integration separately from emission.

## Native navigation owner compilation

`formaSsrPlugin({ navigationOwner: true, ... })` is the supported compiler seam
used by `@getforma/build` navigation groups. It exports the authenticated island
registry instead of activating it and removes the compile-time mount. A static
owner receives an empty registry without requiring `activateIslands({})`.
The separately compiled FMIR remains the source of native structure and props.

An owner may declare one literal empty `div` with `data-forma-outlet="child"`,
outside every registered island. Duplicate, nested, nonempty, dynamic and
island-owned outlets are refused. Successful `getSsrEntryProducts(result)` records
include `navigationOutlet` plus the physical declaration identity and contract for
each emitted island, allowing build to authenticate deduplicated shared islands.
These fields are absent without this opt-in; ordinary static no-JS classification
and existing FMIR bytes are unchanged. Use the build API to obtain the complete
identity, integrity and atomic-publication contract; the low-level plugin alone
does not publish a navigation group.
