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

GoalIntegration
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
ts
// 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:

json
{
	"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
js
// 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:

js
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
ts
// 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:

  • .tsrx is 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 .js is 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.

ts
// 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()],
});
tsrx
// 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>
}
tsx
// 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>
	);
}
tsx
// 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>
}
ts
// 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 useState throws at runtime. Put them in a .tsrx module, 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/.js without the pragma imports from octane, 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's exclude option. 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:

ts
// 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:

html
<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:

bash
pnpm rsbuild dev
pnpm rsbuild build
pnpm octane-rsbuild-preview

The 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:

bash
pnpm add @octanejs/adapter-cloudflare
pnpm add -D wrangler
text
{
	"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.