Transform Pipeline
bext applies a series of source-code transforms during compilation
to optimize, rewrite, and prepare application code for server-side
rendering. There are now two distinct pipelines: the Next.js
compat path (transforms inherited from the Vercel-compatible build)
and the PRISM compile pass (folds JSX subtrees into HTML strings
before the bundler emits jsx() runtime calls).
Both pipelines live under bext-core::transform, but they run at
different stages and target different framework setups.
Architecture
| Pipeline | Stage | Trigger | Output |
|---|---|---|---|
| Source transforms | Per-file, before bundle | NAPI plugin or bext-server compile call | Modified source string |
| PRISM compile pass | Per-file, before bundle | [framework] type = "prism" + .tsx/.jsx filename |
Source with JSX folded to string concat |
| Post-bundle transforms | After Bun bundles | Bundle output contains useRouter or unpolyfilled process |
Patched JS bundle |
| HTML response transforms | At serve time, on rendered HTML | Always | Streamed HTML |
Key cross-cutting design decisions:
- Lazy AST parsing — the AST is only parsed (via
tsc_rs_parser) when at least one AST-based transform needs it. Files that only need string-based transforms skip parsing entirely. - Cow-based output — pipelines useCow<str>to avoid allocating a new string when no transforms modify the file. - Single-pass where possible —post_processreplaces ~120 regex passes from the original TypeScript implementation with a single linear scan. - Deterministic order — transforms run in fixed order (the "priority" numbers some old docs referenced are aspirational — the actual implementation hardcodes the call sequence inapply_source_transforms). Plugin transforms run after all built-ins.
PRISM compile pass (new)
The first transform a PRISM site sees. Runs before tsc-rs strips
JSX, lifts foldable JSX subtrees out of the source and replaces them
with direct string concatenations. By the time the bundler emits
jsx() runtime calls, most of the work is already done.
Activation rule (in bext-turbopack::direct::transform_with_analysis_opts):
options.jsx_import_source == Some("@bext-stack/framework")
&& filename ends with .tsx | .jsx
&& BEXT_PRISM_COMPILE != "0" | "false" | "off"
Six tiers of folding (additive, default-on):
| Tier | Pattern |
|---|---|
| 1 | Fully-static JSX subtrees → string literal |
| 2 | Static-shape + dynamic JSX child → prefix + (expr) + suffix |
| 3a | Dynamic attr values + ternary class → prefix + __bextEsc(expr) + suffix |
| 4 | array.map(arrow) calls → (arr).map(...).join("") |
| 5 | Zero-prop component inlining (recursive) |
| 5.5 | Prop-bearing component inlining (destructured-param) |
Falls through to runtime jsx() for anything not provably safe to
fold (spread attrs, props.children, style={{...}}, etc.).
Runtime-verified byte-equivalent at every layer; 2.81× faster SSR and 2.71× faster HTTP TTFB measured against the previous PRISM runtime on a 50-row Tailwind table (numbers in the PRISM compile pass doc). The JS runtime got 3-5× faster in commit 2782ae6; the relative speedup of the compile pass over the new runtime is correspondingly smaller, but the compile pass still wins absolutely (14 µs compiled vs ~99 µs interpreted on the jsx-shootout 38 KB fixture).
The pass is implemented in bext-core/src/transform/prism_compile.rs
(~1400 LOC, 28 unit tests). Wired into the bundler at two places:
bext-turbopack/src/direct.rs::compile_closure (entry source) and
transform_with_analysis_opts (transitive imports).
Source transforms (Next.js compat)
Run by bext-core::transform::apply_source_transforms on each file
before bundling. Used by Next.js-compat sites and by bext-plugin's
NAPI path. PRISM sites typically don't need most of these (no
Next.js imports to strip, no process.env.NEXT_PUBLIC_ to inline)
— the pipeline still runs but most transforms early-bail on the
content check.
The transforms execute in fixed order (lower number = earlier); each is idempotent and falls through cleanly when not needed.
0. Flow Extract
bext-core/src/transform/flow_extract.rs — extracts
"use flow" directive definitions from source files and generates
durable flow manifests. Flow functions are rewritten to register
with the flow runtime. AST-based.
// Input
"use flow";
export async function onboardUser(userId) { /* ... */ }
// Output: directive removed, function registered with flow runtime
1. Barrel Optimize
barrel_optimize.rs — converts barrel imports (re-exports from
index files) into direct imports to enable tree-shaking. Most
impactful for icon libraries like lucide-react. String-based.
// Input
// Output
1b. Font Optimize
font_optimize.rs — transforms next/font/google and
next/font/local imports into optimized font loading with preload
hints, font-display: swap, and self-hosted font files. String-based.
2. Import Strip
import_strip.rs — removes imports of server-only packages that
should not appear in client bundles. Uses an Aho-Corasick automaton
(compiled once at startup) for efficient multi-pattern matching
against a configurable banned package list.
3. Shim Inject
shim_inject.rs — strips side-effect imports for boundary markers
("server-only", "client-only") and injects the appropriate
runtime shims.
3. CSS Module
css_module.rs — transforms .module.css imports into CSS
module stubs that export class name mappings. The actual CSS is
extracted and processed separately.
// Input
// Output
const styles = { root: "button_root_a1b2c3", label: "button_label_d4e5f6" };
4. Env Inline
env_inline.rs — replaces process.env.NEXT_PUBLIC_* references
with their literal values using a pre-compiled Aho-Corasick
automaton. Enables dead-code elimination when environment-gated
branches are evaluated at build time.
// Input
if (process.env.NEXT_PUBLIC_FEATURE_FLAG === "true") { /* ... */ }
// Output (NEXT_PUBLIC_FEATURE_FLAG="true")
if ("true" === "true") { /* ... */ }
5. Server Boundary
server_boundary.rs — processes "use server" and "use client"
directives. Server-marked functions are extracted into a server
actions manifest. Client-marked files are split at the boundary for
client-side hydration.
6. Cache Directive
cache_directive.rs — handles the "use cache" directive
(Next.js 15+). Functions marked with "use cache" are wrapped with
ISR cache integration, and their arguments are used as cache keys.
Post-bundle transforms
After Bun bundles the application, a second transform pass runs on
the output. Implemented in bext-core/src/transform/post_process.rs,
which composes:
useRouter Patch
use_router_patch.rs — patches the Next.js useRouter
invariant check that throws when called outside a router context.
Necessary because bext's SSR does not use Next.js's router provider.
Process Polyfill
process_polyfill.rs — prepends a process global polyfill to
bundles that reference process.env outside of the patterns already
handled by Env Inline. Detects whether the polyfill is needed before
injecting to avoid unnecessary overhead.
Bare Specifier Shimming
Catch-all transform (lives in the bundler emit path) that rewrites
any remaining bare specifier imports (imports without ./, ../,
or / prefixes) to point to shimmed modules. React and React DOM
imports are preserved (never shimmed).
RSC directive rewrites (Next.js RSC mode only)
rsc_directives.rs — rewrites server/client directives for the
RSC compile path. Called from bext-server's
ssr_pipeline::rsc_server_compile_native when a route is compiled
under --conditions=react-server for the Flight payload.
HTML response transforms
These run on the rendered HTML response at serve time (in bext-server, not bext-core/transform):
Server Island Replacement
Replaces <bext-island> placeholder elements with their
server-rendered content. Each island is rendered independently and
injected into the HTML stream.
Preload Hint Extraction
Scans <head> for <link rel="stylesheet">, <link rel="preload">,
and <script src="..."> tags to generate HTTP 103 Early Hints
headers. Lets the browser start downloading critical resources
before the full response arrives.
Plugin transforms
Plugin transforms run after all built-in source transforms. They
implement the TransformPlugin trait:
pub trait TransformPlugin: Send + Sync {
fn name(&self) -> &str;
fn priority(&self) -> u32 { 1000 }
fn matches(&self, path: &str, source: &str) -> bool;
fn transform(&self, source: &str, path: &str, ctx: &TransformContext) -> Option;
}
The matches method is called first as a fast path to skip files
that don't need processing. The TransformContext provides
node_env and NEXT_PUBLIC_ environment variables.
Pipeline summary
| Stage | Transform | Crate / file | Type |
|---|---|---|---|
| Per-file | PRISM compile pass | bext-core::transform::prism_compile |
AST-based JSX fold |
| Per-file | Flow Extract | bext-core::transform::flow_extract |
AST |
| Per-file | Barrel Optimize | bext-core::transform::barrel_optimize |
String |
| Per-file | Font Optimize | bext-core::transform::font_optimize |
String |
| Per-file | Import Strip | bext-core::transform::import_strip |
AST + Aho-Corasick |
| Per-file | Shim Inject | bext-core::transform::shim_inject |
String |
| Per-file | CSS Module | bext-core::transform::css_module |
String |
| Per-file | Env Inline | bext-core::transform::env_inline |
Aho-Corasick |
| Per-file | Server Boundary | bext-core::transform::server_boundary |
AST |
| Per-file | Cache Directive | bext-core::transform::cache_directive |
AST |
| Per-file (RSC) | RSC Directives | bext-core::transform::rsc_directives |
String |
| Post-bundle | useRouter Patch | bext-core::transform::use_router_patch |
String |
| Post-bundle | Process Polyfill | bext-core::transform::process_polyfill |
String |
| Post-bundle | Bare Specifier Shim | bext-turbopack::direct |
String |
| Response | Island Replacement | bext-server |
String |
| Response | Preload Extraction | bext-server |
String |
| Anywhere | Plugin Transforms | per plugin | Plugin-defined |
All per-file transforms are automatic and cannot be disabled
individually. If a transform's pre-filter (e.g.
source.contains("import") or source.contains("use server"))
doesn't match the file content, the transform is skipped with zero
cost. Same for the PRISM compile pass — files without < get an
early-bail before the AST parse.
Opting out
| Pipeline | How |
|---|---|
| PRISM compile pass | BEXT_PRISM_COMPILE=0 env var (per-build) |
| Source transforms | Not individually disable-able. Use a different framework type ([framework] type = "static" skips source transforms entirely). |
| Plugin transforms | Don't load the plugin |
Cross-references
- PRISM — the framework that the compile pass targets - PRISM compile pass — full reference for what the pass folds, what it skips, and the perf data - Architecture overview — workspace structure, where each transform lives - Plugins — writing custom transforms