brumate

Getting started

A brumate app is a Vite app with the brumate plugin, route files under src/routes, and a small server that renders pages. This page sets one up from scratch.

Install

pnpm add brumate
pnpm add -D vite @brumate/tsc typescript

The project needs Node 22.18 or newer, which runs .ts files directly.

Project layout

my-app/
├── package.json
├── tsconfig.json
├── vite.config.ts
├── server.ts            the development server
└── src/
    ├── client.ts        starts the loader in the browser
    └── routes/
        ├── _layout.brm  the document around every page
        └── index.brm    the page at /

Configure Vite

// vite.config.ts
import brumate from 'brumate/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [brumate()],
  build: {
    manifest: true,
    rolldownOptions: { input: 'src/client.ts' },
  },
});

The client entry starts the loader. The loader then loads the core and the code of the components on the page:

// src/client.ts
import 'virtual:brumate/start';

Add a layout and a page

A layout renders the document around the pages. children is the page for the current path:

<!-- src/routes/_layout.brm -->
<script>
  import { $props } from 'brumate/signals';

  const { children } = $props();
</script>

<html lang="en">
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <main>{children}</main>
  </body>
</html>

index.brm is the page at /:

<!-- src/routes/index.brm -->
<script>
  import { $signal } from 'brumate/signals';

  let count = $signal(0);
</script>

<h1>Hello</h1>
<button onClick=(() => count++)>Clicked {count} times</button>

Routing describes the other route files, such as [id].brm and _error.brm.

Serve pages in development

virtual:brumate/routes exports handle, which answers a request with the page for its path. In development, Vite loads it and serves the client modules:

// server.ts
import { createServer as createHTTPServer } from 'node:http';
import { Readable } from 'node:stream';
import { toNodeHeaders } from 'brumate/server';
import { errorPage } from 'brumate/vite';
import { createServer } from 'vite';

const vite = await createServer({ server: { middlewareMode: true }, appType: 'custom' });

createHTTPServer((request, response) => {
  vite.middlewares(request, response, async () => {
    try {
      const { handle } = await vite.ssrLoadModule('virtual:brumate/routes');
      const answer = await handle(new Request(`http://localhost${request.url}`), {
        scripts:
          '<script type="module" src="/@vite/client"></script><script type="module" async src="/src/client.ts"></script>',
      });
      if (!answer) {
        response.statusCode = 404;
        response.end('Not found');
        return;
      }
      response.writeHead(answer.status, toNodeHeaders(answer.headers));
      Readable.fromWeb(answer.body).pipe(response);
    } catch (error) {
      response.statusCode = 500;
      response.setHeader('content-type', 'text/html');
      response.end(errorPage(error, vite));
    }
  });
}).listen(5173);

Run it:

node server.ts

Open http://localhost:5173. The page shows before any script runs, and the click handler loads with the page.

Deploying describes the production build and server, and static sites.

Edit while it runs

The dev server updates the page when you save a component, and keeps its state when it can:

Rendering again uses the client router. With navigate: false, the page reloads instead.

An edit of a module of reactive logic updates the components that call it in the same way.

Type-check

brumate-tsc type-checks the project, .brm files included. Add the types of brumate to tsconfig.json:

{
  "compilerOptions": {
    "types": ["vite/client", "brumate/types"]
  }
}
pnpm brumate-tsc --noEmit

TypeScript describes what is checked, and Editors sets up VS Code.

Next