Star on GitHub

Docs

Setting up a project

With no configuration, npx leglas gives you a single preview of your app root. A config file is how you get more than one thing to compare.

Configuration

Create leglas.config.ts at the project root. .js, .mjs and .json work too. Resolution walks upward from the working directory, so in a monorepo the nearest file wins. Node reads the TypeScript config natively; there is no compiler or extra dependency involved.

export default {
  devServer: "http://localhost:3000",
  previews: [
    { title: "Current", url: "/" },
    { title: "Wave", url: "/?v-hero=wave", note: "Full-bleed, anchored low.", tags: ["Hero"] },
    {
      title: "Dot grid",
      url: "/?v-hero=dotgrid",
      note: "Lattice that wakes near the pointer.",
      tags: ["Hero"],
    },
  ],
};
FieldRequiredPurpose
titleyesLabel in the rail, and the key for your saved layout. Must be unique.
urlunless fileRoot relative (/pricing) or absolute (https://staging.example.com)
notenoSecond line under the title
tagsnoThe first tag renders as a pill
branchnoPreview a git branch instead of the running dev server
filenoAn HTML file served by Leglas itself, instead of url
basedOnnoTitle of the direction this is a variant of; the rail groups the family
askedFornoThe change that was asked for, in the words that were typed
devServernoDefaults to http://localhost:3000
devCommandwith branchHow to start the app. Must contain {port}.
installCommandnoDefaults to npm install
scanPreviewsnoSet false to skip background duplicate scans for expensive apps
recordSetsnoSet false to keep no record of the sets of directions Leglas builds

Leglas proxies whatever answers on devServer. If that port turns out to be served from outside your project, Leglas says so and points at devServer and --user-port rather than quietly proxying the wrong app.

A broken config never stops the server. Leglas starts anyway and the interface reports what to fix, so you are not hunting through a stack trace.

The config is the shared description of a project: commit it and a teammate gets the same directions on clone. leglas add registers a preview on your machine only, in .leglas/previews.json, because exploration is short-lived and its code lives in a gitignored directory; leglas list shows both and marks which are local. Renaming a direction in the rail is local in the same way, so the config title stays the one a teammate sees, and leglas show and leglas keep take either name.

Without a dev server

If the project exists but nothing is listening, set devCommand and Leglas starts your app itself, proxies it and stops it on exit. When --user-port names a server explicitly, Leglas never starts a different one behind that flag.

If there is no app at all, a direction can be a plain HTML file:

export default {
  previews: [
    { title: "Aurora", file: ".leglas/pages/aurora.html" },
    { title: "Ember", file: ".leglas/pages/ember.html" },
  ],
};

Leglas serves each file from its own origin, so the full interface works with no dev server anywhere. The file's directory is mounted rather than the lone file, so stylesheets and images beside it resolve. When the real app arrives, directions graduate to app code and nothing about the interface changes.

Comparing branches

A preview with a branch field is served from its own checkout: Leglas creates a worktree, installs, starts the app with your devCommand on a free port and tears it all down when you quit. In the interface it looks like any other direction, so a branch against your working tree, or three branches against each other, compares the same way two query parameters do.

How it works

Leglas runs one local server that serves the interface at /leglas and forwards every other request to your dev server. Previews load through that proxy, so they are same origin with the interface: no CORS configuration, no cookie special cases.

The proxy is designed to be invisible. Hot module replacement survives the hop, redirects that point at your dev server are rewritten to keep you inside the interface, and responses stream rather than buffer. If an app behaves differently through Leglas than on its own port, that is a bug.

Because a preview is a URL, the same interface compares two routes, two implementations behind a query parameter or a local server against a deployed one. Absolute URLs load directly rather than through the proxy, so a site that refuses to be framed will not preview. The pane says so instead of showing the browser's broken page: which header refused it, a button that opens the page in a tab, and the one header that would let it through if the site is yours. Leglas asks the page without your cookies, so a page that frames only once you are signed in can be uncovered from the pane with "Show the frame anyway".

Leglas also compares what each preview actually draws and warns when two are identical. This catches a typo like ?v-hero=wavee that your app silently ignores while the rail implies a comparison. It reads every preview in the background, one at a time in a hidden frame, and skips branches and absolute URLs, which the browser will not let it read. Set scanPreviews: false for an app too heavy to load that way.

Limitations