Loomguides

Advanced Vite setup

Import specifiers, unbuilt source checkouts, and sharing a Vite config with a React app.

Vite integration covers the setup that works. This page covers the three ways it 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 relative to its own file, as ../src from the executing module. That is what lets it hand Vite absolute paths that work in every setup. loom can be installed in your project, linked from a checkout, or pointed at a different workspace by the CLI.

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

// 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, moving the anchor. The config loads fine and the failure surfaces later, during the build, looking unrelated:

[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

In a link:ed loom checkout that has never run pnpm build:packages, the package’s exports point at dist/ files that do not exist. Vite’s default config loader cannot load the plugin at all:

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

Building the checkout is the real fix:

In the loom checkout
pnpm build:packages

Failing that, vite --configLoader runner processes the config through Vite’s module runner instead of pre-bundling it, resolving TypeScript sources directly. It is marked experimental but has been reliable across dev and build.

See Installation for what a checkout needs — it adds a Rust toolchain.

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. It also sets esbuild.jsx: "automatic" globally, for every module Vite processes in that config, not just Roblox files.

So dropping loomPreview() into an existing React web app’s Vite config replaces that app’s React with loom’s React 18. On React 19 it breaks, and alongside @vitejs/plugin-react you get two JSX transforms fighting.

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

  • A dedicated preview app in your workspace with its own vite.config.ts and dependencies — a config holding nothing but loomPreview() is enough.
  • No Vite project at all — loom preview and loom build run with configFile: false, so they cannot collide with your project’s own config.
  • Mounted inside your app’s dev server, via loom-dev/embed. The gallery still gets its own Vite instance, so the two module graphs never meet.

The third is what these docs use. An Astro integration mounts the gallery in dev and emits the static bundle at build, with each scene iframed per preview.

Where to go when something breaks

Every error string above, plus the runtime ones, is in Troubleshooting.