Choose your build pipeline
Build tools
Use the same Vite integration from a client-only SPA through a routed SSR app, or choose the matching Rspack or Rsbuild integration when those bundlers own your pipeline.
Octane has one compiler with thin bundler integrations. Choosing a bundler changes the build pipeline, not component semantics.
Choose an integration
| Goal | Integration |
|---|---|
| Vite app (SPA or routed SSR) | @octanejs/vite-plugin |
| Rspack SPA or custom server pipeline | @octanejs/rspack-plugin |
| Full Rsbuild app | @octanejs/rsbuild-plugin |
All integrations use the same compiler, runtime selection, source maps, raw Octane dependency discovery, and cache dependencies. Choosing a bundler does not change component semantics.
All three integrations also accept profile: true for a client profiling
build. See Profiling for Chrome tracks, render causes, the
console API, and the recommended production-style workflow.
Vite
Use @octanejs/vite-plugin for every Vite app:
pnpm add octane @octanejs/vite-plugin
pnpm add -D vite// vite.config.ts
import { defineConfig } from 'vite';
import { octane } from '@octanejs/vite-plugin';
export default defineConfig({
plugins: [octane()],
});Without octane.config.ts, the plugin compiles Octane source and leaves Vite's
normal client-only SPA behavior intact, including its import.meta.hot HMR
dialect. Add an Octane config with routes to activate the full app layer:
routing, streaming SSR, hydration, production client/server builds, and preview.
Raw-source Octane packages are discovered recursively. A binding whose dependency must stay outside Vite's rolling dependency optimizer can declare that package-owned constraint in its manifest:
{
"octane": {
"vite": {
"optimizeDeps": {
"exclude": ["identity-sensitive-core", "@identity-sensitive/*"]
}
}
}
}The Vite adapter adds exact entries to optimizeDeps.exclude automatically and
expands a terminal family/* rule to matching packages declared by the binding or
application. This metadata is for raw binding authors; applications consuming the
binding do not need to repeat it in vite.config.ts.
Rspack
The Rspack package is deliberately low-level. It compiles .tsrx, eligible
Octane .tsx, and raw Octane .ts/.js dependency sources, but leaves HTML,
routing, and server orchestration to your application.
pnpm add octane
pnpm add -D @rspack/core @octanejs/rspack-plugin// rspack.config.mjs
import { OctaneRspackPlugin } from '@octanejs/rspack-plugin';
export default {
entry: './src/main.tsrx',
plugins: [new OctaneRspackPlugin()],
};The plugin infers client or server compilation from standard Rspack targets. Set it explicitly for an unusual target or a multi-compiler setup:
new OctaneRspackPlugin({ environment: 'server' });Server compilation resolves exact bare octane imports to octane/server.
Client compilation uses the DOM runtime and emits import.meta.webpackHot HMR
handoff code when Rspack's hot loader context is active. Set hmr: false to
disable component handoff, or transpile: false if another rule already strips
TypeScript. The loader-only export at @octanejs/rspack-plugin/loader is
available for custom rule composition.
Rsbuild
Rsbuild is the closest equivalent to the full Vite metaframework integration. Its Environment API builds a browser hydration environment and a Node SSR environment with the correct Octane runtime in each.
pnpm add octane @octanejs/rsbuild-plugin
pnpm add -D @rsbuild/core// rsbuild.config.ts
import { defineConfig } from '@rsbuild/core';
import { pluginOctane } from '@octanejs/rsbuild-plugin';
export default defineConfig({
plugins: [pluginOctane()],
});Without octane.config.ts, pluginOctane() behaves as a compiler integration
and preserves your own entries. With routes in octane.config.ts, it owns the
web and Node entries required for SSR and hydration.
The shared build.target setting controls both SWC application transforms and
Rspack's generated runtime. Rsbuild accepts one ES level, modules, false, or
esbuild-style browser targets such as ['chrome100', 'firefox100']; do not mix
ES levels and browser targets in the same array.
Mixed toolchains and file ownership
By default the Octane compiler owns every project .tsrx and .tsx module. In
a codebase where another framework's toolchain also compiles .tsx — for
example a React app hosting Octane islands through octane/react, or any other
framework sharing the repository — that default would send the host framework's
modules through the wrong compiler.
Set requireDirective: true (accepted by all three integrations) to split
ownership explicitly. The rule is short:
.tsrxis always Octane's. The extension itself is the marker — nothing else compiles the syntax, so the files need no annotation at all.- Everything else opts in with the pragma. A
.tsx,.ts, or.jsis Octane's only if it opens with a leading/** @jsxImportSource octane */pragma comment — full compilation for.tsx, octane hook slotting for.ts/.js; without the pragma it passes through untouched to the host framework's own pipeline.
In a .tsx the pragma is the same comment TypeScript already reads for
per-file JSX typing, so one marker does both jobs: it types the file against
octane's JSX and tells Octane's bundler integrations that this module is
theirs to compile. In a JSX-less .ts/.js module TypeScript ignores the
pragma, so there it acts purely as the Octane ownership marker. It must be
leading — at the very top of the file, before any code. A pragma naming a
registered renderer's intrinsics module (for example
@octanejs/three/intrinsics) claims the file for Octane the same way. A pragma
pointing at another framework (for example /** @jsxImportSource react */)
does not claim the file: it stays with the host toolchain, exactly like a
file with no pragma at all.
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { octane } from '@octanejs/vite-plugin';
export default defineConfig({
plugins: [octane({ requireDirective: true }), react()],
});// src/islands/Counter.tsrx — compiled by Octane (by extension, no marker)
import { useState } from 'octane';
export function Counter() @{
const [count, setCount] = useState(0);
<button onClick={() => setCount(count + 1)}>{'clicks:' + count}</button>
}// src/App.tsx — no pragma: compiled by the host framework's own pipeline
import { OctaneCompat } from 'octane/react';
import { Counter } from './islands/Counter.tsrx';
export function App() {
return (
<OctaneCompat>
<Counter />
</OctaneCompat>
);
}// src/islands/Badge.tsx — the pragma opts a .tsx into Octane
/** @jsxImportSource octane */
export function Badge(props: { label: string }) @{
<span class="badge">{props.label}</span>
}// src/hooks/useCount.ts — the pragma opts a plain hooks module into Octane
/** @jsxImportSource octane */
import { useState } from 'octane';
export function useCount() {
return useState(0);
}The remaining details:
- Custom octane hooks in a mixed project need compiler-assigned hook slots —
an unslotted
useStatethrows at runtime. Put them in a.tsrxmodule, or add the pragma to the.ts(or.tsx) module that defines them, exactly as above. (Installed octane packages are unaffected: their package manifest keeps making the per-package decision, hook slotting included.) - If a project
.tsx/.ts/.jswithout the pragma imports fromoctane, the build emits a warning naming the file, since that is usually a forgotten pragma. - A different tsrx compiler (for example
@tsrx/react) can own part of the project's.tsrx: list those paths in the integration'sexcludeoption. Excluded paths are never Octane's — no compilation, no warnings. An octane pragma inside an excluded path is a conflict and gets a warning naming it (the exclusion wins). - Installed and linked packages are exempt: their package manifest's Octane declaration remains the per-package decision, so bindings need no pragmas.
The pragma is an ordinary comment, so it is valid — and ships unchanged — even
when requireDirective is off; shared code and libraries can adopt it
unconditionally.
Full app configuration
The config surface is shared by the Vite and Rsbuild integrations. Keep route module IDs project-root-relative and declarative so either bundler can turn them into stable client/server imports:
// octane.config.ts
import { defineConfig, RenderRoute, ServerRoute } from '@octanejs/rsbuild-plugin';
export default defineConfig({
router: {
routes: [
new RenderRoute({ path: '/', entry: '/src/Home.tsrx' }),
new RenderRoute({
path: '/posts/:id',
entry: ['Post', '/src/Post.tsrx'],
layout: '/src/Layout.tsrx',
}),
new ServerRoute({
path: '/api/health',
handler: () => Response.json({ ok: true }),
}),
],
},
});SSR templates require both markers:
<head>
<!--ssr-head-->
</head>
<body>
<div id="root"><!--ssr-body--></div>
</body>The generated client entry hydrates the matched route, layout, pre-hydration
hook, and root boundaries. The generated server entry statically imports route
and module server owners, supports streaming or buffered SSR, and exposes the
request-handler shape selected by the deployment adapter.
Production and preview
Use the normal Rsbuild commands:
pnpm rsbuild dev
pnpm rsbuild build
pnpm octane-rsbuild-previewThe app build writes static browser assets to dist/client and the self-contained
SSR entry and template metadata to dist/server. octane-rsbuild-preview
starts that server entry. Change build.outDir in octane.config.ts to move
both outputs together; configured deployment adapters run after both
environments finish.
For Cloudflare Workers, install @octanejs/adapter-cloudflare and set
adapter: cloudflare() in octane.config.ts. The build emits the module Worker
at dist/server/worker.js. Keep wrangler.jsonc user-owned so bindings and
routes remain explicit:
pnpm add @octanejs/adapter-cloudflare
pnpm add -D wrangler{
"name": "my-octane-app",
"main": "./dist/server/worker.js",
"compatibility_date": "2026-07-14",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": "./dist/client",
"binding": "ASSETS",
},
}Leave static assets asset-first and keep assets.not_found_handling unset or
set to "none": both the "single-page-application" and "404-page" modes can
prevent navigation misses from reaching Octane SSR. Cloudflare bindings and the
execution context are available to middleware and ServerRoute handlers as
context.platform.{env,ctx}. Use wrangler dev for platform-local preview and
wrangler deploy for deployment.
Rsbuild app mode currently expects root-path hosting: keep server.base at /
and output.assetPrefix at auto or /. Deployments beneath a subpath should
rewrite that prefix to the app root at the hosting proxy. Route/config edits
within an already enabled app reload the browser. Changing build.target,
build.outDir, build.minify, or adding the first route reshapes environments
and needs a dev-server restart.
Renderer targets
Compiler and app-core options use declarative module IDs rather than bundler callbacks or instantiated renderer objects. That keeps DOM, Three, and future Lynx renderer selection an Octane compiler/config concern.
@octanejs/rsbuild-plugin already follows the Rsbuild plugin model consumed by
Rspeedy, but this release targets Octane's DOM renderer. A Lynx renderer target
and Rspeedy preset are future work; do not point the current web hydration entry
at a Lynx application.