# JSX / TSX on onvibe (no build step)

onvibe serves the files you deploy and runs them in an isolated runtime — there is **no build
phase** (no `npm install`, no bundler). You can still write views in **JSX/TSX**: Deno transpiles
them on the fly. This is the recommended way to build UIs with components — prefer it over Fresh,
SvelteKit, Vite or any framework that needs a local build, unless you specifically need one.

Start from it with `create_project({ template: "jsx" })`.

## The rules (follow all of them)

1. **Put a per-file pragma at the very top of every `.tsx` file:**

   ```tsx
   /** @jsxImportSource npm:preact */
   ```

   This tells Deno which JSX runtime to use. It is **per file** — the runtime does NOT read
   `deno.json`, so a `jsxImportSource` in `deno.json` is ignored. Every `.tsx` file needs its own
   pragma line.

2. **Keep the entry `main.ts` (plain TypeScript, no JSX).** onvibe's entry file is `main.ts` and
   it exports the handler. A `.ts` file cannot contain JSX syntax, so put all JSX in `.tsx` files
   and import from them.

3. **Use only `npm:` / `jsr:` specifiers and relative imports.** e.g. `npm:preact`,
   `npm:preact-render-to-string`, `./views.tsx`. Do NOT rely on bare specifiers mapped through
   `deno.json` `imports` — the runtime does not load that import map, so bare `import "preact"`
   fails. Always write the full `npm:`/`jsr:` specifier.

4. **Render server-side to a string** with `preact-render-to-string`; the handler serves that
   string as HTML.

## Canonical structure

`views.tsx` — all JSX lives here:

```tsx
/** @jsxImportSource npm:preact */
import { render } from "npm:preact-render-to-string";

function Layout({ title, children }: { title: string; children: unknown }) {
  return (
    <html lang="en">
      <head><meta charset="utf-8" /><title>{title}</title></head>
      <body>{children as any}</body>
    </html>
  );
}

export function renderHome(): string {
  return "<!doctype html>" + render(
    <Layout title="My app">
      <h1>Hello</h1>
    </Layout>,
  );
}
```

`main.ts` — entry, no JSX:

```ts
import { withErrorReporting } from "./.onvibe/helpers.ts";
import { renderHome } from "./views.tsx";

async function handler(req: Request): Promise<Response> {
  const url = new URL(req.url);
  if (req.method === "GET" && url.pathname === "/") {
    return new Response(renderHome(), {
      headers: { "content-type": "text/html; charset=utf-8" },
    });
  }
  return new Response("Not found", { status: 404 });
}

export default withErrorReporting(handler);
```

## Notes

- **This is server-side rendering (SSR).** The browser receives plain HTML. For client-side
  interactivity you must ship browser JavaScript separately (e.g. a small `<script>` or a client
  module served from a route); the `.tsx` components above render on the server only.
- Preact `class` and `className` both work; `class` is fine.
- Split views into multiple `.tsx` files freely — just remember the pragma on each one.
- Combine with `create_database` + `npm:pg` exactly as any other onvibe app; nothing about the
  database pattern changes.

## Why not Fresh / SvelteKit / Vite?

Those frameworks require a local build, and their bundlers emit bare `import "pg"`-style
specifiers that depend on an import map the runtime does not load — so they need extra setup and
can break. Native JSX/TSX as above needs no build and no import map, so it "just works". Reach for
a framework only when you truly need its features.
