Skip to content
forma.
v3.2.0
☰
Browse documentation
Start here
Application guides
Architecture
Build interfaces
Native rendering
Tooling
Security and operations
API reference

Docs/Tooling

FORMA 3.2

Compiler API

Generate paired native and browser products from supported TypeScript components.

View source ↗Markdown ↓Source fc76ea8
On this page

npm License: MIT

FMIR and client JSX compiler for 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 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: 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. 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 and the format document for the current wire contract.

Build diagnostics#

The server-renderable subset and diagnostic catalogue 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 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, 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.

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?#

ScenarioNeed compiler?
Learning FormaJS, building prototypesNo
Rust SSR with forma-serverYes — 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)#

PackageDescription
@getforma/coreReactive DOM library — signals, h(), islands, SSR hydration
@getforma/compilerThis package — h() optimization, declared props and islands, IR emission
@getforma/buildProduction pipeline — bundling, hashing, compression, manifest

Backend (Rust)#

PackageDescription
forma-irFMIR binary format — parser and native HTML walker
forma-serverTyped SSR and Axum responses — page rendering, asset serving, CSP headers

Full Framework#

PackageDescription
@getforma/create-appnpx @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.

Find your next answer.

Search Forma documentation, guides and examples.