Lucentive Labs
Lucentive Labs Docs
Packages / API

@lucentive-labs/loupe-generator

The Node-only builder that turns a config into one self-contained, portable index.html.

The generator is the portable-artifact path. Given a config, it validates, copies referenced assets, bundles the vanilla renderer, and inlines JS + CSS into one self-contained index.html that runs under file:// or any static host — with zero runtime dependency on these packages. Output is deterministic.

This is the primary way to consume Loupe from any repo without publishing anything.

import { generate } from "@lucentive-labs/loupe-generator";

Node-only. It uses node:fs and bundles with esbuild, so it runs in a build script (tsx generate.ts), not in the browser.

generate(rawConfig, opts): Promise<GenerateResult>

interface GenerateOptions {
  outDir: string;        // where index.html + assets/ are written
  assetsDir?: string;    // resolve relative asset `src` against this (default: cwd)
  initial?: Selections;  // picks embedded in the artifact (default: recommended)
  storageKey?: string;   // localStorage key for persistence inside the artifact
}

interface GenerateResult {
  htmlPath: string;      // absolute path to the written index.html
  assets: string[];      // absolute paths of copied asset files
  html: string;          // the HTML string (also written to disk)
}
generate.ts
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { generate } from "@lucentive-labs/loupe-generator";
import { config } from "./loupe.config.js";

const here = path.dirname(fileURLToPath(import.meta.url));

const result = await generate(config, {
  outDir: path.join(here, "dist"),
  assetsDir: here,
});
console.log(`Generated ${result.htmlPath}`);
console.log(`Copied ${result.assets.length} assets`);
tsx generate.ts            # → dist/index.html + dist/assets/*

What it does, in order

Validate

Runs parseConfig + validateConfig. Any structural or semantic error throws before anything is written — a bad config never produces an artifact.

Copy assets deterministically

Iterates config.assets in sorted key order, copies each referenced file into dist/assets/ with a collision-safe filename, and rewrites the config's asset src values to the copied paths. Already-remote (http(s)) sources are left untouched. A missing asset file throws.

Bundle the renderer

Bundles the browser entry (which mounts loupe-dom) into a single minified IIFE with esbuild, in memory.

Inline into one HTML file

Writes a deterministic HTML template with the theme CSS, the part styles, the bundled JS, and the embedded config + initial selections all inlined. JSON embeds are escaped against </script> breakout.

The result is portable in the strongest sense: open dist/index.html by double-clicking it, drop it in a static bucket, or commit it to a PR. It carries the interactive picker, the live preview, the export brief, and the copy button — and nothing it needs is external.

Determinism

The artifact is reproducible: config order is preserved, asset filenames are assigned in sorted order, and there are no timestamps or absolute paths in the output. The same config and assets always produce the same bytes — so a regenerated artifact diffs cleanly.

Add dist/ to the example's .gitignore; the artifact is always reproducible with tsx generate.ts. The full explore → author → generate → verify → export loop is on the agent method.

Also exported

safeUrl is re-exported for convenience (consumers occasionally want the same URL allowlist the renderer uses), along with the Config and Selections types.