# Vite integration

> Add loomPreview() to your own Vite project — the dependencies, the zero-config default, the options, your own HTML entry, static builds and gallery mode.

Source: https://docs.astra-void.xyz/loom/guides/vite-integration/

`loomPreview()` is the whole product. The `loom` CLI is a thin wrapper around it — a Vite server with
`configFile: false` and this one plugin — so anything the CLI does, your own `vite.config.ts` gets by
calling the plugin yourself, including the parts that used to be CLI-only: the generated page, the
client-entry detection, and gallery mode.

Everything below was verified end to end against Vite 6.4.3 and pnpm 11, on a project sitting outside
loom's own workspace.

## 1. Dependencies

```json title="package.json"
{
  "name": "my-game-ui",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  "devDependencies": {
    "@loom-dev/preview": "^0.11.0",
    "react": "^18.3.1",
    "vite": "^6.0.0"
  }
}
```

React and Vite are peers of `@loom-dev/preview`, so they are yours to install. React 18 is the only
supported major — the adapter's `react-reconciler@0.29` targets it, and the peer range says so. See
[Installation](https://docs.astra-void.xyz/loom/getting-started/installation.md) for the reasoning.

## 2. The config

```ts title="vite.config.ts"
import { loomPreview } from "@loom-dev/preview/vite";
import { defineConfig } from "vite";

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

That is genuinely the whole config, and there is **no `index.html` to write either**. `vite` and
`vite build` now work against a roblox-ts source tree as-is:

- `loomPreview()` returns an array of plugins and contributes its own `esbuild`, `optimizeDeps`,
  `resolve.alias` and `server.fs.allow` through the `config` hook, deep-merged with — and overridable
  by — whatever you write.
- With no `index.html` in the project root, it generates one: a full-viewport `#loom-root` on a
  `#14161a` background plus a module script pointing at your client entry, which it finds by walking
  the roblox-ts conventions (`src/main.client.tsx` first — the [full
  list](https://docs.astra-void.xyz/loom/getting-started/first-preview.md#what-the-plugin-looks-for) is in Your first preview).

See the [plugin reference](https://docs.astra-void.xyz/loom/reference/vite-plugin.md) for the exact contributions.

## 3. Options

Every option is optional; the defaults are what the CLI uses.

```ts title="vite.config.ts"
loomPreview({
  entry: "src/ui/boot.tsx",   // only if your entry isn't a conventional name
  targets: "src/scenes",      // gallery mode: glob, directory, list, or true
  title: "my-game-ui",        // <title> of the generated page
  html: false,                // opt out of the generated page entirely
})
```

| Option | Type | Default | Effect |
| --- | --- | --- | --- |
| `entry` | `string` | auto-detected | The client entry, root-relative (`/src/boot.tsx`) or relative to the project root. Only needed when none of the conventional names fit. |
| `targets` | `string \| string[] \| true` | — | Gallery mode. Same semantics as the CLI's `--targets`: a glob, a directory (expanded to `<dir>/**/*.loom.tsx`), a list, or `true` for the default glob. No client entry is needed. |
| `title` | `string` | `"loom preview"` / `"loom gallery"` | `<title>` of the generated page. |
| `html` | `boolean` | `true` | `false` keeps the plugin out of the HTML business entirely — no generated page, no entry detection, no Rollup input. Only the module plumbing stays. |

> **Your own index.html still wins**
>
> The generated page only fills in when the project root has no `index.html`. If it has one, Vite
> serves it as usual and the plugin stays out of the way — in that case the `#loom-root` element is
> yours to provide (see below). Gallery mode is the one exception: `targets` means you asked for the
> sidebar shell, so its page wins over a project `index.html`.

## 4. Writing your own HTML entry

If you do keep an `index.html`, it needs one thing loom cares about: an element with
`id="loom-root"`. If it is missing, `createRoot()` creates one and appends it to `<body>`, but then
it is unstyled and the preview will not fill the viewport.

```html title="index.html"
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>my-game-ui preview</title>
    <style>
      html, body { margin: 0; height: 100%; background: #14161a; }
      #loom-root { position: relative; width: 100vw; height: 100vh; overflow: hidden; }
    </style>
  </head>
  <body>
    <div id="loom-root"></div>
    <script type="module" src="/src/main.client.tsx"></script>
  </body>
</html>
```

That is exactly what the plugin generates, so the reason to write it yourself is wanting something
else in the document — a font link, a wrapper element, your own background.

The entry itself is an ordinary roblox-ts client entry — see [Your first
preview](https://docs.astra-void.xyz/loom/getting-started/first-preview.md). At this point `vite` serves an interactive preview:
state updates, `Activated` handlers and text input all work.

## 5. `vite build` produces a static preview

```bash
pnpm vite build     # → dist/
```

The build emits a self-contained client-only SPA: your entry, the adapter, the renderer and the WASM
layout binary, hostable anywhere. The Roblox datatype globals are part of the bundle — the plugin
prepends `import ".../globals.ts"` to the page's entry modules, so `installGlobals()` runs before any
module that touches `UDim2` evaluates. That applies to the generated page *and* to your own
`index.html`, whose module scripts it reads out of the file.

> **Older versions needed a manual globals import**
>
> Before 0.3.0 the globals were dev-server-only: a stock `vite build` compiled cleanly, shipped, and
> then threw `ReferenceError: UDim2 is not defined` on first render unless you wrote
> `import "@loom-dev/preview/globals";` as the first import of your entry. That import is now
> unnecessary — and still harmless if you keep it, since the installer is idempotent.

## 6. Gallery mode from the plugin

Pass `targets` and the same `*.loom.tsx` gallery the CLI serves comes up under `vite`, and
`vite build` emits it as a static, deep-linkable site:

```ts title="vite.config.ts"
export default defineConfig({
  plugins: [loomPreview({ targets: "src/scenes" })],
});
```

Each target becomes its own async chunk, and the built page honors the same
[`?target=` / `?chrome=` / `?theme=` / `?background=` / `?base=` URL contract](https://docs.astra-void.xyz/loom/guides/static-builds-and-embedding.md#the-url-contract)
`loom build` produces — they are the same code path. The target contract itself is covered in
[Gallery targets](https://docs.astra-void.xyz/loom/guides/gallery-targets.md).

## Next steps

Three things reliably break a working setup once it moves into a real workspace: importing the
plugin by file path, pointing it at an unbuilt source checkout, and sharing a Vite config with an
app that has its own React. All three are in [Advanced Vite
setup](https://docs.astra-void.xyz/loom/guides/advanced-vite-setup.md).

For error strings and what each one actually means, see
[Troubleshooting](https://docs.astra-void.xyz/loom/guides/troubleshooting.md).
