Loomreference

CLI

`loom preview` and `loom build` — every flag, its parsing rules, and config precedence.

loom — Roblox UI preview
Usage:
loom preview [dir] [--port <n>] [--host] [--targets [glob]]
loom build [dir] --targets [glob] [--out <dir>] [--base <path>] [--no-assets]

Any other first argument (including none) prints this usage text and exits 0.

loom preview

Starts a Vite dev server with loomPreview() pre-applied and configFile: false, so the project’s own vite.config.ts is ignored.

ArgumentDefaultRules
dir.Positional. Resolved against the current working directory. A missing directory exits 1 with loom: directory not found: <path>.
--port <n>5173Must be all digits, else exits 1 with loom: --port requires a numeric value. Left unset when absent so loom.config.ts can supply it.
--host [addr]falseBoolean (binds all interfaces) unless followed by a value not starting with -, which is used as the bind address.
--targets [glob]—Presence switches to gallery mode. A following value not starting with - is used as the glob; otherwise the default **/*.loom.tsx applies.

Mode selection

The CLI picks gallery or single-entry mode and hands the result to loomPreview() as targets. The plugin serves either way, so loomPreview({ targets }) in your own config behaves identically.

  1. --targets present, or loom.config.ts supplies targets → gallery mode. The generated page mounts targets itself. No client entry is needed, and a project’s own index.html is overridden.

  2. Otherwise, a project index.html is used as-is.

  3. Otherwise a page is generated with a full-viewport #loom-root and a module script pointing at the first matching entry:

    src/main.client.tsx
    src/main.client.ts
    src/client/main.client.tsx
    src/client/main.client.ts
    src/main.tsx
    src/main.ts
    src/index.tsx
    src/client.tsx
  4. If none exist → exit 1 listing the candidates. The CLI pre-flights the same lookup the plugin would do, so this is a clean exit rather than a server that fails on first request.

File-system access

server.fs.allow is the union of the project directory, the nearest ancestor containing a pnpm-workspace.yaml, and the directory holding the installed @loom-dev/preview sources. .git alone is not a workspace marker — it would over-widen access to a home-directory repository.

loom build

Runs vite build programmatically over the same gallery and writes a self-contained, client-only SPA. It is loomPreview({ targets }) under vite build with configFile: false, so a project whose own config passes targets emits the same output.

ArgumentDefaultRules
dir.Positional, resolved against the working directory.
--targets [glob]requiredSame parsing as preview. Absent → exit 1: loom: build requires --targets [glob] (the static gallery is target-driven).
--out <dir>dist-previewResolved against the working directory, not dir. Emptied before writing.
--base <path>./Vite’s public base path. The relative default keeps assets host-agnostic.
--no-assetsoffSkips the rbxassetid:// bake, keeping the build off the network — see static builds. Boolean; takes no value.

build does not read loom.config.ts, and exits non-zero with no targets matched … — nothing to build when the glob matches nothing.

On success it prints the resolved output directory:

loom build → /Users/you/code/my-ui/dist-preview

loom.config.ts

Read only by preview, from <dir>/loom.config.ts. It is imported through tsx, and an import failure prints a warning and is otherwise ignored.

loom.config.ts
export default {
targets: "src/scenes", // string | string[]
port: 5204, // number
};

Precedence and validation:

  • CLI flags always win over the file.
  • targets must be a string or a non-empty array of strings.
  • port is honoured only when targets is valid. A config with a missing or malformed targets is skipped in full, port included, with a hint on stderr.

Glob semantics

--targets normalization:

InputPattern used
flag with no value**/*.loom.tsx
. or an empty string**/*.loom.tsx
a value containing *used verbatim
any other value (a directory)<value>/**/*.loom.tsx

Leading ./ and trailing slashes are stripped first. The matcher supports three constructs: **/ for any depth including none, ** for anything, and * for anything but /. Every other character is literal.

Discovery walks the project recursively, skipping node_modules and dot-entries, and returns sorted POSIX-style relative paths. Unreadable directories are skipped rather than fatal.

Live target discovery

Under the dev server, adding or deleting a *.loom.tsx file invalidates the virtual target module and triggers a full page reload, since the sidebar is rebuilt from the import map. The URL hash survives it, and editing a target’s contents is ordinary HMR.

loom-dev/embed — the programmatic API

Both commands own the process: preview binds a port, build owns an output directory. A host application owns both already, so loom-dev/embed exposes the same two pipelines as functions:

Mount the gallery on a host dev server
import { createGalleryServer, buildGallery } from "loom-dev/embed";
const gallery = await createGalleryServer({
root: "/path/to/my-ui", // project the targets live in
targets: "src/scenes", // same semantics as --targets
base: "/loom-preview/", // public path it answers under
});
hostServer.middlewares.use(gallery.middleware);
// …and on shutdown:
await gallery.close();
Emit the static gallery from a host build
await buildGallery({
root: "/path/to/my-ui",
targets: "src/scenes",
outDir: "dist/loom-preview", // --out
// base defaults to "./" — relative assets, hostable anywhere
});
ExportWhat it does
createGalleryServer(options)Gallery Vite server in middleware mode; resolves to { base, middleware, vite, close() }
buildGallery(options)Static gallery build; resolves to the resolved output directory. Takes assets: false to skip the rbxassetid:// bake, as withLoomGallery does
findGalleryTargets(root, targets?)The relative target paths that would be served — for skipping cleanly when a checkout is absent
isGalleryRequest(url, base)Whether a URL belongs to a gallery mounted at base
normalizeGalleryBase(base?)Forces the leading and trailing slash Vite’s base wants
DEFAULT_GALLERY_BASE"/loom-preview/"

createGalleryServer also takes hmrPort. Middleware mode has no HTTP server of its own to upgrade, so Vite always puts HMR on a standalone port. Its default, 24678, is the first port every other Vite dev server grabs. A collision is quiet rather than fatal: Vite logs it, and previews never hot-reload. Left unset, loom picks a free port per server.

middleware answers only under base, redirecting the bare /loom-preview to the trailing-slash form. It calls next() for everything else, so it is safe ahead of the host’s own routes. Everything else matches the CLI: the same target discovery, the same URL contract, the same per-target error containment.