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

Docs/Tooling

FORMA 3.0

Compiler API

Generate paired native and browser products from supported TypeScript components.

View source ↗Markdown ↓Source 753be2a
On this page

npm License: MIT

FMIR compiler for Forma. Emits binary modules for native Rust SSR while browser bundles keep runtime h().

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 target the published, coordinated 3.0.0 package set. Select that version explicitly:

bash
npm install -D @getforma/compiler@3.0.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 3.0 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) 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.

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.0.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.

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.

Find your next answer.

Search Forma 3.0 documentation, guides and examples.