The compiler
Reze's Vite plugin compiles JSX into DOM creation and reactive bindings. Work that can be determined from source is performed during compilation; the browser executes the resulting JavaScript and the runtime helpers it imports.
This is a compilation requirement, not an optional optimization. Compiler syntax and control-flow tags are not ordinary runtime functions.
The compiler parses and prints JavaScript through Oxc. Reactive syntax, props, and control flow are normalized before a shared view representation is lowered for the selected target. Source maps point back to the input supplied to the compiler, so upstream JSX preprocessors can compose their maps.
The frontend checks its rewrite plans before rebuilding semantic information. Plain JSX without normalization work reuses the first pass's scopes and node facts.
Static template strings are shared when doing so reduces emitted code size. Each use still creates its own DOM nodes. Compiler-only imports are removed after lowering; ordinary calls and re-exports retain the imports they use. Generated variable names and formatting are not a public API.
Browser parser normalization also happens at compile time. Table structure, raw-text elements, and HTML/SVG/MathML boundaries use the same layout for client rendering and hydration. Ordinary trees bypass the HTML parser; lowercase tag names are inspected without allocating normalized copies. Static node accessors reuse captured siblings along ancestor paths instead of walking from the root again.
Known string-valued classes use a direct attribute update. They do not pull the object/array class-merging helper or its per-element bookkeeping into an ordinary client bundle. JSX returned directly by a function or concise arrow is lowered into that function's body rather than adding a per-call wrapper closure.
Framework runtime package entrypoints are dependencies, not compiler input. The Vite plugin leaves them uncompiled even when workspace links resolve outside node_modules.
Compiler syntax versus runtime APIs
Compiler syntax lets a reactive value look like a local variable:
import { $computed, $signal } from "reze-js";
export function Counter() {
let count = $signal(0);
const doubled = $computed(count * 2);
return (
<button onClick={() => (count += 1)}>
{count} × 2 = {doubled}
</button>
);
}
The compiler rewrites reads and assignments of the declared binding. It also wraps the expression passed to $computed in a tracked computation. The equivalent public runtime model uses explicit getters and setters from @rezejs/signals:
import { computed, signal } from "@rezejs/signals";
export function Counter() {
const [count, setCount] = signal(0);
const doubled = computed(() => count() * 2);
return (
<button onClick={() => setCount(count() + 1)}>
{count()} × 2 = {doubled()}
</button>
);
}
This illustrates the API model, not an exact dump of generated code. The ordinary APIs work without rewriting their calls; JSX still needs compilation.
Declaration and module boundaries
Declare $signal and $computed with a single identifier in a let or const declaration. They cannot be used as arbitrary expressions, destructured declarations, or values passed to another function.
The rewritten binding is local to the file that declares it. Do not export it directly. Export an explicit reading function instead:
import { $signal } from "reze-js";
let count = $signal(0);
export const readCount = () => count;
export function increment() {
count += 1;
}
Alternatively, export ordinary signal getters and setters from @rezejs/signals. A consumer must call the getter in a tracked computation or JSX binding to observe future changes. Passing readCount() to an ordinary function passes the current value, not a live subscription.
Additional restrictions:
- A computed binding is read-only; update its source state instead.
$computedaccepts an expression, not an inline function literal. Use$computed(calculate())for a multi-statement calculation.- Write signals through direct assignments, not destructuring assignments or
forloop heads. - Increment and decrement statements are supported, but
count++inside a larger expression is rejected. Usecount += 1or a block-bodied handler.
See reactivity for the state model and async computations.
JSX intrinsics
Native tags such as button describe DOM elements. Their TypeScript attributes are described by Reze's JSX.IntrinsicElements. Component functions use their own props types.
Reze also provides compiler-recognized control-flow tags:
import { $signal, Show } from "reze-js";
export function Disclosure() {
let open = $signal(false);
return (
<section>
<button onClick={() => (open = !open)}>Toggle details</button>
<Show when={open} fallback={<p>Details are hidden.</p>}>
<p>Details are visible.</p>
</Show>
</section>
);
}
These tags are compiled into control-flow helpers. Use them as JSX tags: do not call, alias as a runtime component value, or re-export the imported intrinsic. Their accepted attributes are fixed; arbitrary attributes and spreads are errors.
The interface guide explains conditional rendering, lists, boundaries, and portals.
Transformation scope
By default, the Vite plugin processes .ts, .tsx, .js, .jsx, .mts, .cts, .mjs, and .cjs, excluding files in node_modules. It compiles the client environment and, when SSG is enabled, the build-time HTML environment.
Preprocessors must emit JSX before Reze runs. Extend the default extensions when adding another source format:
import reze, { DEFAULT_ROUTE_EXTENSIONS } from "@rezejs/vite-plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [reze({ extensions: [...DEFAULT_ROUTE_EXTENSIONS, ".mdx"] })],
});
This only enables the extension; an MDX-to-JSX preprocessor must also be configured to run first. A nonempty extensions list replaces the defaults rather than adding to them.
Direct compiler API
Build tools can call the native compiler without the Vite plugin:
import { compile } from "@rezejs/compiler";
const result = compile("export const App = () => <main>Hello</main>;", "src/App.tsx", { target: "client", sourceMap: true });
compile(source, filename, options?) returns null when the module needs no transformation. Otherwise it returns code, an optional source-map JSON string in map, and diagnostics. Compilation errors return diagnostics without code; check their severity before passing output to the next transform.
In this repository, pnpm build always invokes NAPI/Cargo for the native compiler. Cargo handles incremental compilation; Turbo tracks both Rust crates, workspace manifests, and the toolchain without caching the platform-specific native task.
| Option | Contract |
|---|---|
target | "client" by default; "hydrate" and "html" select the other backends. |
moduleId | A nonempty, stable module identity required by "hydrate" and "html". Ordinary client compilation only needs filename. |
sourceMap | Generate a version 3 source map; enabled by default. |
debugNames | Include names for profiling; disabled by default. |
hot | Generate component hot-update registration; disabled by default. |
links | Module exporting the router link helper for compiler-managed native anchors. |
profile | Optional profiling facts; mismatched source hashes or schema versions are ignored. |
All targets produce code, not a precomputed HTML string. "client" creates browser DOM, "hydrate" binds existing page DOM, and "html" produces executable code for the build-time HTML runtime. The HTML target is not browser code or a public request-time rendering API.
The HTML and hydration compilations of a module must use the same canonical moduleId and the same compiler input, including the output of JSX-producing preprocessors. Use POSIX project-relative paths, or a package name plus package-relative path for external packages; do not derive identity from an absolute machine path. Missing identity reports MISSING_MODULE_ID, even when the source otherwise needs no transformation.
Site identities use the first 16 lowercase hexadecimal characters of BLAKE3 over the UTF-8 bytes of moduleId + "\0" + compiler input, followed by the site's base-36 ordinal. The build registry rejects differing inputs for one module identity and hash collisions between module identities.
Diagnostics
Diagnostics include a bracketed code, source location, and repair information. Errors stop the transform; warnings are reported through Vite. Fix the cause instead of bypassing the transform.
| Code | Meaning | Correction |
|---|---|---|
SIGNAL_NOT_DECLARED | Compiler syntax has no supported local declaration. | Initialize one named variable with let or const. |
SIGNAL_EXPORTED | A rewritten reactive binding is exported. | Export a getter or use the ordinary runtime API. |
COMPUTED_FUNCTION | $computed received an inline function. | Pass the expression directly. |
COMPUTED_WRITTEN | Derived state is being assigned. | Update its source signals. |
CONTROL_FLOW_AS_VALUE | An intrinsic is used outside a JSX tag. | Render the tag inside an ordinary component. |
CONTROL_FLOW_ATTRIBUTE | An intrinsic received an unsupported attribute. | Remove it or use the documented attribute. |
To capture all diagnostic severities, including informational diagnostics:
import reze from "@rezejs/vite-plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [reze({ diagnostics: { jsonl: "diagnostics.jsonl" } })],
});
The file is appended to, not replaced. Entries include the code, severity, message, source locations, enclosing path, labels, fixes, and rendered explanation. The installed compiler includes a repair guide at node_modules/@rezejs/compiler/skills/reze-compiler-diagnostics/SKILL.md.
Development and production
The plugin enables debug names outside production and compiler HMR support when serving with Vite HMR enabled. Source maps follow Vite's source-map configuration.
Use a production build when measuring shipped code. Generated output depends on the application and enabled features; architecture alone is not a bundle-size or speed measurement.
Next: Vite configuration, reactivity, and installation.