@fluixi/compiler
v1.0.0-alpha.97
Published
compiles JSX and html`` templates to direct DOM calls via shared IR; Babel is used as parser only
Readme
@fluixi/compiler
The Fluixi compiler — parses, lowers and emits. Not a Babel plugin.
✨ Overview
@fluixi/compiler turns Fluixi source into fine-grained reactive output. Both authoring
syntaxes — JSX and html`...` templates — lower to the same host-agnostic IR, and
the imperative backend compiles that straight into @fluixi/dom runtime
calls (createNativeElement/spread/insert/createComponent/…). No VDOM, and no
jsx() dispatch at runtime.
It owns the transform rather than running inside someone else's: parse once, splice the emitted code over the spans it replaces. Nothing is reprinted, so a hole keeps the text the author wrote and every edit carries a source map; types are left in place for the bundler to strip.
@babel/parser is the only package it takes from that namespace, used as a parser — the
same position Vue's compiler is in. @babel/core and the plugin machinery are optional
peers, needed only by babel: true.
The @fluixi/jsx automatic runtime is still what esbuild and TypeScript's
jsxImportSource point at; this compiler simply never emits a call into it.
It also provides the "use server" transform (server functions): the client build
gets an RPC stub, the server build keeps and registers the body.
Most apps don't use this package directly — @fluixi/vite-plugin (and
@fluixi/start) wrap it.
What it does to your code
Beyond turning markup into DOM calls:
Props you destructured stay reactive. function Card({ title }) compiles to
props.title at each use, so the binding keeps updating, and ...rest becomes the
splitProps call you would have written. Anything it can't prove safe — a reassigned
binding, a computed key, a default it would have to re-run per read — is left exactly as
written and reported. On by default; set propsDestructure: false for a package that ships
its source for consumers to compile, since the rewrite belongs to whoever compiles.
$ intrinsics resolve without an import. $signal, $memo, $effect, $store,
$resource, $selector, $deferred, $untrack, $untrackStore compile to the runtime
calls they stand for, and the import is added for what a module actually used. A primitive
is recognised by the binding it came from, never by name: import { signal as state } is
recognised, a local function signal() is not. A module that binds a $ name keeps its
own meaning and gets a diagnostic.
Unimported tags resolve through rules. resolve says where a capitalised tag comes
from — an entry per component, or one pattern for a family:
createVitePlugin({ resolve: [{ match: /^([A-Z]\w+)$/, module: '/src/components/$1.tsx' }] })load: splits a component into its own chunk. load:visible, load:idle,
load:interaction, load:media decide when its module is fetched. It works on a component
a rule supplies and on one you imported yourself — in the second case the static import is
removed, which only happens when the name is used nowhere but as a tag.
It says when it couldn't do these things. A tag nothing supplies and a load: that
can't apply both build cleanly and fail at runtime, so they're reported at build time with
the reason. The bundler adapters print them; TransformResult carries them as
propsDiagnostics, intrinsicDiagnostics, loadDiagnostics and unresolvedDiagnostics
if you're driving the transform yourself.
📦 Installation
pnpm add -D @fluixi/compiler🚀 Usage
Via the Vite integration (the common path):
import { defineConfig } from 'vite';
import { createVitePlugin, serverFunctionsVitePlugin } from '@fluixi/compiler/integrations';
export default defineConfig({
plugins: [
serverFunctionsVitePlugin(), // "use server" transform
createVitePlugin(),
],
});Or compile a module directly:
import { transformTemplates } from '@fluixi/compiler/transform';
const { code, map } = transformTemplates(source, 'App.tsx', {})!;transformTemplates returns null when there is nothing to do — no JSX, no templates, no
$ intrinsics and nothing to report — so a bundler can leave the file untouched.
Compiling through Babel
fluixi({ babel: true }) routes through the Babel front-end instead. It exists for a
project whose own Babel plugins have to see the compiled output; it needs @babel/core
and friends installed, and says so rather than degrading. Everything else — the IR, the
passes, the backend — is the same code either way.
🔌 Subpaths
| Import | What | Babel |
| --- | --- | --- |
| @fluixi/compiler | IR, codegen backends, resolve/optimize passes, options | none |
| @fluixi/compiler/builtins | the control-flow / router component names | none |
| @fluixi/compiler/transform | transformTemplates, transformServerFns | parser only |
| @fluixi/compiler/integrations | vite, webpack, rollup and esbuild adapters | parser only |
| @fluixi/compiler/babel | the Babel plugin front-end | @babel/core |
no-babel-in-core.spec.ts fails the build if that column stops being true.
