Lucentive Labs
Loupe Docs · Lucentive Labs
Packages / API

@lucentive-labs/loupe-generator

The Node-only builder that turns a config into a portable static review artifact.

The generator is the portable-artifact path. Given a config, it validates, copies referenced assets, bundles the vanilla renderer, and inlines JS + CSS into index.html. Asset-light configs can be one file; configs with local assets include the copied files beside it. The result runs under file:// or a static host with no runtime package dependency. Output is deterministic.

Install the published generator in the consuming repository, then run it as a build-time tool.

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 the application into HTML

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: open dist/index.html by double-clicking it, drop the output directory in a static bucket, or commit it to a PR. It carries the interactive picker, live preview, export brief, and copy button. Referenced local assets remain required as copied sibling files.

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.