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 use Cow<str> to avoid allocating a new string when no transforms modify the file. - Single-pass where possiblepost_process replaces ~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 in apply_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