# Advanced Vite setup

> The three things that bite once loomPreview() is in a real workspace — import specifiers, unbuilt source checkouts, and sharing a Vite config with a React app.

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

[Vite integration](https://docs.astra-void.xyz/loom/guides/vite-integration.md) covers the setup that works. This page covers the
three ways a working setup stops working: importing the plugin the wrong way, pointing it at an
unbuilt checkout, and sharing a Vite config with an app that has its own React.

## Always import the plugin by its bare specifier

`loomPreview()` locates the modules it aliases — the `@rbxts/react-roblox` client shim, the
`@rbxts/services` shim, the globals installer, the React shim, the gallery shell — **relative to its
own file**, as `../src` from the module that is executing. That is what lets it hand Vite absolute
paths that work whether loom is installed in your project, linked from a checkout, or pointed at a
completely different workspace by the CLI.

It also means the import specifier you write is load-bearing. Import
`@loom-dev/preview/vite` and nothing else:

```ts
// correct
import { loomPreview } from "@loom-dev/preview/vite";

// broken — do not do this
import { loomPreview } from "../loom/packages/preview/src/vite.ts";
```

A path import lets Vite's config bundler inline the plugin into a temporary file inside *your*
project, which moves the anchor. The config then loads fine and the failure surfaces much later,
during the build, as something that looks unrelated:

```text
[commonjs--resolver] Failed to resolve entry for package "@loom-dev/layout".
The package may have incorrect main/module/exports specified in its package.json.
```

## Consuming an unbuilt source checkout

If you `link:` a loom checkout and have not run `pnpm build:packages` there, the package's `exports`
point at `dist/` files that do not exist yet, and Vite's default config loader — which externalizes
the package and hands it to Node — cannot load the plugin at all:

```text
Cannot find module '.../packages/preview/dist/vite.js'
```

Building the checkout is the real fix:

```bash title="In the loom checkout"
pnpm build:packages
```

Failing that, `vite --configLoader runner` (and `vite build --configLoader runner`) processes the
config through Vite's module runner instead of pre-bundling it, which can resolve TypeScript sources
directly. It is marked experimental in Vite's CLI help but has been reliable across dev and build.

See [Installation](https://docs.astra-void.xyz/loom/getting-started/installation.md#working-from-a-source-checkout) for what a
checkout needs in the first place — it adds a Rust toolchain to the requirements.

## Don't merge this into an app that also uses React

`loomPreview()` aliases bare `react`, `react/jsx-runtime` and `react/jsx-dev-runtime` to loom's own
React 18 copy, and sets `esbuild.jsx: "automatic"` globally. Those are not scoped to Roblox files —
they apply to every module Vite processes in that config.

So if you drop `loomPreview()` into the Vite config of an existing React web app, that app's React
gets replaced with loom's React 18 too. If the app is on React 19, it breaks. If it also runs
`@vitejs/plugin-react`, you now have two JSX transforms fighting.

Keep the preview in its own Vite project. Three arrangements work well:

- **A dedicated preview app** in your workspace (`apps/preview/`) with its own `vite.config.ts` and
  its own dependencies — a `vite.config.ts` holding nothing but `loomPreview()` is enough. This is
  what loom's `apps/interactive` and `apps/gallery-demo` are.
- **No Vite project at all** — use `loom preview` and `loom build`, pointed at your source directory.
  The CLI runs with `configFile: false`, so it cannot collide with your project's own Vite config
  even if one exists.
- **Mounted inside your app's dev server**, via
  [`loom-dev/embed`](https://docs.astra-void.xyz/loom/reference/cli.md#loom-devembed--the-programmatic-api). The gallery still gets
  its own Vite instance — your app just forwards requests under a base path to it, so the two module
  graphs never meet.

The third is what these docs themselves use: an Astro integration mounts the gallery in dev and emits
the static bundle at build, and each scene is iframed per preview. See
[Static builds and embedding](https://docs.astra-void.xyz/loom/guides/static-builds-and-embedding.md#a-working-embed-pipeline).

> **The same rule applies to the docs you are reading**
>
> `loomPreview()` in this site's `astro.config.mjs` would hijack the docs' own React 19. The gallery
> runs in a separate Vite instance for exactly that reason.

## Where to go when something breaks

Every error string above, plus the ones that come from the runtime rather than the config, is in
[Troubleshooting](https://docs.astra-void.xyz/loom/guides/troubleshooting.md) with the reasoning behind it.
