Quick start

Scaffold an app that already runs, or add Octane to a project you already have. Either way you end with the same thing: a component, a root, and a page.

Create a new app

One command writes the project and installs it:

pnpm create octane my-app

It asks what you are building, then writes the TypeScript settings .tsrx needs, the bundler config, the scripts, and the entry files, and installs the dependencies with the package manager you ran it through. Answer the prompt up front with --template:

bash
npm create octane my-app -- --template spa        # client-only app
npm create octane my-app -- --template fullstack  # routing, SSR, and a server route

npm needs that -- to forward the flag; pnpm and yarn forward it already, so drop it there. Then:

bash
cd my-app
npm run dev

spa is a client-only app. fullstack adds octane.config.ts, a second page, streaming SSR, hydration, a server route, and a production build. Both open on a page that links back into these docs, and both are a working starting point rather than a directory of things to delete.

The scaffolder is octane create, and the CLI guide documents its flags — including --no-install, which prints the dependency list instead of installing it.

Install into an existing project

The rest of this page is the same work by hand, against a Vite project you already have. pnpm dlx @octanejs/cli init does all of it for you; the steps below are what it writes.

Install the runtime and the recommended Vite integration:

pnpm add octane @octanejs/vite-plugin

Then add the plugin to Vite:

ts
// vite.config.ts
import { defineConfig } from 'vite';
import { octane } from '@octanejs/vite-plugin';

export default defineConfig({
	plugins: [octane()],
});

The Vite plugin includes the compiler. Use this same integration for client-only SPAs and for apps that add routing, server rendering, hydration, and production server builds.

For Rspack, install @octanejs/rspack-plugin and add new OctaneRspackPlugin() to plugins. For full apps on Rsbuild, install @octanejs/rsbuild-plugin and add pluginOctane() instead.

The build tools guide has complete Vite, Rspack, and Rsbuild examples, including server compilation, routing, SSR, hydration, HMR, production output, and preview.

Build your first component

A component is a TypeScript function that returns UI. This one receives a title, remembers a count, and updates it when the button is clicked:

tsrx
// src/App.tsrx
import { useState } from 'octane';

interface AppProps {
	title: string;
}

export function App(props: AppProps) @{
	const [count, setCount] = useState(0);

	<main>
		<h1>{props.title as string}</h1>
		<p>{'Button pressed ' + count + ' times'}</p>
		<button onClick={() => setCount((current) => current + 1)}>
			Press me
		</button>
	</main>
}

There are only four ideas here: App is the component, props are its inputs, useState is its memory, and onClick handles the browser event. Calling setCount renders the component again with the new value.

The @{ ... } body is TSRX shorthand for returning its final JSX node. If you prefer standard .tsx or .jsx, use a normal function body with return; the APIs and runtime stay the same.

Connect it to the page

Give Octane an empty element in your HTML:

html
<!-- index.html -->
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>

Create a root for that element and render the component into it:

ts
// src/main.ts
import { createRoot } from 'octane';
import { App } from './App.tsrx';

const container = document.getElementById('app')!;
const root = createRoot(container);

root.render(App, { title: 'My first Octane app' });

That is a complete client-rendered Octane app. Start your Vite development server, open the page, and the button will update without replacing the rest of the document.

TSRX at a glance

TSRX keeps rendered branches and lists next to the elements they control. For example, @for renders a keyed list and @empty handles the no-results case:

tsrx
export function TodoList(props) @{
	<ul>
		@for (const todo of props.todos; key todo.id) {
			<li>{todo.label as string}</li>
		} @empty {
			<li>No tasks yet</li>
		}
	</ul>
}

The same family includes @if for two-way branches, @switch for several choices, and @try for loading and error states. Plain JavaScript still belongs in the setup part of the function. The TSRX vs TSX/JSX guide shows when each syntax is the better fit.

Next