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:
// correctimport { loomPreview } from "@loom-dev/preview/vite";
// broken — do not do thisimport { 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:
pnpm build:packagesFailing 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.tsand dependencies — a config holding nothing butloomPreview()is enough. - No Vite project at all —
loom previewandloom buildrun withconfigFile: 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.
loomPreview() in this site’s astro.config.mjs would hijack the docs’ own React 19, which is why the gallery runs in a separate Vite instance.
Where to go when something breaks
Every error string above, plus the runtime ones, is in Troubleshooting.