@bettercms-ai/preview-runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # @bettercms-ai/preview-runtime
2
+
3
+ Zero-config component previews for Next.js and Astro apps on BetterCMS.
4
+
5
+ You do not install or configure this package. BetterCMS runs it for you:
6
+
7
+ - **In your repository's CI.** When someone opens **Output** on a component in the BetterCMS dashboard,
8
+ BetterCMS adds `.github/workflows/bcms-component-validation.yml` to your repository (once) and starts
9
+ it. The workflow runs `npx @bettercms-ai/preview-runtime validate`, which builds a preview of your app
10
+ as it is, renders the component in a real browser at each viewport, checks it, and reports the result.
11
+ - **On BetterCMS hosting.** The preview built in CI is what the dashboard shows, rendered by your own
12
+ code with live content.
13
+
14
+ Nothing is written to your repository except that one workflow file. Routes are generated into the CI
15
+ checkout for the build and removed afterwards.
16
+
17
+ ## What it needs to know
18
+
19
+ Which file implements a component. The agent that writes a component records it through the BetterCMS
20
+ MCP tool `set_component_source` (for example `src/components/sections/Hero.astro`, or
21
+ `components/Hero.tsx` with an export name).
22
+
23
+ ## Commands
24
+
25
+ ```bash
26
+ # Build a preview runtime from an app (used by `validate`; useful when debugging locally)
27
+ bcms-preview build --manifest manifest.json --out .bcms-preview-release [--cwd path/to/app]
28
+
29
+ # Validate one component (CI only; configured by BCMS_* environment variables the workflow sets)
30
+ bcms-preview validate
31
+ ```
32
+
33
+ `manifest.json`:
34
+
35
+ ```json
36
+ { "components": [{ "id": "cmp_1", "source": { "path": "src/components/Hero.astro" } }] }
37
+ ```
38
+
39
+ ## How a preview renders
40
+
41
+ - **Astro**: built with `output: "server"` and `@astrojs/node`, so `.astro` components render on the
42
+ server with the props the dashboard sends.
43
+ - **Next.js**: built with `output: "standalone"`, so client components and async server components
44
+ render the way they do in your app.
45
+
46
+ The preview is served under `/__bettercms/component-preview/` and only accepts a short-lived, signed
47
+ session from the BetterCMS dashboard.
package/dist/cli.js ADDED
@@ -0,0 +1,676 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/build.ts
4
+ import { spawnSync } from "child_process";
5
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
6
+ import { createRequire } from "module";
7
+ import { dirname, extname, join, relative, resolve, sep } from "path";
8
+ import { fileURLToPath } from "url";
9
+ var here = dirname(fileURLToPath(import.meta.url));
10
+ var PREVIEW_BASE = "/__bettercms/component-preview";
11
+ var COMPONENT_EXTENSIONS = /* @__PURE__ */ new Set([".astro", ".tsx", ".jsx", ".ts", ".js", ".mjs"]);
12
+ var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
13
+ var CliFailure = class extends Error {
14
+ };
15
+ function fail(message) {
16
+ throw new CliFailure(message);
17
+ }
18
+ function snapshot(root, names) {
19
+ const saved = names.map((name) => {
20
+ const file = join(root, name);
21
+ return { file, content: existsSync(file) ? readFileSync(file) : null };
22
+ });
23
+ return () => {
24
+ for (const { file, content } of saved) {
25
+ if (content === null) rmSync(file, { force: true });
26
+ else writeFileSync(file, content);
27
+ }
28
+ };
29
+ }
30
+ var MUTATED_BY_BUILD = ["tsconfig.json", "next-env.d.ts", "package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"];
31
+ function readJson(file) {
32
+ try {
33
+ return JSON.parse(readFileSync(file, "utf8"));
34
+ } catch (error) {
35
+ fail(`could not read ${file}: ${error.message}`);
36
+ }
37
+ }
38
+ function validateManifest(root, manifest) {
39
+ if (!manifest || !Array.isArray(manifest.components) || manifest.components.length === 0) {
40
+ fail("the manifest lists no components");
41
+ }
42
+ const seen = /* @__PURE__ */ new Set();
43
+ return manifest.components.map((entry) => {
44
+ if (!entry || typeof entry.id !== "string" || !entry.id.trim()) fail("a manifest entry has no id");
45
+ if (seen.has(entry.id)) fail(`component ${entry.id} is listed twice`);
46
+ seen.add(entry.id);
47
+ const path = entry.source?.path;
48
+ if (typeof path !== "string" || path.startsWith("/") || path.includes("\\") || path.split("/").includes("..")) {
49
+ fail(`component ${entry.id} has an unsafe source path: ${String(path)}`);
50
+ }
51
+ if (!COMPONENT_EXTENSIONS.has(extname(path))) fail(`component ${entry.id}: unsupported file type ${extname(path)}`);
52
+ if (!existsSync(join(root, path))) fail(`component ${entry.id}: ${path} does not exist`);
53
+ const named = entry.source.export;
54
+ if (named !== void 0 && named !== "default" && !IDENTIFIER.test(named)) {
55
+ fail(`component ${entry.id}: export "${named}" is not a valid identifier`);
56
+ }
57
+ return { id: entry.id, source: { path, export: named ?? "default" } };
58
+ });
59
+ }
60
+ function detectFramework(root) {
61
+ const pkg = readJson(join(root, "package.json"));
62
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
63
+ if (deps.astro) return "astro";
64
+ if (deps.next) return "next";
65
+ fail("this app depends on neither astro nor next");
66
+ }
67
+ function run(root, command, args) {
68
+ const result = spawnSync(command, args, { cwd: root, stdio: "inherit", env: process.env });
69
+ if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} exited with ${result.status}`);
70
+ }
71
+ function bin(root, name) {
72
+ const local = join(root, "node_modules", ".bin", name);
73
+ if (!existsSync(local)) fail(`${name} is not installed in ${root}. Install the app's dependencies first.`);
74
+ return local;
75
+ }
76
+ function registrySource(fromDir, root, entries) {
77
+ const imports = [];
78
+ const keys = [];
79
+ entries.forEach((entry, index) => {
80
+ let specifier = relative(fromDir, join(root, entry.source.path)).split(sep).join("/");
81
+ if (!specifier.startsWith(".")) specifier = `./${specifier}`;
82
+ if ([".tsx", ".ts", ".jsx", ".js"].includes(extname(specifier))) specifier = specifier.slice(0, -extname(specifier).length);
83
+ const local = `Component${index}`;
84
+ imports.push(entry.source.export === "default" ? `import ${local} from ${JSON.stringify(specifier)};` : `import { ${entry.source.export} as ${local} } from ${JSON.stringify(specifier)};`);
85
+ keys.push(` ${JSON.stringify(entry.id)}: ${local},`);
86
+ });
87
+ return `${imports.join("\n")}
88
+
89
+ export const registry: Record<string, any> = {
90
+ ${keys.join("\n")}
91
+ };
92
+
93
+ export const has = (componentId: string): boolean => Object.prototype.hasOwnProperty.call(registry, componentId);
94
+ `;
95
+ }
96
+ function writeRuntimeLibrary(dir) {
97
+ writeFileSync(join(dir, "server.mjs"), readFileSync(join(here, "server.js"), "utf8"));
98
+ const types = join(here, "server.d.ts");
99
+ if (existsSync(types)) writeFileSync(join(dir, "server.d.mts"), readFileSync(types, "utf8"));
100
+ writeFileSync(join(dir, "shell.ts"), `export default ${JSON.stringify(readFileSync(join(here, "shell.global.js"), "utf8"))};
101
+ `);
102
+ }
103
+ function astroGlobalStyles(root) {
104
+ const found = /* @__PURE__ */ new Set();
105
+ const scan = (dir) => {
106
+ if (!existsSync(dir)) return;
107
+ for (const name of readdirSync(dir)) {
108
+ const full = join(dir, name);
109
+ if (statSync(full).isDirectory()) scan(full);
110
+ else if (name.endsWith(".astro")) {
111
+ for (const match of readFileSync(full, "utf8").matchAll(/^\s*import\s+["']([^"']+\.css)["'];?/gm)) {
112
+ const target = resolve(dirname(full), match[1]);
113
+ if (target.startsWith(root) && existsSync(target)) found.add(target);
114
+ }
115
+ }
116
+ }
117
+ };
118
+ scan(join(root, "src", "layouts"));
119
+ return [...found];
120
+ }
121
+ var ASTRO_NODE_ADAPTER = { "5": "^9", "6": "^10", "7": "^11" };
122
+ function ensureAstroNodeAdapter(root) {
123
+ const require2 = createRequire(join(root, "package.json"));
124
+ try {
125
+ require2.resolve("@astrojs/node");
126
+ return;
127
+ } catch {
128
+ }
129
+ const astroVersion = readJson(require2.resolve("astro/package.json")).version;
130
+ const range = ASTRO_NODE_ADAPTER[astroVersion.split(".")[0]];
131
+ if (!range) fail(`Astro ${astroVersion} is not supported for component previews yet`);
132
+ run(root, "npm", ["install", "--no-save", "--no-audit", "--no-fund", `@astrojs/node@${range}`]);
133
+ }
134
+ function buildAstro(root, entries, out) {
135
+ const configName = ["astro.config.mjs", "astro.config.js", "astro.config.ts", "astro.config.mts"].find((f) => existsSync(join(root, f)));
136
+ if (!configName) fail("no astro.config file found");
137
+ ensureAstroNodeAdapter(root);
138
+ const gen = join(root, ".bcms-preview");
139
+ const wrapper = join(root, "astro.config.bcms-preview.mjs");
140
+ rmSync(gen, { recursive: true, force: true });
141
+ mkdirSync(gen, { recursive: true });
142
+ try {
143
+ writeRuntimeLibrary(gen);
144
+ writeFileSync(join(gen, "registry.ts"), registrySource(gen, root, entries));
145
+ const handler = (method, body) => `export const prerender = false;
146
+ export const ${method} = ${body};
147
+ `;
148
+ writeFileSync(join(gen, "runtime.ts"), `import { handleRuntime } from "./server.mjs";
149
+ import shell from "./shell";
150
+ ${handler("GET", "() => handleRuntime(shell)")}`);
151
+ writeFileSync(join(gen, "session.ts"), `import { handleSession } from "./server.mjs";
152
+ import { has } from "./registry";
153
+ ${handler("POST", "({ request }: { request: Request }) => handleSession(request, has)")}`);
154
+ writeFileSync(join(gen, "props.ts"), `import { handleProps } from "./server.mjs";
155
+ import { has } from "./registry";
156
+ ${handler("POST", "({ request }: { request: Request }) => handleProps(request, has)")}`);
157
+ writeFileSync(join(gen, "health.ts"), `import { handleHealth } from "./server.mjs";
158
+ ${handler("GET", "() => handleHealth()")}`);
159
+ const styles = astroGlobalStyles(root).map((file) => `import ${JSON.stringify(relative(gen, file).split(sep).join("/"))};`).join("\n");
160
+ writeFileSync(join(gen, "render.astro"), `---
161
+ ${styles}
162
+ import { renderEntry, renderHeaders } from "./server.mjs";
163
+ import { registry } from "./registry";
164
+ export const prerender = false;
165
+ const headers = renderHeaders();
166
+ const entry = renderEntry(Astro.url.searchParams.get("id"));
167
+ const Component = entry ? registry[entry.componentId] : undefined;
168
+ for (const [name, value] of Object.entries(headers)) Astro.response.headers.set(name, value);
169
+ if (!entry || !Component) return new Response("Not found", { status: 404, headers });
170
+ ---
171
+ <html lang="en" data-bcms-preview-render="1">
172
+ <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /></head>
173
+ <body><Component {...entry.props} /></body>
174
+ </html>
175
+ `);
176
+ writeFileSync(wrapper, `import user from "./${configName}";
177
+ import node from "@astrojs/node";
178
+
179
+ const routes = ["runtime.ts", "session.ts", "props.ts", "render.astro", "health.ts"];
180
+
181
+ export default {
182
+ ...user,
183
+ output: "server",
184
+ base: ${JSON.stringify(PREVIEW_BASE)},
185
+ adapter: node({ mode: "standalone" }),
186
+ integrations: [
187
+ ...(user.integrations ?? []),
188
+ {
189
+ name: "bettercms-component-preview",
190
+ hooks: {
191
+ "astro:config:setup": ({ injectRoute }) => {
192
+ for (const file of routes) {
193
+ injectRoute({
194
+ pattern: "/__bcms/" + file.replace(/\\.(ts|astro)$/, ""),
195
+ entrypoint: new URL("./.bcms-preview/" + file, import.meta.url),
196
+ prerender: false,
197
+ });
198
+ }
199
+ },
200
+ },
201
+ },
202
+ ],
203
+ };
204
+ `);
205
+ rmSync(join(root, "dist"), { recursive: true, force: true });
206
+ run(root, bin(root, "astro"), ["build", "--config", "astro.config.bcms-preview.mjs"]);
207
+ } finally {
208
+ rmSync(gen, { recursive: true, force: true });
209
+ rmSync(wrapper, { force: true });
210
+ }
211
+ const app = join(out, "app");
212
+ mkdirSync(app, { recursive: true });
213
+ cpSync(join(root, "dist"), app, { recursive: true });
214
+ cpSync(join(root, "package.json"), join(app, "package.json"));
215
+ cpSync(join(root, "node_modules"), join(app, "node_modules"), { recursive: true, verbatimSymlinks: true });
216
+ writeFileSync(join(out, "bcms-runtime.json"), `${JSON.stringify({ kind: "node", dir: "app", entry: "server/entry.mjs" })}
217
+ `);
218
+ }
219
+ function buildNext(root, entries, out) {
220
+ const appDir = ["app", join("src", "app")].map((d) => join(root, d)).find((d) => existsSync(d));
221
+ if (!appDir) fail("no App Router directory (app/ or src/app/) found");
222
+ const configName = ["next.config.mjs", "next.config.js", "next.config.ts", "next.config.cjs"].find((f) => existsSync(join(root, f)));
223
+ const gen = join(appDir, "%5F%5Fbcms");
224
+ const userConfig = configName ? join(root, configName.replace("next.config", "next.config.bcms-user")) : null;
225
+ const wrapperName = configName && configName.endsWith(".ts") ? "next.config.ts" : "next.config.mjs";
226
+ rmSync(gen, { recursive: true, force: true });
227
+ mkdirSync(gen, { recursive: true });
228
+ if (configName && userConfig) renameSync(join(root, configName), userConfig);
229
+ try {
230
+ writeRuntimeLibrary(gen);
231
+ writeFileSync(join(gen, "registry.ts"), registrySource(gen, root, entries));
232
+ const route = (name, source) => {
233
+ mkdirSync(join(gen, name), { recursive: true });
234
+ writeFileSync(join(gen, name, "route.ts"), `export const dynamic = "force-dynamic";
235
+ ${source}`);
236
+ };
237
+ route("runtime", `import { handleRuntime } from "../server.mjs";
238
+ import shell from "../shell";
239
+ export function GET() {
240
+ return handleRuntime(shell);
241
+ }
242
+ `);
243
+ route("session", `import { handleSession } from "../server.mjs";
244
+ import { has } from "../registry";
245
+ export function POST(request: Request) {
246
+ return handleSession(request, has);
247
+ }
248
+ `);
249
+ route("props", `import { handleProps } from "../server.mjs";
250
+ import { has } from "../registry";
251
+ export function POST(request: Request) {
252
+ return handleProps(request, has);
253
+ }
254
+ `);
255
+ route("health", `import { handleHealth } from "../server.mjs";
256
+ export function GET() {
257
+ return handleHealth();
258
+ }
259
+ `);
260
+ mkdirSync(join(gen, "render"), { recursive: true });
261
+ writeFileSync(join(gen, "render", "page.tsx"), `import { notFound } from "next/navigation";
262
+ import { renderEntry } from "../server.mjs";
263
+ import { registry } from "../registry";
264
+
265
+ export const dynamic = "force-dynamic";
266
+
267
+ export default async function BetterCMSComponentPreview({ searchParams }: { searchParams: Promise<{ id?: string }> }) {
268
+ const { id } = await searchParams;
269
+ const entry = renderEntry(id);
270
+ const Component = entry ? registry[entry.componentId] : undefined;
271
+ if (!entry || !Component) notFound();
272
+ // The marker the runtime page checks before acknowledging: a 404 or an error page never carries it.
273
+ return (
274
+ <>
275
+ <Component {...entry.props} />
276
+ <template data-bcms-preview-render="1" />
277
+ </>
278
+ );
279
+ }
280
+ `);
281
+ const importUser = userConfig ? `import user from "./${relative(root, userConfig)}";` : "const user = {};";
282
+ writeFileSync(join(root, wrapperName), `${importUser}
283
+
284
+ // No frame-ancestors here: next.config headers are fixed at build time, and the dashboard origin the
285
+ // render frame must allow is runtime configuration. The platform's proxy sets it for this whole prefix.
286
+ const RENDER_HEADERS = [
287
+ { key: "referrer-policy", value: "no-referrer" },
288
+ { key: "cache-control", value: "no-store" },
289
+ ];
290
+
291
+ export default async function betterCMSComponentPreviewConfig(phase, context) {
292
+ const resolved = typeof user === "function" ? await user(phase, context) : user;
293
+ const userHeaders = resolved.headers;
294
+ return {
295
+ ...resolved,
296
+ output: "standalone",
297
+ basePath: ${JSON.stringify(PREVIEW_BASE)},
298
+ async headers() {
299
+ const own = typeof userHeaders === "function" ? await userHeaders() : [];
300
+ return [...own, { source: "/__bcms/render", headers: RENDER_HEADERS }];
301
+ },
302
+ };
303
+ }
304
+ `);
305
+ rmSync(join(root, ".next"), { recursive: true, force: true });
306
+ run(root, bin(root, "next"), ["build"]);
307
+ } finally {
308
+ rmSync(gen, { recursive: true, force: true });
309
+ rmSync(join(root, wrapperName), { force: true });
310
+ if (configName && userConfig && existsSync(userConfig)) renameSync(userConfig, join(root, configName));
311
+ }
312
+ const standalone = join(root, ".next", "standalone");
313
+ if (!existsSync(join(standalone, "server.js"))) fail("next build produced no standalone server");
314
+ const app = join(out, "app");
315
+ mkdirSync(app, { recursive: true });
316
+ cpSync(standalone, app, { recursive: true, verbatimSymlinks: true });
317
+ if (existsSync(join(root, ".next", "static"))) cpSync(join(root, ".next", "static"), join(app, ".next", "static"), { recursive: true });
318
+ if (existsSync(join(root, "public"))) cpSync(join(root, "public"), join(app, "public"), { recursive: true });
319
+ writeFileSync(join(out, "bcms-runtime.json"), `${JSON.stringify({ kind: "node", dir: "app", entry: "server.js" })}
320
+ `);
321
+ }
322
+ function buildPreviewRuntime(input) {
323
+ const root = resolve(input.root);
324
+ const out = resolve(input.out);
325
+ const entries = validateManifest(root, readJson(resolve(input.manifestPath)));
326
+ const framework = detectFramework(root);
327
+ rmSync(out, { recursive: true, force: true });
328
+ mkdirSync(out, { recursive: true });
329
+ const restore = snapshot(root, MUTATED_BY_BUILD);
330
+ try {
331
+ if (framework === "astro") buildAstro(root, entries, out);
332
+ else buildNext(root, entries, out);
333
+ } finally {
334
+ restore();
335
+ }
336
+ return { framework, out, components: entries.map((e) => e.id) };
337
+ }
338
+
339
+ // src/validate.ts
340
+ import { spawn, spawnSync as spawnSync2 } from "child_process";
341
+ import { createHash, randomBytes } from "crypto";
342
+ import { mkdtempSync, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
343
+ import { createRequire as createRequire2 } from "module";
344
+ import { createServer } from "net";
345
+ import { tmpdir } from "os";
346
+ import { dirname as dirname2, join as join2 } from "path";
347
+ var PREVIEW_ROUTE_BASE = "/__bettercms/component-preview/__bcms";
348
+ var RUNTIME_PATH = `${PREVIEW_ROUTE_BASE}/runtime`;
349
+ var ValidationFailure = class extends Error {
350
+ constructor(code, message) {
351
+ super(message);
352
+ this.code = code;
353
+ this.name = "ValidationFailure";
354
+ }
355
+ code;
356
+ };
357
+ function required(name) {
358
+ const value = process.env[name]?.trim();
359
+ if (!value) throw new CliFailure(`${name} is not set. This command runs inside the BetterCMS component validation workflow.`);
360
+ return value;
361
+ }
362
+ var sha256 = (data) => createHash("sha256").update(data).digest("hex");
363
+ function canonical(value) {
364
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
365
+ if (value && typeof value === "object") {
366
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
367
+ }
368
+ return JSON.stringify(value);
369
+ }
370
+ function freePort() {
371
+ return new Promise((resolve2, reject) => {
372
+ const server = createServer();
373
+ server.once("error", reject);
374
+ server.listen(0, "127.0.0.1", () => {
375
+ const address = server.address();
376
+ const port = typeof address === "object" && address ? address.port : 0;
377
+ server.close(() => resolve2(port));
378
+ });
379
+ });
380
+ }
381
+ async function waitForOk(url, timeoutMs) {
382
+ const deadline = Date.now() + timeoutMs;
383
+ while (Date.now() < deadline) {
384
+ try {
385
+ if ((await fetch(url)).ok) return true;
386
+ } catch {
387
+ }
388
+ await new Promise((r) => setTimeout(r, 500));
389
+ }
390
+ return false;
391
+ }
392
+ function ensureBrowser() {
393
+ const require2 = createRequire2(import.meta.url);
394
+ const cli = join2(dirname2(require2.resolve("playwright/package.json")), "cli.js");
395
+ const args = [cli, "install", "chromium"];
396
+ if (process.platform === "linux" && process.env.CI) args.push("--with-deps");
397
+ const result = spawnSync2(process.execPath, args, { stdio: "inherit" });
398
+ if (result.status !== 0) {
399
+ throw new ValidationFailure("COMPONENT_VALIDATION_BROWSER_UNAVAILABLE", "A headless browser could not be installed on this runner.");
400
+ }
401
+ }
402
+ async function renderInBrowser(url, viewports, tokenNames) {
403
+ ensureBrowser();
404
+ const { chromium } = await import("playwright");
405
+ const browser = await chromium.launch();
406
+ try {
407
+ let consoleErrors = 0;
408
+ let runtimeErrors = 0;
409
+ const missingTokens = /* @__PURE__ */ new Set();
410
+ const results = [];
411
+ const problems = [];
412
+ for (const viewport of viewports) {
413
+ const page = await browser.newPage({ viewport: { width: viewport.width, height: viewport.height } });
414
+ page.on("console", (message) => {
415
+ if (message.type() === "error") {
416
+ consoleErrors += 1;
417
+ problems.push(`${viewport.name} console: ${message.text().slice(0, 200)}`);
418
+ }
419
+ });
420
+ page.on("pageerror", (error) => {
421
+ runtimeErrors += 1;
422
+ problems.push(`${viewport.name} error: ${error.message.slice(0, 200)}`);
423
+ });
424
+ const response = await page.goto(url, { waitUntil: "load", timeout: 45e3 });
425
+ await page.waitForLoadState("networkidle", { timeout: 15e3 }).catch(() => {
426
+ });
427
+ const rendered = await page.locator("[data-bcms-preview-render]").count();
428
+ if (!response?.ok() || rendered === 0) {
429
+ runtimeErrors += 1;
430
+ problems.push(`${viewport.name}: the component page did not render (HTTP ${response?.status() ?? "none"})`);
431
+ }
432
+ const missing = await page.evaluate((names) => {
433
+ const referenced = /* @__PURE__ */ new Set();
434
+ const scan = (text) => {
435
+ for (const match of text.matchAll(/var\(\s*(--[A-Za-z0-9_-]+)/g)) referenced.add(match[1]);
436
+ };
437
+ for (const sheet of Array.from(document.styleSheets)) {
438
+ try {
439
+ for (const rule of Array.from(sheet.cssRules)) scan(rule.cssText);
440
+ } catch {
441
+ }
442
+ }
443
+ document.querySelectorAll("[style]").forEach((element) => scan(element.getAttribute("style") ?? ""));
444
+ const style = getComputedStyle(document.documentElement);
445
+ return names.filter((name) => referenced.has(name) && !style.getPropertyValue(name).trim());
446
+ }, tokenNames);
447
+ for (const token of missing) missingTokens.add(token);
448
+ const png = await page.screenshot({ fullPage: true });
449
+ results.push({
450
+ name: viewport.name,
451
+ width: viewport.width,
452
+ height: viewport.height,
453
+ // No baseline exists for a first validation. The database requires this exact shape for it:
454
+ // no baseline or diff digest, and the check marked as needing review.
455
+ status: "baseline-missing",
456
+ candidateDigest: `sha256:${sha256(png)}`
457
+ });
458
+ await page.close();
459
+ }
460
+ return { results, consoleErrors, runtimeErrors, missingTokens: [...missingTokens].sort(), problems };
461
+ } finally {
462
+ await browser.close();
463
+ }
464
+ }
465
+ async function validateComponent() {
466
+ const apiUrl = required("BCMS_API_URL");
467
+ const apiKey = required("BCMS_API_KEY");
468
+ const projectId = required("BCMS_PROJECT_ID");
469
+ const requestId = required("BCMS_REQUEST_ID");
470
+ const componentId = required("BCMS_COMPONENT_ID");
471
+ const commitSha = required("BCMS_COMMIT_SHA");
472
+ const familyKey = required("BCMS_FAMILY_KEY");
473
+ const previewOrigin = required("BCMS_PREVIEW_ORIGIN");
474
+ const nativeViewports = JSON.parse(required("BCMS_NATIVE_VIEWPORTS"));
475
+ const api = async (path, init = {}) => {
476
+ const response = await fetch(new URL(path, apiUrl), {
477
+ method: init.method ?? "GET",
478
+ headers: {
479
+ authorization: `Bearer ${apiKey}`,
480
+ "content-type": "application/json",
481
+ ...init.claim ? { "x-bcms-component-claim": init.claim } : {}
482
+ },
483
+ ...init.body === void 0 ? {} : { body: JSON.stringify(init.body) }
484
+ });
485
+ const text = await response.text();
486
+ let parsed = null;
487
+ try {
488
+ parsed = JSON.parse(text);
489
+ } catch {
490
+ parsed = null;
491
+ }
492
+ if (!response.ok) {
493
+ const code = typeof parsed?.error === "string" && /^[A-Z][A-Z0-9_]*$/.test(parsed.error) ? parsed.error : `API_HTTP_${response.status}`;
494
+ const detail = typeof parsed?.message === "string" ? parsed.message : typeof parsed?.error === "string" ? parsed.error : text.slice(0, 300);
495
+ throw new ValidationFailure(code, `${init.method ?? "GET"} ${path} answered ${response.status}: ${detail}`);
496
+ }
497
+ return (parsed && "data" in parsed ? parsed.data : parsed) ?? {};
498
+ };
499
+ const claimPath = `/api/v1/projects/${projectId}/component-implementation/implementation-requests/${requestId}`;
500
+ let claim = null;
501
+ const claimRequest = async () => {
502
+ claim = await api(`${claimPath}/claim`, {
503
+ method: "POST",
504
+ body: {
505
+ componentId,
506
+ commitSha,
507
+ adapter: { protocol: "bcms-component-runtime-v1", kind: "project-route", path: RUNTIME_PATH, familyKey, previewOrigin, nativeViewports },
508
+ providerRunId: required("BCMS_RUN_ID"),
509
+ providerRunAttempt: Number(process.env.BCMS_RUN_ATTEMPT) || 1,
510
+ providerRunUrl: required("BCMS_RUN_URL"),
511
+ workflowRef: required("BCMS_WORKFLOW_REF"),
512
+ // The server caps this at the request's own expiry and at ten minutes.
513
+ credentialExpiresAt: new Date(Date.now() + 10 * 6e4 - 5e3).toISOString()
514
+ }
515
+ });
516
+ return claim;
517
+ };
518
+ const work = mkdtempSync(join2(tmpdir(), "bcms-validate-"));
519
+ let runtime = null;
520
+ try {
521
+ const manifest = await api(`/api/v1/projects/${projectId}/component-preview/manifest`);
522
+ const entry = manifest.components.find((c) => c.id === componentId);
523
+ if (!entry) {
524
+ throw new ValidationFailure(
525
+ "COMPONENT_SOURCE_NOT_RECORDED",
526
+ "No file is recorded as this component's source. The agent that writes a component records it with set_component_source."
527
+ );
528
+ }
529
+ const manifestPath = join2(work, "manifest.json");
530
+ writeFileSync2(manifestPath, JSON.stringify({ components: manifest.components.map(({ id: id2, source }) => ({ id: id2, source })) }));
531
+ const out = join2(work, "runtime");
532
+ try {
533
+ buildPreviewRuntime({ root: process.cwd(), manifestPath, out });
534
+ } catch (error) {
535
+ throw new ValidationFailure("COMPONENT_PREVIEW_BUILD_FAILED", `The preview build failed: ${error.message}`);
536
+ }
537
+ const runtimeManifest = JSON.parse(readFileSync2(join2(out, "bcms-runtime.json"), "utf8"));
538
+ const port = await freePort();
539
+ const validatorKey = randomBytes(32).toString("base64url");
540
+ const local = `http://127.0.0.1:${port}`;
541
+ const { BCMS_API_KEY: _withheld, ...inherited } = process.env;
542
+ runtime = spawn(process.execPath, [join2(out, runtimeManifest.dir, runtimeManifest.entry)], {
543
+ cwd: join2(out, runtimeManifest.dir),
544
+ env: {
545
+ ...inherited,
546
+ NODE_ENV: "production",
547
+ PORT: String(port),
548
+ HOST: "127.0.0.1",
549
+ HOSTNAME: "127.0.0.1",
550
+ BCMS_API_URL: apiUrl,
551
+ BCMS_DASHBOARD_ORIGIN: local,
552
+ BCMS_PREVIEW_ORIGIN: local,
553
+ BCMS_PREVIEW_VALIDATOR_KEY: validatorKey
554
+ },
555
+ stdio: ["ignore", "inherit", "inherit"]
556
+ });
557
+ if (!await waitForOk(`${local}${PREVIEW_ROUTE_BASE}/health`, 6e4)) {
558
+ throw new ValidationFailure("COMPONENT_PREVIEW_RUNTIME_DID_NOT_START", "The preview runtime did not start within 60 seconds.");
559
+ }
560
+ const stored = await fetch(`${local}${PREVIEW_ROUTE_BASE}/props`, {
561
+ method: "POST",
562
+ headers: { "content-type": "application/json", "x-bcms-validator-key": validatorKey },
563
+ body: JSON.stringify({ componentId, props: entry.defaultProps })
564
+ });
565
+ if (!stored.ok) {
566
+ throw new ValidationFailure("COMPONENT_PREVIEW_RUNTIME_REFUSED", `The preview runtime refused the component (HTTP ${stored.status}).`);
567
+ }
568
+ const { id } = await stored.json();
569
+ const render = await renderInBrowser(
570
+ `${local}${PREVIEW_ROUTE_BASE}/render?id=${encodeURIComponent(id)}`,
571
+ nativeViewports,
572
+ manifest.brandTokenNames
573
+ );
574
+ const tarball = join2(work, "bundle.tgz");
575
+ if (spawnSync2("tar", ["-czf", tarball, "-C", out, "."], { stdio: "inherit" }).status !== 0) {
576
+ throw new ValidationFailure("COMPONENT_PREVIEW_BUNDLE_PACK_FAILED", "The preview runtime could not be packaged.");
577
+ }
578
+ const bytes = readFileSync2(tarball);
579
+ runtime.kill();
580
+ runtime = null;
581
+ const target = (await claimRequest()).request;
582
+ const checks = {
583
+ brandKit: {
584
+ status: render.missingTokens.length === 0 ? "passed" : "failed",
585
+ contractHash: target.brandContractHash,
586
+ missingTokens: render.missingTokens
587
+ },
588
+ runtime: {
589
+ status: render.runtimeErrors === 0 && render.consoleErrors === 0 ? "passed" : "failed",
590
+ runtimeErrors: render.runtimeErrors,
591
+ consoleErrors: render.consoleErrors
592
+ },
593
+ visual: { status: "baseline-missing", reviewRequired: true, viewports: render.results }
594
+ };
595
+ const upload = await api(`/api/v1/projects/${projectId}/artifacts/upload-url`, { method: "POST" });
596
+ const put = await fetch(upload.uploadUrl, { method: "PUT", body: bytes, headers: { "content-type": "application/gzip" } });
597
+ if (!put.ok) {
598
+ throw new ValidationFailure("COMPONENT_PREVIEW_BUNDLE_UPLOAD_FAILED", `Storage refused the preview bundle (HTTP ${put.status}).`);
599
+ }
600
+ await api(`/api/v1/projects/${projectId}/component-preview/bundles`, {
601
+ method: "POST",
602
+ body: { commitSha: target.commitSha, uploadKey: upload.uploadKey, checksum: `sha256:${sha256(bytes)}`, sizeBytes: bytes.byteLength }
603
+ });
604
+ const evidenceDigest = `sha256:${sha256(canonical({ requestId, commitSha: target.commitSha, checks }))}`;
605
+ await api(`${claimPath}/complete`, {
606
+ method: "POST",
607
+ claim: claim.claimCapability,
608
+ body: {
609
+ componentId: target.componentId,
610
+ candidateId: target.candidateId,
611
+ componentVersion: target.componentVersion,
612
+ familyKey: target.familyKey,
613
+ familyContractHash: target.familyContractHash,
614
+ schemaHash: target.schemaHash,
615
+ brandContractHash: target.brandContractHash,
616
+ dependenciesHash: target.dependenciesHash,
617
+ commitSha: target.commitSha,
618
+ adapterHash: target.adapterHash,
619
+ familyManifestHash: target.familyManifestHash,
620
+ nativeViewports: target.nativeViewports,
621
+ checks,
622
+ evidenceDigest
623
+ }
624
+ });
625
+ const passed = checks.brandKit.status === "passed" && checks.runtime.status === "passed";
626
+ console.log(`bcms-preview: validation ${passed ? "PASSED" : "FAILED"} for ${componentId}`);
627
+ if (!passed) {
628
+ if (render.missingTokens.length) console.log(` brand tokens used but not defined: ${render.missingTokens.join(", ")}`);
629
+ for (const problem of render.problems) console.log(` ${problem}`);
630
+ }
631
+ } catch (error) {
632
+ const code = error instanceof ValidationFailure ? error.code : "COMPONENT_VALIDATION_FAILED";
633
+ const message = error.message.slice(0, 2e3);
634
+ const reported = claim ?? await claimRequest().catch((claimError) => {
635
+ console.error(`bcms-preview: could not claim the request to report the failure: ${claimError.message}`);
636
+ return null;
637
+ });
638
+ if (reported) {
639
+ await api(`${claimPath}/fail`, {
640
+ method: "POST",
641
+ claim: reported.claimCapability,
642
+ body: { componentId, errorCode: code, errorMessage: message }
643
+ }).catch((reportError) => console.error(`bcms-preview: could not report the failure: ${reportError.message}`));
644
+ }
645
+ throw new CliFailure(`${code}: ${message}`);
646
+ } finally {
647
+ runtime?.kill();
648
+ rmSync2(work, { recursive: true, force: true });
649
+ }
650
+ }
651
+
652
+ // src/cli.ts
653
+ function arg(name) {
654
+ const index = process.argv.indexOf(`--${name}`);
655
+ return index === -1 ? void 0 : process.argv[index + 1];
656
+ }
657
+ async function main() {
658
+ const command = process.argv[2];
659
+ if (command === "build") {
660
+ const manifestPath = arg("manifest");
661
+ const out = arg("out");
662
+ if (!manifestPath || !out) throw new CliFailure("usage: bcms-preview build --manifest <file> --out <dir> [--cwd <dir>]");
663
+ console.log(JSON.stringify(buildPreviewRuntime({ root: arg("cwd") ?? process.cwd(), manifestPath, out })));
664
+ return;
665
+ }
666
+ if (command === "validate") {
667
+ await validateComponent();
668
+ return;
669
+ }
670
+ throw new CliFailure("usage: bcms-preview <build|validate>");
671
+ }
672
+ main().catch((error) => {
673
+ console.error(`bcms-preview: ${error instanceof CliFailure ? error.message : error.stack ?? String(error)}`);
674
+ process.exitCode = 1;
675
+ });
676
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/build.ts","../src/validate.ts","../src/cli.ts"],"sourcesContent":["/**\n * Builds a component preview runtime from an unmodified Next.js or Astro app. Used by `bcms-preview build`\n * and by the validator.\n *\n * The manifest names which file implements which component:\n * { \"components\": [{ \"id\": \"cmp_1\", \"source\": { \"path\": \"src/components/Hero.astro\" } }] }\n *\n * 🔴 NOTHING IN THE CUSTOMER'S REPOSITORY IS CHANGED. Routes and a registry are generated into the\n * checkout, the framework builds, the result is packaged as a runtime release, and every generated file\n * is removed again — including when the build fails. In CI the checkout is thrown away anyway; on a\n * developer machine this is the difference between a tool and a mess.\n */\nimport { spawnSync } from \"node:child_process\";\nimport { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { dirname, extname, join, relative, resolve, sep } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\ntype ManifestEntry = { id: string; source: { path: string; export?: string } };\ntype Manifest = { components: ManifestEntry[] };\ntype Framework = \"astro\" | \"next\";\n\nconst here = dirname(fileURLToPath(import.meta.url));\nconst PREVIEW_BASE = \"/__bettercms/component-preview\";\nconst COMPONENT_EXTENSIONS = new Set([\".astro\", \".tsx\", \".jsx\", \".ts\", \".js\", \".mjs\"]);\nconst IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * 🔴 THROWN, NOT `process.exit`. Writes to a piped stderr are asynchronous in Node, so exiting on the\n * next line dropped the message: in CI the step failed with no reason in the log at all. The single\n * handler at the bottom prints and sets the exit code, and the process ends once the write has flushed.\n */\nexport class CliFailure extends Error {}\n\nexport function fail(message: string): never {\n throw new CliFailure(message);\n}\n\n/**\n * Files a framework build rewrites in place. `next build` edits tsconfig.json and next-env.d.ts; an\n * `npm install --no-save` still rewrites the lockfile. Restored afterwards, so a preview build leaves the\n * app exactly as it found it.\n */\nfunction snapshot(root: string, names: string[]): () => void {\n const saved = names.map((name) => {\n const file = join(root, name);\n return { file, content: existsSync(file) ? readFileSync(file) : null };\n });\n return () => {\n for (const { file, content } of saved) {\n if (content === null) rmSync(file, { force: true });\n else writeFileSync(file, content);\n }\n };\n}\n\nconst MUTATED_BY_BUILD = [\"tsconfig.json\", \"next-env.d.ts\", \"package-lock.json\", \"pnpm-lock.yaml\", \"yarn.lock\", \"bun.lock\"];\n\nfunction readJson<T>(file: string): T {\n try {\n return JSON.parse(readFileSync(file, \"utf8\")) as T;\n } catch (error) {\n fail(`could not read ${file}: ${(error as Error).message}`);\n }\n}\n\n/** Manifest paths travel from an API into generated imports: relative, inside the app, and real. */\nexport function validateManifest(root: string, manifest: Manifest): ManifestEntry[] {\n if (!manifest || !Array.isArray(manifest.components) || manifest.components.length === 0) {\n fail(\"the manifest lists no components\");\n }\n const seen = new Set<string>();\n return manifest.components.map((entry) => {\n if (!entry || typeof entry.id !== \"string\" || !entry.id.trim()) fail(\"a manifest entry has no id\");\n if (seen.has(entry.id)) fail(`component ${entry.id} is listed twice`);\n seen.add(entry.id);\n const path = entry.source?.path;\n if (typeof path !== \"string\" || path.startsWith(\"/\") || path.includes(\"\\\\\") || path.split(\"/\").includes(\"..\")) {\n fail(`component ${entry.id} has an unsafe source path: ${String(path)}`);\n }\n if (!COMPONENT_EXTENSIONS.has(extname(path))) fail(`component ${entry.id}: unsupported file type ${extname(path)}`);\n if (!existsSync(join(root, path))) fail(`component ${entry.id}: ${path} does not exist`);\n const named = entry.source.export;\n if (named !== undefined && named !== \"default\" && !IDENTIFIER.test(named)) {\n fail(`component ${entry.id}: export \"${named}\" is not a valid identifier`);\n }\n return { id: entry.id, source: { path, export: named ?? \"default\" } };\n });\n}\n\nfunction detectFramework(root: string): Framework {\n const pkg = readJson<{ dependencies?: Record<string, string>; devDependencies?: Record<string, string> }>(join(root, \"package.json\"));\n const deps = { ...pkg.dependencies, ...pkg.devDependencies };\n if (deps.astro) return \"astro\";\n if (deps.next) return \"next\";\n fail(\"this app depends on neither astro nor next\");\n}\n\nfunction run(root: string, command: string, args: string[]) {\n const result = spawnSync(command, args, { cwd: root, stdio: \"inherit\", env: process.env });\n if (result.status !== 0) throw new Error(`${command} ${args.join(\" \")} exited with ${result.status}`);\n}\n\nfunction bin(root: string, name: string): string {\n const local = join(root, \"node_modules\", \".bin\", name);\n if (!existsSync(local)) fail(`${name} is not installed in ${root}. Install the app's dependencies first.`);\n return local;\n}\n\n/** A registry module: one import per component, keyed by component id. */\nfunction registrySource(fromDir: string, root: string, entries: ManifestEntry[]): string {\n const imports: string[] = [];\n const keys: string[] = [];\n entries.forEach((entry, index) => {\n let specifier = relative(fromDir, join(root, entry.source.path)).split(sep).join(\"/\");\n if (!specifier.startsWith(\".\")) specifier = `./${specifier}`;\n // TypeScript sources are imported without their extension, the way the app itself imports them.\n if ([\".tsx\", \".ts\", \".jsx\", \".js\"].includes(extname(specifier))) specifier = specifier.slice(0, -extname(specifier).length);\n const local = `Component${index}`;\n imports.push(entry.source.export === \"default\"\n ? `import ${local} from ${JSON.stringify(specifier)};`\n : `import { ${entry.source.export} as ${local} } from ${JSON.stringify(specifier)};`);\n keys.push(` ${JSON.stringify(entry.id)}: ${local},`);\n });\n return `${imports.join(\"\\n\")}\\n\\nexport const registry: Record<string, any> = {\\n${keys.join(\"\\n\")}\\n};\\n\\nexport const has = (componentId: string): boolean => Object.prototype.hasOwnProperty.call(registry, componentId);\\n`;\n}\n\nfunction writeRuntimeLibrary(dir: string) {\n writeFileSync(join(dir, \"server.mjs\"), readFileSync(join(here, \"server.js\"), \"utf8\"));\n const types = join(here, \"server.d.ts\");\n if (existsSync(types)) writeFileSync(join(dir, \"server.d.mts\"), readFileSync(types, \"utf8\"));\n writeFileSync(join(dir, \"shell.ts\"), `export default ${JSON.stringify(readFileSync(join(here, \"shell.global.js\"), \"utf8\"))};\\n`);\n}\n\n/** CSS the app's layouts import. The render page has no layout, so it imports them itself. */\nfunction astroGlobalStyles(root: string): string[] {\n const found = new Set<string>();\n const scan = (dir: string) => {\n if (!existsSync(dir)) return;\n for (const name of readdirSync(dir)) {\n const full = join(dir, name);\n if (statSync(full).isDirectory()) scan(full);\n else if (name.endsWith(\".astro\")) {\n for (const match of readFileSync(full, \"utf8\").matchAll(/^\\s*import\\s+[\"']([^\"']+\\.css)[\"'];?/gm)) {\n const target = resolve(dirname(full), match[1]!);\n if (target.startsWith(root) && existsSync(target)) found.add(target);\n }\n }\n }\n };\n scan(join(root, \"src\", \"layouts\"));\n return [...found];\n}\n\nconst ASTRO_NODE_ADAPTER: Record<string, string> = { \"5\": \"^9\", \"6\": \"^10\", \"7\": \"^11\" };\n\nfunction ensureAstroNodeAdapter(root: string) {\n const require = createRequire(join(root, \"package.json\"));\n try {\n require.resolve(\"@astrojs/node\");\n return;\n } catch {\n // Not installed: add it without touching package.json or the lockfile.\n }\n const astroVersion = readJson<{ version: string }>(require.resolve(\"astro/package.json\")).version;\n const range = ASTRO_NODE_ADAPTER[astroVersion.split(\".\")[0]!];\n if (!range) fail(`Astro ${astroVersion} is not supported for component previews yet`);\n run(root, \"npm\", [\"install\", \"--no-save\", \"--no-audit\", \"--no-fund\", `@astrojs/node@${range}`]);\n}\n\nfunction buildAstro(root: string, entries: ManifestEntry[], out: string) {\n const configName = [\"astro.config.mjs\", \"astro.config.js\", \"astro.config.ts\", \"astro.config.mts\"].find((f) => existsSync(join(root, f)));\n if (!configName) fail(\"no astro.config file found\");\n ensureAstroNodeAdapter(root);\n\n const gen = join(root, \".bcms-preview\");\n const wrapper = join(root, \"astro.config.bcms-preview.mjs\");\n rmSync(gen, { recursive: true, force: true });\n mkdirSync(gen, { recursive: true });\n try {\n writeRuntimeLibrary(gen);\n writeFileSync(join(gen, \"registry.ts\"), registrySource(gen, root, entries));\n const handler = (method: string, body: string) =>\n `export const prerender = false;\\nexport const ${method} = ${body};\\n`;\n writeFileSync(join(gen, \"runtime.ts\"), `import { handleRuntime } from \"./server.mjs\";\\nimport shell from \"./shell\";\\n${handler(\"GET\", \"() => handleRuntime(shell)\")}`);\n writeFileSync(join(gen, \"session.ts\"), `import { handleSession } from \"./server.mjs\";\\nimport { has } from \"./registry\";\\n${handler(\"POST\", \"({ request }: { request: Request }) => handleSession(request, has)\")}`);\n writeFileSync(join(gen, \"props.ts\"), `import { handleProps } from \"./server.mjs\";\\nimport { has } from \"./registry\";\\n${handler(\"POST\", \"({ request }: { request: Request }) => handleProps(request, has)\")}`);\n writeFileSync(join(gen, \"health.ts\"), `import { handleHealth } from \"./server.mjs\";\\n${handler(\"GET\", \"() => handleHealth()\")}`);\n const styles = astroGlobalStyles(root)\n .map((file) => `import ${JSON.stringify(relative(gen, file).split(sep).join(\"/\"))};`)\n .join(\"\\n\");\n writeFileSync(join(gen, \"render.astro\"), `---\n${styles}\nimport { renderEntry, renderHeaders } from \"./server.mjs\";\nimport { registry } from \"./registry\";\nexport const prerender = false;\nconst headers = renderHeaders();\nconst entry = renderEntry(Astro.url.searchParams.get(\"id\"));\nconst Component = entry ? registry[entry.componentId] : undefined;\nfor (const [name, value] of Object.entries(headers)) Astro.response.headers.set(name, value);\nif (!entry || !Component) return new Response(\"Not found\", { status: 404, headers });\n---\n<html lang=\"en\" data-bcms-preview-render=\"1\">\n <head><meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /></head>\n <body><Component {...entry.props} /></body>\n</html>\n`);\n writeFileSync(wrapper, `import user from \"./${configName}\";\nimport node from \"@astrojs/node\";\n\nconst routes = [\"runtime.ts\", \"session.ts\", \"props.ts\", \"render.astro\", \"health.ts\"];\n\nexport default {\n ...user,\n output: \"server\",\n base: ${JSON.stringify(PREVIEW_BASE)},\n adapter: node({ mode: \"standalone\" }),\n integrations: [\n ...(user.integrations ?? []),\n {\n name: \"bettercms-component-preview\",\n hooks: {\n \"astro:config:setup\": ({ injectRoute }) => {\n for (const file of routes) {\n injectRoute({\n pattern: \"/__bcms/\" + file.replace(/\\\\.(ts|astro)$/, \"\"),\n entrypoint: new URL(\"./.bcms-preview/\" + file, import.meta.url),\n prerender: false,\n });\n }\n },\n },\n },\n ],\n};\n`);\n rmSync(join(root, \"dist\"), { recursive: true, force: true });\n run(root, bin(root, \"astro\"), [\"build\", \"--config\", \"astro.config.bcms-preview.mjs\"]);\n } finally {\n rmSync(gen, { recursive: true, force: true });\n rmSync(wrapper, { force: true });\n }\n\n const app = join(out, \"app\");\n mkdirSync(app, { recursive: true });\n cpSync(join(root, \"dist\"), app, { recursive: true });\n cpSync(join(root, \"package.json\"), join(app, \"package.json\"));\n // Astro does not bundle its dependencies, so the server entry needs them on disk.\n cpSync(join(root, \"node_modules\"), join(app, \"node_modules\"), { recursive: true, verbatimSymlinks: true });\n writeFileSync(join(out, \"bcms-runtime.json\"), `${JSON.stringify({ kind: \"node\", dir: \"app\", entry: \"server/entry.mjs\" })}\\n`);\n}\n\nfunction buildNext(root: string, entries: ManifestEntry[], out: string) {\n const appDir = [\"app\", join(\"src\", \"app\")].map((d) => join(root, d)).find((d) => existsSync(d));\n if (!appDir) fail(\"no App Router directory (app/ or src/app/) found\");\n const configName = [\"next.config.mjs\", \"next.config.js\", \"next.config.ts\", \"next.config.cjs\"].find((f) => existsSync(join(root, f)));\n\n // `%5F%5Fbcms` is how a URL segment starting with an underscore is spelled in the App Router: a plain\n // `__bcms` folder is a PRIVATE folder and silently produces no routes at all.\n const gen = join(appDir, \"%5F%5Fbcms\");\n const userConfig = configName ? join(root, configName.replace(\"next.config\", \"next.config.bcms-user\")) : null;\n const wrapperName = configName && configName.endsWith(\".ts\") ? \"next.config.ts\" : \"next.config.mjs\";\n rmSync(gen, { recursive: true, force: true });\n mkdirSync(gen, { recursive: true });\n if (configName && userConfig) renameSync(join(root, configName), userConfig);\n try {\n writeRuntimeLibrary(gen);\n writeFileSync(join(gen, \"registry.ts\"), registrySource(gen, root, entries));\n const route = (name: string, source: string) => {\n mkdirSync(join(gen, name), { recursive: true });\n writeFileSync(join(gen, name, \"route.ts\"), `export const dynamic = \"force-dynamic\";\\n${source}`);\n };\n route(\"runtime\", `import { handleRuntime } from \"../server.mjs\";\\nimport shell from \"../shell\";\\nexport function GET() {\\n return handleRuntime(shell);\\n}\\n`);\n route(\"session\", `import { handleSession } from \"../server.mjs\";\\nimport { has } from \"../registry\";\\nexport function POST(request: Request) {\\n return handleSession(request, has);\\n}\\n`);\n route(\"props\", `import { handleProps } from \"../server.mjs\";\\nimport { has } from \"../registry\";\\nexport function POST(request: Request) {\\n return handleProps(request, has);\\n}\\n`);\n route(\"health\", `import { handleHealth } from \"../server.mjs\";\\nexport function GET() {\\n return handleHealth();\\n}\\n`);\n mkdirSync(join(gen, \"render\"), { recursive: true });\n writeFileSync(join(gen, \"render\", \"page.tsx\"), `import { notFound } from \"next/navigation\";\nimport { renderEntry } from \"../server.mjs\";\nimport { registry } from \"../registry\";\n\nexport const dynamic = \"force-dynamic\";\n\nexport default async function BetterCMSComponentPreview({ searchParams }: { searchParams: Promise<{ id?: string }> }) {\n const { id } = await searchParams;\n const entry = renderEntry(id);\n const Component = entry ? registry[entry.componentId] : undefined;\n if (!entry || !Component) notFound();\n // The marker the runtime page checks before acknowledging: a 404 or an error page never carries it.\n return (\n <>\n <Component {...entry.props} />\n <template data-bcms-preview-render=\"1\" />\n </>\n );\n}\n`);\n const importUser = userConfig ? `import user from \"./${relative(root, userConfig)}\";` : \"const user = {};\";\n writeFileSync(join(root, wrapperName), `${importUser}\n\n// No frame-ancestors here: next.config headers are fixed at build time, and the dashboard origin the\n// render frame must allow is runtime configuration. The platform's proxy sets it for this whole prefix.\nconst RENDER_HEADERS = [\n { key: \"referrer-policy\", value: \"no-referrer\" },\n { key: \"cache-control\", value: \"no-store\" },\n];\n\nexport default async function betterCMSComponentPreviewConfig(phase, context) {\n const resolved = typeof user === \"function\" ? await user(phase, context) : user;\n const userHeaders = resolved.headers;\n return {\n ...resolved,\n output: \"standalone\",\n basePath: ${JSON.stringify(PREVIEW_BASE)},\n async headers() {\n const own = typeof userHeaders === \"function\" ? await userHeaders() : [];\n return [...own, { source: \"/__bcms/render\", headers: RENDER_HEADERS }];\n },\n };\n}\n`);\n rmSync(join(root, \".next\"), { recursive: true, force: true });\n run(root, bin(root, \"next\"), [\"build\"]);\n } finally {\n rmSync(gen, { recursive: true, force: true });\n rmSync(join(root, wrapperName), { force: true });\n if (configName && userConfig && existsSync(userConfig)) renameSync(userConfig, join(root, configName));\n }\n\n const standalone = join(root, \".next\", \"standalone\");\n if (!existsSync(join(standalone, \"server.js\"))) fail(\"next build produced no standalone server\");\n const app = join(out, \"app\");\n mkdirSync(app, { recursive: true });\n cpSync(standalone, app, { recursive: true, verbatimSymlinks: true });\n if (existsSync(join(root, \".next\", \"static\"))) cpSync(join(root, \".next\", \"static\"), join(app, \".next\", \"static\"), { recursive: true });\n if (existsSync(join(root, \"public\"))) cpSync(join(root, \"public\"), join(app, \"public\"), { recursive: true });\n writeFileSync(join(out, \"bcms-runtime.json\"), `${JSON.stringify({ kind: \"node\", dir: \"app\", entry: \"server.js\" })}\\n`);\n}\n\nexport function buildPreviewRuntime(input: { root: string; manifestPath: string; out: string }) {\n const root = resolve(input.root);\n const out = resolve(input.out);\n const entries = validateManifest(root, readJson<Manifest>(resolve(input.manifestPath)));\n const framework = detectFramework(root);\n\n rmSync(out, { recursive: true, force: true });\n mkdirSync(out, { recursive: true });\n const restore = snapshot(root, MUTATED_BY_BUILD);\n try {\n if (framework === \"astro\") buildAstro(root, entries, out);\n else buildNext(root, entries, out);\n } finally {\n restore();\n }\n return { framework, out, components: entries.map((e) => e.id) };\n}\n","/**\n * `bcms-preview validate` — validates one component in CI with nothing configured in the repository.\n *\n * Run by `.github/workflows/bcms-component-validation.yml`, which BetterCMS commits and dispatches. In\n * order: build a preview runtime from the app as it is, render the component with its default props in a\n * real browser at every native viewport, check brand tokens and the console, package the runtime — and only\n * then claim the request, upload the bundle the platform will serve previews from, and complete it.\n *\n * 🔴 NOTHING FAILS SILENTLY. A component whose checks fail is still COMPLETED — failed evidence is a\n * result the dashboard shows, with the failing checks. Anything that prevents a result (no source file\n * recorded, a build that breaks, a runtime that never starts, no browser) FAILS the request with a named\n * code and the reason, so the panel says what happened instead of waiting for the request to expire.\n */\nimport { spawn, spawnSync, type ChildProcess } from \"node:child_process\";\nimport { createHash, randomBytes } from \"node:crypto\";\nimport { mkdtempSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { createServer } from \"node:net\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { buildPreviewRuntime, CliFailure } from \"./build\";\n\nconst PREVIEW_ROUTE_BASE = \"/__bettercms/component-preview/__bcms\";\nconst RUNTIME_PATH = `${PREVIEW_ROUTE_BASE}/runtime`;\n\ntype Viewport = { name: string; width: number; height: number };\ntype RequestTarget = {\n componentId: string;\n componentVersion: number;\n candidateId: string;\n familyKey: string;\n familyContractHash: string;\n schemaHash: string;\n brandContractHash: string;\n dependenciesHash: string;\n commitSha: string;\n adapterHash: string;\n familyManifestHash: string;\n nativeViewports: Viewport[];\n};\ntype Manifest = {\n components: { id: string; source: { path: string; export: string }; defaultProps: Record<string, unknown> }[];\n brandTokenNames: string[];\n};\n\nclass ValidationFailure extends Error {\n constructor(readonly code: string, message: string) {\n super(message);\n this.name = \"ValidationFailure\";\n }\n}\n\nfunction required(name: string): string {\n const value = process.env[name]?.trim();\n if (!value) throw new CliFailure(`${name} is not set. This command runs inside the BetterCMS component validation workflow.`);\n return value;\n}\n\nconst sha256 = (data: string | Buffer) => createHash(\"sha256\").update(data).digest(\"hex\");\n\nfunction canonical(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonical).join(\",\")}]`;\n if (value && typeof value === \"object\") {\n return `{${Object.keys(value as Record<string, unknown>).sort()\n .map((key) => `${JSON.stringify(key)}:${canonical((value as Record<string, unknown>)[key])}`).join(\",\")}}`;\n }\n return JSON.stringify(value);\n}\n\nfunction freePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const server = createServer();\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n const port = typeof address === \"object\" && address ? address.port : 0;\n server.close(() => resolve(port));\n });\n });\n}\n\nasync function waitForOk(url: string, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n try {\n if ((await fetch(url)).ok) return true;\n } catch {\n // not up yet\n }\n await new Promise((r) => setTimeout(r, 500));\n }\n return false;\n}\n\n/** Install Chromium for the bundled Playwright. On a Linux CI runner, with its system dependencies. */\nfunction ensureBrowser() {\n const require = createRequire(import.meta.url);\n const cli = join(dirname(require.resolve(\"playwright/package.json\")), \"cli.js\");\n const args = [cli, \"install\", \"chromium\"];\n if (process.platform === \"linux\" && process.env.CI) args.push(\"--with-deps\");\n const result = spawnSync(process.execPath, args, { stdio: \"inherit\" });\n if (result.status !== 0) {\n throw new ValidationFailure(\"COMPONENT_VALIDATION_BROWSER_UNAVAILABLE\", \"A headless browser could not be installed on this runner.\");\n }\n}\n\nasync function renderInBrowser(url: string, viewports: Viewport[], tokenNames: string[]) {\n ensureBrowser();\n const { chromium } = await import(\"playwright\");\n const browser = await chromium.launch();\n try {\n let consoleErrors = 0;\n let runtimeErrors = 0;\n const missingTokens = new Set<string>();\n const results: { name: string; width: number; height: number; status: \"baseline-missing\"; candidateDigest: string }[] = [];\n const problems: string[] = [];\n for (const viewport of viewports) {\n const page = await browser.newPage({ viewport: { width: viewport.width, height: viewport.height } });\n page.on(\"console\", (message) => {\n if (message.type() === \"error\") {\n consoleErrors += 1;\n problems.push(`${viewport.name} console: ${message.text().slice(0, 200)}`);\n }\n });\n page.on(\"pageerror\", (error) => {\n runtimeErrors += 1;\n problems.push(`${viewport.name} error: ${error.message.slice(0, 200)}`);\n });\n const response = await page.goto(url, { waitUntil: \"load\", timeout: 45_000 });\n await page.waitForLoadState(\"networkidle\", { timeout: 15_000 }).catch(() => {});\n const rendered = await page.locator(\"[data-bcms-preview-render]\").count();\n if (!response?.ok() || rendered === 0) {\n runtimeErrors += 1;\n problems.push(`${viewport.name}: the component page did not render (HTTP ${response?.status() ?? \"none\"})`);\n }\n /**\n * A token is MISSING only when the rendered page uses it and it resolves to nothing. `--bcms-*` are\n * guaranteed on pages BetterCMS renders itself, not in a customer's framework build, so asking\n * \"is every token defined?\" would fail every starter while saying nothing about the component. The\n * question worth a failure is \"does this component reference a brand token the app never defines?\"\n * — a component that will render unstyled on the live site. Cross-origin stylesheets cannot be read\n * and are skipped.\n */\n const missing = await page.evaluate((names: string[]) => {\n const referenced = new Set<string>();\n const scan = (text: string) => {\n for (const match of text.matchAll(/var\\(\\s*(--[A-Za-z0-9_-]+)/g)) referenced.add(match[1]!);\n };\n for (const sheet of Array.from(document.styleSheets)) {\n try {\n for (const rule of Array.from(sheet.cssRules)) scan(rule.cssText);\n } catch {\n // cross-origin sheet\n }\n }\n document.querySelectorAll(\"[style]\").forEach((element) => scan(element.getAttribute(\"style\") ?? \"\"));\n const style = getComputedStyle(document.documentElement);\n return names.filter((name) => referenced.has(name) && !style.getPropertyValue(name).trim());\n }, tokenNames);\n for (const token of missing) missingTokens.add(token);\n const png = await page.screenshot({ fullPage: true });\n results.push({\n name: viewport.name,\n width: viewport.width,\n height: viewport.height,\n // No baseline exists for a first validation. The database requires this exact shape for it:\n // no baseline or diff digest, and the check marked as needing review.\n status: \"baseline-missing\",\n candidateDigest: `sha256:${sha256(png)}`,\n });\n await page.close();\n }\n return { results, consoleErrors, runtimeErrors, missingTokens: [...missingTokens].sort(), problems };\n } finally {\n await browser.close();\n }\n}\n\nexport async function validateComponent() {\n const apiUrl = required(\"BCMS_API_URL\");\n const apiKey = required(\"BCMS_API_KEY\");\n const projectId = required(\"BCMS_PROJECT_ID\");\n const requestId = required(\"BCMS_REQUEST_ID\");\n const componentId = required(\"BCMS_COMPONENT_ID\");\n const commitSha = required(\"BCMS_COMMIT_SHA\");\n const familyKey = required(\"BCMS_FAMILY_KEY\");\n const previewOrigin = required(\"BCMS_PREVIEW_ORIGIN\");\n const nativeViewports = JSON.parse(required(\"BCMS_NATIVE_VIEWPORTS\")) as Viewport[];\n\n const api = async <T>(path: string, init: { method?: string; body?: unknown; claim?: string } = {}): Promise<T> => {\n const response = await fetch(new URL(path, apiUrl), {\n method: init.method ?? \"GET\",\n headers: {\n authorization: `Bearer ${apiKey}`,\n \"content-type\": \"application/json\",\n ...(init.claim ? { \"x-bcms-component-claim\": init.claim } : {}),\n },\n ...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),\n });\n const text = await response.text();\n let parsed: { data?: unknown; error?: unknown; message?: unknown } | null = null;\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = null;\n }\n if (!response.ok) {\n const code = typeof parsed?.error === \"string\" && /^[A-Z][A-Z0-9_]*$/.test(parsed.error) ? parsed.error : `API_HTTP_${response.status}`;\n const detail = typeof parsed?.message === \"string\" ? parsed.message : typeof parsed?.error === \"string\" ? parsed.error : text.slice(0, 300);\n throw new ValidationFailure(code, `${init.method ?? \"GET\"} ${path} answered ${response.status}: ${detail}`);\n }\n return ((parsed && \"data\" in parsed ? parsed.data : parsed) ?? {}) as T;\n };\n\n const claimPath = `/api/v1/projects/${projectId}/component-implementation/implementation-requests/${requestId}`;\n let claim: { request: RequestTarget; claimCapability: string } | null = null;\n /**\n * 🔴 CLAIMED LAST, NOT FIRST. A claim credential lives minutes, and `complete` refuses an expired one.\n * Claiming before a framework build, a browser install and a render meant every real run finished with\n * a credential that had already lapsed — and the request sat \"running\" until it expired. Everything slow\n * happens first; the claim is followed only by an upload and the completion.\n */\n const claimRequest = async () => {\n claim = await api<{ request: RequestTarget; claimCapability: string }>(`${claimPath}/claim`, {\n method: \"POST\",\n body: {\n componentId,\n commitSha,\n adapter: { protocol: \"bcms-component-runtime-v1\", kind: \"project-route\", path: RUNTIME_PATH, familyKey, previewOrigin, nativeViewports },\n providerRunId: required(\"BCMS_RUN_ID\"),\n providerRunAttempt: Number(process.env.BCMS_RUN_ATTEMPT) || 1,\n providerRunUrl: required(\"BCMS_RUN_URL\"),\n workflowRef: required(\"BCMS_WORKFLOW_REF\"),\n // The server caps this at the request's own expiry and at ten minutes.\n credentialExpiresAt: new Date(Date.now() + 10 * 60_000 - 5_000).toISOString(),\n },\n });\n return claim;\n };\n\n const work = mkdtempSync(join(tmpdir(), \"bcms-validate-\"));\n let runtime: ChildProcess | null = null;\n try {\n const manifest = await api<Manifest>(`/api/v1/projects/${projectId}/component-preview/manifest`);\n const entry = manifest.components.find((c) => c.id === componentId);\n if (!entry) {\n throw new ValidationFailure(\n \"COMPONENT_SOURCE_NOT_RECORDED\",\n \"No file is recorded as this component's source. The agent that writes a component records it with set_component_source.\",\n );\n }\n\n const manifestPath = join(work, \"manifest.json\");\n writeFileSync(manifestPath, JSON.stringify({ components: manifest.components.map(({ id, source }) => ({ id, source })) }));\n const out = join(work, \"runtime\");\n try {\n buildPreviewRuntime({ root: process.cwd(), manifestPath, out });\n } catch (error) {\n throw new ValidationFailure(\"COMPONENT_PREVIEW_BUILD_FAILED\", `The preview build failed: ${(error as Error).message}`);\n }\n\n const runtimeManifest = JSON.parse(readFileSync(join(out, \"bcms-runtime.json\"), \"utf8\")) as { dir: string; entry: string };\n const port = await freePort();\n const validatorKey = randomBytes(32).toString(\"base64url\");\n const local = `http://127.0.0.1:${port}`;\n // 🔴 The customer's code runs in this process tree. It gets no BetterCMS API key.\n const { BCMS_API_KEY: _withheld, ...inherited } = process.env;\n runtime = spawn(process.execPath, [join(out, runtimeManifest.dir, runtimeManifest.entry)], {\n cwd: join(out, runtimeManifest.dir),\n env: {\n ...inherited,\n NODE_ENV: \"production\",\n PORT: String(port),\n HOST: \"127.0.0.1\",\n HOSTNAME: \"127.0.0.1\",\n BCMS_API_URL: apiUrl,\n BCMS_DASHBOARD_ORIGIN: local,\n BCMS_PREVIEW_ORIGIN: local,\n BCMS_PREVIEW_VALIDATOR_KEY: validatorKey,\n },\n stdio: [\"ignore\", \"inherit\", \"inherit\"],\n });\n if (!(await waitForOk(`${local}${PREVIEW_ROUTE_BASE}/health`, 60_000))) {\n throw new ValidationFailure(\"COMPONENT_PREVIEW_RUNTIME_DID_NOT_START\", \"The preview runtime did not start within 60 seconds.\");\n }\n\n const stored = await fetch(`${local}${PREVIEW_ROUTE_BASE}/props`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"x-bcms-validator-key\": validatorKey },\n body: JSON.stringify({ componentId, props: entry.defaultProps }),\n });\n if (!stored.ok) {\n throw new ValidationFailure(\"COMPONENT_PREVIEW_RUNTIME_REFUSED\", `The preview runtime refused the component (HTTP ${stored.status}).`);\n }\n const { id } = (await stored.json()) as { id: string };\n\n // The viewports the dispatch declared are exactly the ones the claim records as the adapter's.\n const render = await renderInBrowser(\n `${local}${PREVIEW_ROUTE_BASE}/render?id=${encodeURIComponent(id)}`,\n nativeViewports,\n manifest.brandTokenNames,\n );\n\n const tarball = join(work, \"bundle.tgz\");\n if (spawnSync(\"tar\", [\"-czf\", tarball, \"-C\", out, \".\"], { stdio: \"inherit\" }).status !== 0) {\n throw new ValidationFailure(\"COMPONENT_PREVIEW_BUNDLE_PACK_FAILED\", \"The preview runtime could not be packaged.\");\n }\n const bytes = readFileSync(tarball);\n runtime.kill();\n runtime = null;\n\n const target = (await claimRequest()).request;\n const checks = {\n brandKit: {\n status: render.missingTokens.length === 0 ? \"passed\" : \"failed\",\n contractHash: target.brandContractHash,\n missingTokens: render.missingTokens,\n },\n runtime: {\n status: render.runtimeErrors === 0 && render.consoleErrors === 0 ? \"passed\" : \"failed\",\n runtimeErrors: render.runtimeErrors,\n consoleErrors: render.consoleErrors,\n },\n visual: { status: \"baseline-missing\", reviewRequired: true, viewports: render.results },\n };\n\n // Uploaded before completing: validated evidence pointing at a runtime nobody can start renders nothing.\n const upload = await api<{ uploadUrl: string; uploadKey: string }>(`/api/v1/projects/${projectId}/artifacts/upload-url`, { method: \"POST\" });\n const put = await fetch(upload.uploadUrl, { method: \"PUT\", body: bytes, headers: { \"content-type\": \"application/gzip\" } });\n if (!put.ok) {\n throw new ValidationFailure(\"COMPONENT_PREVIEW_BUNDLE_UPLOAD_FAILED\", `Storage refused the preview bundle (HTTP ${put.status}).`);\n }\n await api(`/api/v1/projects/${projectId}/component-preview/bundles`, {\n method: \"POST\",\n body: { commitSha: target.commitSha, uploadKey: upload.uploadKey, checksum: `sha256:${sha256(bytes)}`, sizeBytes: bytes.byteLength },\n });\n\n const evidenceDigest = `sha256:${sha256(canonical({ requestId, commitSha: target.commitSha, checks }))}`;\n await api(`${claimPath}/complete`, {\n method: \"POST\",\n claim: claim!.claimCapability,\n body: {\n componentId: target.componentId,\n candidateId: target.candidateId,\n componentVersion: target.componentVersion,\n familyKey: target.familyKey,\n familyContractHash: target.familyContractHash,\n schemaHash: target.schemaHash,\n brandContractHash: target.brandContractHash,\n dependenciesHash: target.dependenciesHash,\n commitSha: target.commitSha,\n adapterHash: target.adapterHash,\n familyManifestHash: target.familyManifestHash,\n nativeViewports: target.nativeViewports,\n checks,\n evidenceDigest,\n },\n });\n\n const passed = checks.brandKit.status === \"passed\" && checks.runtime.status === \"passed\";\n console.log(`bcms-preview: validation ${passed ? \"PASSED\" : \"FAILED\"} for ${componentId}`);\n if (!passed) {\n if (render.missingTokens.length) console.log(` brand tokens used but not defined: ${render.missingTokens.join(\", \")}`);\n for (const problem of render.problems) console.log(` ${problem}`);\n }\n } catch (error) {\n const code = error instanceof ValidationFailure ? error.code : \"COMPONENT_VALIDATION_FAILED\";\n const message = (error as Error).message.slice(0, 2000);\n // A failure before the claim is still reported: claim, then fail at once, so the dashboard names the\n // reason instead of showing \"running\" until the request expires.\n const reported = claim ?? await claimRequest().catch((claimError) => {\n console.error(`bcms-preview: could not claim the request to report the failure: ${(claimError as Error).message}`);\n return null;\n });\n if (reported) {\n await api(`${claimPath}/fail`, {\n method: \"POST\",\n claim: reported.claimCapability,\n body: { componentId, errorCode: code, errorMessage: message },\n }).catch((reportError) => console.error(`bcms-preview: could not report the failure: ${(reportError as Error).message}`));\n }\n throw new CliFailure(`${code}: ${message}`);\n } finally {\n runtime?.kill();\n rmSync(work, { recursive: true, force: true });\n }\n}\n","/**\n * bcms-preview\n *\n * bcms-preview build --manifest <file> --out <dir> [--cwd <dir>]\n * bcms-preview validate (in CI; configured entirely by BCMS_* environment variables)\n */\nimport { buildPreviewRuntime, CliFailure } from \"./build\";\nimport { validateComponent } from \"./validate\";\n\nfunction arg(name: string): string | undefined {\n const index = process.argv.indexOf(`--${name}`);\n return index === -1 ? undefined : process.argv[index + 1];\n}\n\nasync function main() {\n const command = process.argv[2];\n if (command === \"build\") {\n const manifestPath = arg(\"manifest\");\n const out = arg(\"out\");\n if (!manifestPath || !out) throw new CliFailure(\"usage: bcms-preview build --manifest <file> --out <dir> [--cwd <dir>]\");\n console.log(JSON.stringify(buildPreviewRuntime({ root: arg(\"cwd\") ?? process.cwd(), manifestPath, out })));\n return;\n }\n if (command === \"validate\") {\n await validateComponent();\n return;\n }\n throw new CliFailure(\"usage: bcms-preview <build|validate>\");\n}\n\n/**\n * One handler for every failure, and no `process.exit`: writes to a piped stderr are asynchronous, so\n * exiting right after the write dropped the message and CI showed a failed step with no reason.\n */\nmain().catch((error) => {\n console.error(`bcms-preview: ${error instanceof CliFailure ? error.message : (error as Error).stack ?? String(error)}`);\n process.exitCode = 1;\n});\n"],"mappings":";;;AAYA,SAAS,iBAAiB;AAC1B,SAAS,QAAQ,YAAY,WAAW,cAAc,aAAa,YAAY,QAAQ,UAAU,qBAAqB;AACtH,SAAS,qBAAqB;AAC9B,SAAS,SAAS,SAAS,MAAM,UAAU,SAAS,WAAW;AAC/D,SAAS,qBAAqB;AAM9B,IAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AACnD,IAAM,eAAe;AACrB,IAAM,uBAAuB,oBAAI,IAAI,CAAC,UAAU,QAAQ,QAAQ,OAAO,OAAO,MAAM,CAAC;AACrF,IAAM,aAAa;AAOZ,IAAM,aAAN,cAAyB,MAAM;AAAC;AAEhC,SAAS,KAAK,SAAwB;AAC3C,QAAM,IAAI,WAAW,OAAO;AAC9B;AAOA,SAAS,SAAS,MAAc,OAA6B;AAC3D,QAAM,QAAQ,MAAM,IAAI,CAAC,SAAS;AAChC,UAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,WAAO,EAAE,MAAM,SAAS,WAAW,IAAI,IAAI,aAAa,IAAI,IAAI,KAAK;AAAA,EACvE,CAAC;AACD,SAAO,MAAM;AACX,eAAW,EAAE,MAAM,QAAQ,KAAK,OAAO;AACrC,UAAI,YAAY,KAAM,QAAO,MAAM,EAAE,OAAO,KAAK,CAAC;AAAA,UAC7C,eAAc,MAAM,OAAO;AAAA,IAClC;AAAA,EACF;AACF;AAEA,IAAM,mBAAmB,CAAC,iBAAiB,iBAAiB,qBAAqB,kBAAkB,aAAa,UAAU;AAE1H,SAAS,SAAY,MAAiB;AACpC,MAAI;AACF,WAAO,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;AAAA,EAC9C,SAAS,OAAO;AACd,SAAK,kBAAkB,IAAI,KAAM,MAAgB,OAAO,EAAE;AAAA,EAC5D;AACF;AAGO,SAAS,iBAAiB,MAAc,UAAqC;AAClF,MAAI,CAAC,YAAY,CAAC,MAAM,QAAQ,SAAS,UAAU,KAAK,SAAS,WAAW,WAAW,GAAG;AACxF,SAAK,kCAAkC;AAAA,EACzC;AACA,QAAM,OAAO,oBAAI,IAAY;AAC7B,SAAO,SAAS,WAAW,IAAI,CAAC,UAAU;AACxC,QAAI,CAAC,SAAS,OAAO,MAAM,OAAO,YAAY,CAAC,MAAM,GAAG,KAAK,EAAG,MAAK,4BAA4B;AACjG,QAAI,KAAK,IAAI,MAAM,EAAE,EAAG,MAAK,aAAa,MAAM,EAAE,kBAAkB;AACpE,SAAK,IAAI,MAAM,EAAE;AACjB,UAAM,OAAO,MAAM,QAAQ;AAC3B,QAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,MAAM,GAAG,EAAE,SAAS,IAAI,GAAG;AAC7G,WAAK,aAAa,MAAM,EAAE,+BAA+B,OAAO,IAAI,CAAC,EAAE;AAAA,IACzE;AACA,QAAI,CAAC,qBAAqB,IAAI,QAAQ,IAAI,CAAC,EAAG,MAAK,aAAa,MAAM,EAAE,2BAA2B,QAAQ,IAAI,CAAC,EAAE;AAClH,QAAI,CAAC,WAAW,KAAK,MAAM,IAAI,CAAC,EAAG,MAAK,aAAa,MAAM,EAAE,KAAK,IAAI,iBAAiB;AACvF,UAAM,QAAQ,MAAM,OAAO;AAC3B,QAAI,UAAU,UAAa,UAAU,aAAa,CAAC,WAAW,KAAK,KAAK,GAAG;AACzE,WAAK,aAAa,MAAM,EAAE,aAAa,KAAK,6BAA6B;AAAA,IAC3E;AACA,WAAO,EAAE,IAAI,MAAM,IAAI,QAAQ,EAAE,MAAM,QAAQ,SAAS,UAAU,EAAE;AAAA,EACtE,CAAC;AACH;AAEA,SAAS,gBAAgB,MAAyB;AAChD,QAAM,MAAM,SAA8F,KAAK,MAAM,cAAc,CAAC;AACpI,QAAM,OAAO,EAAE,GAAG,IAAI,cAAc,GAAG,IAAI,gBAAgB;AAC3D,MAAI,KAAK,MAAO,QAAO;AACvB,MAAI,KAAK,KAAM,QAAO;AACtB,OAAK,4CAA4C;AACnD;AAEA,SAAS,IAAI,MAAc,SAAiB,MAAgB;AAC1D,QAAM,SAAS,UAAU,SAAS,MAAM,EAAE,KAAK,MAAM,OAAO,WAAW,KAAK,QAAQ,IAAI,CAAC;AACzF,MAAI,OAAO,WAAW,EAAG,OAAM,IAAI,MAAM,GAAG,OAAO,IAAI,KAAK,KAAK,GAAG,CAAC,gBAAgB,OAAO,MAAM,EAAE;AACtG;AAEA,SAAS,IAAI,MAAc,MAAsB;AAC/C,QAAM,QAAQ,KAAK,MAAM,gBAAgB,QAAQ,IAAI;AACrD,MAAI,CAAC,WAAW,KAAK,EAAG,MAAK,GAAG,IAAI,wBAAwB,IAAI,yCAAyC;AACzG,SAAO;AACT;AAGA,SAAS,eAAe,SAAiB,MAAc,SAAkC;AACvF,QAAM,UAAoB,CAAC;AAC3B,QAAM,OAAiB,CAAC;AACxB,UAAQ,QAAQ,CAAC,OAAO,UAAU;AAChC,QAAI,YAAY,SAAS,SAAS,KAAK,MAAM,MAAM,OAAO,IAAI,CAAC,EAAE,MAAM,GAAG,EAAE,KAAK,GAAG;AACpF,QAAI,CAAC,UAAU,WAAW,GAAG,EAAG,aAAY,KAAK,SAAS;AAE1D,QAAI,CAAC,QAAQ,OAAO,QAAQ,KAAK,EAAE,SAAS,QAAQ,SAAS,CAAC,EAAG,aAAY,UAAU,MAAM,GAAG,CAAC,QAAQ,SAAS,EAAE,MAAM;AAC1H,UAAM,QAAQ,YAAY,KAAK;AAC/B,YAAQ,KAAK,MAAM,OAAO,WAAW,YACjC,UAAU,KAAK,SAAS,KAAK,UAAU,SAAS,CAAC,MACjD,YAAY,MAAM,OAAO,MAAM,OAAO,KAAK,WAAW,KAAK,UAAU,SAAS,CAAC,GAAG;AACtF,SAAK,KAAK,KAAK,KAAK,UAAU,MAAM,EAAE,CAAC,KAAK,KAAK,GAAG;AAAA,EACtD,CAAC;AACD,SAAO,GAAG,QAAQ,KAAK,IAAI,CAAC;AAAA;AAAA;AAAA,EAAuD,KAAK,KAAK,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AACpG;AAEA,SAAS,oBAAoB,KAAa;AACxC,gBAAc,KAAK,KAAK,YAAY,GAAG,aAAa,KAAK,MAAM,WAAW,GAAG,MAAM,CAAC;AACpF,QAAM,QAAQ,KAAK,MAAM,aAAa;AACtC,MAAI,WAAW,KAAK,EAAG,eAAc,KAAK,KAAK,cAAc,GAAG,aAAa,OAAO,MAAM,CAAC;AAC3F,gBAAc,KAAK,KAAK,UAAU,GAAG,kBAAkB,KAAK,UAAU,aAAa,KAAK,MAAM,iBAAiB,GAAG,MAAM,CAAC,CAAC;AAAA,CAAK;AACjI;AAGA,SAAS,kBAAkB,MAAwB;AACjD,QAAM,QAAQ,oBAAI,IAAY;AAC9B,QAAM,OAAO,CAAC,QAAgB;AAC5B,QAAI,CAAC,WAAW,GAAG,EAAG;AACtB,eAAW,QAAQ,YAAY,GAAG,GAAG;AACnC,YAAM,OAAO,KAAK,KAAK,IAAI;AAC3B,UAAI,SAAS,IAAI,EAAE,YAAY,EAAG,MAAK,IAAI;AAAA,eAClC,KAAK,SAAS,QAAQ,GAAG;AAChC,mBAAW,SAAS,aAAa,MAAM,MAAM,EAAE,SAAS,wCAAwC,GAAG;AACjG,gBAAM,SAAS,QAAQ,QAAQ,IAAI,GAAG,MAAM,CAAC,CAAE;AAC/C,cAAI,OAAO,WAAW,IAAI,KAAK,WAAW,MAAM,EAAG,OAAM,IAAI,MAAM;AAAA,QACrE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,OAAK,KAAK,MAAM,OAAO,SAAS,CAAC;AACjC,SAAO,CAAC,GAAG,KAAK;AAClB;AAEA,IAAM,qBAA6C,EAAE,KAAK,MAAM,KAAK,OAAO,KAAK,MAAM;AAEvF,SAAS,uBAAuB,MAAc;AAC5C,QAAMA,WAAU,cAAc,KAAK,MAAM,cAAc,CAAC;AACxD,MAAI;AACF,IAAAA,SAAQ,QAAQ,eAAe;AAC/B;AAAA,EACF,QAAQ;AAAA,EAER;AACA,QAAM,eAAe,SAA8BA,SAAQ,QAAQ,oBAAoB,CAAC,EAAE;AAC1F,QAAM,QAAQ,mBAAmB,aAAa,MAAM,GAAG,EAAE,CAAC,CAAE;AAC5D,MAAI,CAAC,MAAO,MAAK,SAAS,YAAY,8CAA8C;AACpF,MAAI,MAAM,OAAO,CAAC,WAAW,aAAa,cAAc,aAAa,iBAAiB,KAAK,EAAE,CAAC;AAChG;AAEA,SAAS,WAAW,MAAc,SAA0B,KAAa;AACvE,QAAM,aAAa,CAAC,oBAAoB,mBAAmB,mBAAmB,kBAAkB,EAAE,KAAK,CAAC,MAAM,WAAW,KAAK,MAAM,CAAC,CAAC,CAAC;AACvI,MAAI,CAAC,WAAY,MAAK,4BAA4B;AAClD,yBAAuB,IAAI;AAE3B,QAAM,MAAM,KAAK,MAAM,eAAe;AACtC,QAAM,UAAU,KAAK,MAAM,+BAA+B;AAC1D,SAAO,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC5C,YAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAClC,MAAI;AACF,wBAAoB,GAAG;AACvB,kBAAc,KAAK,KAAK,aAAa,GAAG,eAAe,KAAK,MAAM,OAAO,CAAC;AAC1E,UAAM,UAAU,CAAC,QAAgB,SAC/B;AAAA,eAAiD,MAAM,MAAM,IAAI;AAAA;AACnE,kBAAc,KAAK,KAAK,YAAY,GAAG;AAAA;AAAA,EAAgF,QAAQ,OAAO,4BAA4B,CAAC,EAAE;AACrK,kBAAc,KAAK,KAAK,YAAY,GAAG;AAAA;AAAA,EAAqF,QAAQ,QAAQ,oEAAoE,CAAC,EAAE;AACnN,kBAAc,KAAK,KAAK,UAAU,GAAG;AAAA;AAAA,EAAmF,QAAQ,QAAQ,kEAAkE,CAAC,EAAE;AAC7M,kBAAc,KAAK,KAAK,WAAW,GAAG;AAAA,EAAiD,QAAQ,OAAO,sBAAsB,CAAC,EAAE;AAC/H,UAAM,SAAS,kBAAkB,IAAI,EAClC,IAAI,CAAC,SAAS,UAAU,KAAK,UAAU,SAAS,KAAK,IAAI,EAAE,MAAM,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC,GAAG,EACnF,KAAK,IAAI;AACZ,kBAAc,KAAK,KAAK,cAAc,GAAG;AAAA,EAC3C,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAcP;AACG,kBAAc,SAAS,uBAAuB,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAQlD,KAAK,UAAU,YAAY,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAoBrC;AACG,WAAO,KAAK,MAAM,MAAM,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC3D,QAAI,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,SAAS,YAAY,+BAA+B,CAAC;AAAA,EACtF,UAAE;AACA,WAAO,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC5C,WAAO,SAAS,EAAE,OAAO,KAAK,CAAC;AAAA,EACjC;AAEA,QAAM,MAAM,KAAK,KAAK,KAAK;AAC3B,YAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAClC,SAAO,KAAK,MAAM,MAAM,GAAG,KAAK,EAAE,WAAW,KAAK,CAAC;AACnD,SAAO,KAAK,MAAM,cAAc,GAAG,KAAK,KAAK,cAAc,CAAC;AAE5D,SAAO,KAAK,MAAM,cAAc,GAAG,KAAK,KAAK,cAAc,GAAG,EAAE,WAAW,MAAM,kBAAkB,KAAK,CAAC;AACzG,gBAAc,KAAK,KAAK,mBAAmB,GAAG,GAAG,KAAK,UAAU,EAAE,MAAM,QAAQ,KAAK,OAAO,OAAO,mBAAmB,CAAC,CAAC;AAAA,CAAI;AAC9H;AAEA,SAAS,UAAU,MAAc,SAA0B,KAAa;AACtE,QAAM,SAAS,CAAC,OAAO,KAAK,OAAO,KAAK,CAAC,EAAE,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,WAAW,CAAC,CAAC;AAC9F,MAAI,CAAC,OAAQ,MAAK,kDAAkD;AACpE,QAAM,aAAa,CAAC,mBAAmB,kBAAkB,kBAAkB,iBAAiB,EAAE,KAAK,CAAC,MAAM,WAAW,KAAK,MAAM,CAAC,CAAC,CAAC;AAInI,QAAM,MAAM,KAAK,QAAQ,YAAY;AACrC,QAAM,aAAa,aAAa,KAAK,MAAM,WAAW,QAAQ,eAAe,uBAAuB,CAAC,IAAI;AACzG,QAAM,cAAc,cAAc,WAAW,SAAS,KAAK,IAAI,mBAAmB;AAClF,SAAO,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC5C,YAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAClC,MAAI,cAAc,WAAY,YAAW,KAAK,MAAM,UAAU,GAAG,UAAU;AAC3E,MAAI;AACF,wBAAoB,GAAG;AACvB,kBAAc,KAAK,KAAK,aAAa,GAAG,eAAe,KAAK,MAAM,OAAO,CAAC;AAC1E,UAAM,QAAQ,CAAC,MAAc,WAAmB;AAC9C,gBAAU,KAAK,KAAK,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9C,oBAAc,KAAK,KAAK,MAAM,UAAU,GAAG;AAAA,EAA4C,MAAM,EAAE;AAAA,IACjG;AACA,UAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,CAA6I;AAC9J,UAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,CAA0K;AAC3L,UAAM,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,CAAsK;AACrL,UAAM,UAAU;AAAA;AAAA;AAAA;AAAA,CAAuG;AACvH,cAAU,KAAK,KAAK,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAClD,kBAAc,KAAK,KAAK,UAAU,UAAU,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAmBlD;AACG,UAAM,aAAa,aAAa,uBAAuB,SAAS,MAAM,UAAU,CAAC,OAAO;AACxF,kBAAc,KAAK,MAAM,WAAW,GAAG,GAAG,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAexC,KAAK,UAAU,YAAY,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAO3C;AACG,WAAO,KAAK,MAAM,OAAO,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC5D,QAAI,MAAM,IAAI,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC;AAAA,EACxC,UAAE;AACA,WAAO,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC5C,WAAO,KAAK,MAAM,WAAW,GAAG,EAAE,OAAO,KAAK,CAAC;AAC/C,QAAI,cAAc,cAAc,WAAW,UAAU,EAAG,YAAW,YAAY,KAAK,MAAM,UAAU,CAAC;AAAA,EACvG;AAEA,QAAM,aAAa,KAAK,MAAM,SAAS,YAAY;AACnD,MAAI,CAAC,WAAW,KAAK,YAAY,WAAW,CAAC,EAAG,MAAK,0CAA0C;AAC/F,QAAM,MAAM,KAAK,KAAK,KAAK;AAC3B,YAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAClC,SAAO,YAAY,KAAK,EAAE,WAAW,MAAM,kBAAkB,KAAK,CAAC;AACnE,MAAI,WAAW,KAAK,MAAM,SAAS,QAAQ,CAAC,EAAG,QAAO,KAAK,MAAM,SAAS,QAAQ,GAAG,KAAK,KAAK,SAAS,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AACtI,MAAI,WAAW,KAAK,MAAM,QAAQ,CAAC,EAAG,QAAO,KAAK,MAAM,QAAQ,GAAG,KAAK,KAAK,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAC3G,gBAAc,KAAK,KAAK,mBAAmB,GAAG,GAAG,KAAK,UAAU,EAAE,MAAM,QAAQ,KAAK,OAAO,OAAO,YAAY,CAAC,CAAC;AAAA,CAAI;AACvH;AAEO,SAAS,oBAAoB,OAA4D;AAC9F,QAAM,OAAO,QAAQ,MAAM,IAAI;AAC/B,QAAM,MAAM,QAAQ,MAAM,GAAG;AAC7B,QAAM,UAAU,iBAAiB,MAAM,SAAmB,QAAQ,MAAM,YAAY,CAAC,CAAC;AACtF,QAAM,YAAY,gBAAgB,IAAI;AAEtC,SAAO,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC5C,YAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAClC,QAAM,UAAU,SAAS,MAAM,gBAAgB;AAC/C,MAAI;AACF,QAAI,cAAc,QAAS,YAAW,MAAM,SAAS,GAAG;AAAA,QACnD,WAAU,MAAM,SAAS,GAAG;AAAA,EACnC,UAAE;AACA,YAAQ;AAAA,EACV;AACA,SAAO,EAAE,WAAW,KAAK,YAAY,QAAQ,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE;AAChE;;;ACtVA,SAAS,OAAO,aAAAC,kBAAoC;AACpD,SAAS,YAAY,mBAAmB;AACxC,SAAS,aAAa,gBAAAC,eAAc,UAAAC,SAAQ,iBAAAC,sBAAqB;AACjE,SAAS,iBAAAC,sBAAqB;AAC9B,SAAS,oBAAoB;AAC7B,SAAS,cAAc;AACvB,SAAS,WAAAC,UAAS,QAAAC,aAAY;AAG9B,IAAM,qBAAqB;AAC3B,IAAM,eAAe,GAAG,kBAAkB;AAsB1C,IAAM,oBAAN,cAAgC,MAAM;AAAA,EACpC,YAAqB,MAAc,SAAiB;AAClD,UAAM,OAAO;AADM;AAEnB,SAAK,OAAO;AAAA,EACd;AAAA,EAHqB;AAIvB;AAEA,SAAS,SAAS,MAAsB;AACtC,QAAM,QAAQ,QAAQ,IAAI,IAAI,GAAG,KAAK;AACtC,MAAI,CAAC,MAAO,OAAM,IAAI,WAAW,GAAG,IAAI,oFAAoF;AAC5H,SAAO;AACT;AAEA,IAAM,SAAS,CAAC,SAA0B,WAAW,QAAQ,EAAE,OAAO,IAAI,EAAE,OAAO,KAAK;AAExF,SAAS,UAAU,OAAwB;AACzC,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,IAAI,MAAM,IAAI,SAAS,EAAE,KAAK,GAAG,CAAC;AACnE,MAAI,SAAS,OAAO,UAAU,UAAU;AACtC,WAAO,IAAI,OAAO,KAAK,KAAgC,EAAE,KAAK,EAC3D,IAAI,CAAC,QAAQ,GAAG,KAAK,UAAU,GAAG,CAAC,IAAI,UAAW,MAAkC,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,GAAG,CAAC;AAAA,EAC3G;AACA,SAAO,KAAK,UAAU,KAAK;AAC7B;AAEA,SAAS,WAA4B;AACnC,SAAO,IAAI,QAAQ,CAACC,UAAS,WAAW;AACtC,UAAM,SAAS,aAAa;AAC5B,WAAO,KAAK,SAAS,MAAM;AAC3B,WAAO,OAAO,GAAG,aAAa,MAAM;AAClC,YAAM,UAAU,OAAO,QAAQ;AAC/B,YAAM,OAAO,OAAO,YAAY,YAAY,UAAU,QAAQ,OAAO;AACrE,aAAO,MAAM,MAAMA,SAAQ,IAAI,CAAC;AAAA,IAClC,CAAC;AAAA,EACH,CAAC;AACH;AAEA,eAAe,UAAU,KAAa,WAAqC;AACzE,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,SAAO,KAAK,IAAI,IAAI,UAAU;AAC5B,QAAI;AACF,WAAK,MAAM,MAAM,GAAG,GAAG,GAAI,QAAO;AAAA,IACpC,QAAQ;AAAA,IAER;AACA,UAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;AAAA,EAC7C;AACA,SAAO;AACT;AAGA,SAAS,gBAAgB;AACvB,QAAMC,WAAUC,eAAc,YAAY,GAAG;AAC7C,QAAM,MAAMC,MAAKC,SAAQH,SAAQ,QAAQ,yBAAyB,CAAC,GAAG,QAAQ;AAC9E,QAAM,OAAO,CAAC,KAAK,WAAW,UAAU;AACxC,MAAI,QAAQ,aAAa,WAAW,QAAQ,IAAI,GAAI,MAAK,KAAK,aAAa;AAC3E,QAAM,SAASI,WAAU,QAAQ,UAAU,MAAM,EAAE,OAAO,UAAU,CAAC;AACrE,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,kBAAkB,4CAA4C,2DAA2D;AAAA,EACrI;AACF;AAEA,eAAe,gBAAgB,KAAa,WAAuB,YAAsB;AACvF,gBAAc;AACd,QAAM,EAAE,SAAS,IAAI,MAAM,OAAO,YAAY;AAC9C,QAAM,UAAU,MAAM,SAAS,OAAO;AACtC,MAAI;AACF,QAAI,gBAAgB;AACpB,QAAI,gBAAgB;AACpB,UAAM,gBAAgB,oBAAI,IAAY;AACtC,UAAM,UAAkH,CAAC;AACzH,UAAM,WAAqB,CAAC;AAC5B,eAAW,YAAY,WAAW;AAChC,YAAM,OAAO,MAAM,QAAQ,QAAQ,EAAE,UAAU,EAAE,OAAO,SAAS,OAAO,QAAQ,SAAS,OAAO,EAAE,CAAC;AACnG,WAAK,GAAG,WAAW,CAAC,YAAY;AAC9B,YAAI,QAAQ,KAAK,MAAM,SAAS;AAC9B,2BAAiB;AACjB,mBAAS,KAAK,GAAG,SAAS,IAAI,aAAa,QAAQ,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,EAAE;AAAA,QAC3E;AAAA,MACF,CAAC;AACD,WAAK,GAAG,aAAa,CAAC,UAAU;AAC9B,yBAAiB;AACjB,iBAAS,KAAK,GAAG,SAAS,IAAI,WAAW,MAAM,QAAQ,MAAM,GAAG,GAAG,CAAC,EAAE;AAAA,MACxE,CAAC;AACD,YAAM,WAAW,MAAM,KAAK,KAAK,KAAK,EAAE,WAAW,QAAQ,SAAS,KAAO,CAAC;AAC5E,YAAM,KAAK,iBAAiB,eAAe,EAAE,SAAS,KAAO,CAAC,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAC9E,YAAM,WAAW,MAAM,KAAK,QAAQ,4BAA4B,EAAE,MAAM;AACxE,UAAI,CAAC,UAAU,GAAG,KAAK,aAAa,GAAG;AACrC,yBAAiB;AACjB,iBAAS,KAAK,GAAG,SAAS,IAAI,6CAA6C,UAAU,OAAO,KAAK,MAAM,GAAG;AAAA,MAC5G;AASA,YAAM,UAAU,MAAM,KAAK,SAAS,CAAC,UAAoB;AACvD,cAAM,aAAa,oBAAI,IAAY;AACnC,cAAM,OAAO,CAAC,SAAiB;AAC7B,qBAAW,SAAS,KAAK,SAAS,6BAA6B,EAAG,YAAW,IAAI,MAAM,CAAC,CAAE;AAAA,QAC5F;AACA,mBAAW,SAAS,MAAM,KAAK,SAAS,WAAW,GAAG;AACpD,cAAI;AACF,uBAAW,QAAQ,MAAM,KAAK,MAAM,QAAQ,EAAG,MAAK,KAAK,OAAO;AAAA,UAClE,QAAQ;AAAA,UAER;AAAA,QACF;AACA,iBAAS,iBAAiB,SAAS,EAAE,QAAQ,CAAC,YAAY,KAAK,QAAQ,aAAa,OAAO,KAAK,EAAE,CAAC;AACnG,cAAM,QAAQ,iBAAiB,SAAS,eAAe;AACvD,eAAO,MAAM,OAAO,CAAC,SAAS,WAAW,IAAI,IAAI,KAAK,CAAC,MAAM,iBAAiB,IAAI,EAAE,KAAK,CAAC;AAAA,MAC5F,GAAG,UAAU;AACb,iBAAW,SAAS,QAAS,eAAc,IAAI,KAAK;AACpD,YAAM,MAAM,MAAM,KAAK,WAAW,EAAE,UAAU,KAAK,CAAC;AACpD,cAAQ,KAAK;AAAA,QACX,MAAM,SAAS;AAAA,QACf,OAAO,SAAS;AAAA,QAChB,QAAQ,SAAS;AAAA;AAAA;AAAA,QAGjB,QAAQ;AAAA,QACR,iBAAiB,UAAU,OAAO,GAAG,CAAC;AAAA,MACxC,CAAC;AACD,YAAM,KAAK,MAAM;AAAA,IACnB;AACA,WAAO,EAAE,SAAS,eAAe,eAAe,eAAe,CAAC,GAAG,aAAa,EAAE,KAAK,GAAG,SAAS;AAAA,EACrG,UAAE;AACA,UAAM,QAAQ,MAAM;AAAA,EACtB;AACF;AAEA,eAAsB,oBAAoB;AACxC,QAAM,SAAS,SAAS,cAAc;AACtC,QAAM,SAAS,SAAS,cAAc;AACtC,QAAM,YAAY,SAAS,iBAAiB;AAC5C,QAAM,YAAY,SAAS,iBAAiB;AAC5C,QAAM,cAAc,SAAS,mBAAmB;AAChD,QAAM,YAAY,SAAS,iBAAiB;AAC5C,QAAM,YAAY,SAAS,iBAAiB;AAC5C,QAAM,gBAAgB,SAAS,qBAAqB;AACpD,QAAM,kBAAkB,KAAK,MAAM,SAAS,uBAAuB,CAAC;AAEpE,QAAM,MAAM,OAAU,MAAc,OAA4D,CAAC,MAAkB;AACjH,UAAM,WAAW,MAAM,MAAM,IAAI,IAAI,MAAM,MAAM,GAAG;AAAA,MAClD,QAAQ,KAAK,UAAU;AAAA,MACvB,SAAS;AAAA,QACP,eAAe,UAAU,MAAM;AAAA,QAC/B,gBAAgB;AAAA,QAChB,GAAI,KAAK,QAAQ,EAAE,0BAA0B,KAAK,MAAM,IAAI,CAAC;AAAA,MAC/D;AAAA,MACA,GAAI,KAAK,SAAS,SAAY,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,KAAK,IAAI,EAAE;AAAA,IACvE,CAAC;AACD,UAAM,OAAO,MAAM,SAAS,KAAK;AACjC,QAAI,SAAwE;AAC5E,QAAI;AACF,eAAS,KAAK,MAAM,IAAI;AAAA,IAC1B,QAAQ;AACN,eAAS;AAAA,IACX;AACA,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,OAAO,OAAO,QAAQ,UAAU,YAAY,oBAAoB,KAAK,OAAO,KAAK,IAAI,OAAO,QAAQ,YAAY,SAAS,MAAM;AACrI,YAAM,SAAS,OAAO,QAAQ,YAAY,WAAW,OAAO,UAAU,OAAO,QAAQ,UAAU,WAAW,OAAO,QAAQ,KAAK,MAAM,GAAG,GAAG;AAC1I,YAAM,IAAI,kBAAkB,MAAM,GAAG,KAAK,UAAU,KAAK,IAAI,IAAI,aAAa,SAAS,MAAM,KAAK,MAAM,EAAE;AAAA,IAC5G;AACA,YAAS,UAAU,UAAU,SAAS,OAAO,OAAO,WAAW,CAAC;AAAA,EAClE;AAEA,QAAM,YAAY,oBAAoB,SAAS,qDAAqD,SAAS;AAC7G,MAAI,QAAoE;AAOxE,QAAM,eAAe,YAAY;AAC/B,YAAQ,MAAM,IAAyD,GAAG,SAAS,UAAU;AAAA,MAC3F,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ;AAAA,QACA;AAAA,QACA,SAAS,EAAE,UAAU,6BAA6B,MAAM,iBAAiB,MAAM,cAAc,WAAW,eAAe,gBAAgB;AAAA,QACvI,eAAe,SAAS,aAAa;AAAA,QACrC,oBAAoB,OAAO,QAAQ,IAAI,gBAAgB,KAAK;AAAA,QAC5D,gBAAgB,SAAS,cAAc;AAAA,QACvC,aAAa,SAAS,mBAAmB;AAAA;AAAA,QAEzC,qBAAqB,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,MAAS,GAAK,EAAE,YAAY;AAAA,MAC9E;AAAA,IACF,CAAC;AACD,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,YAAYF,MAAK,OAAO,GAAG,gBAAgB,CAAC;AACzD,MAAI,UAA+B;AACnC,MAAI;AACF,UAAM,WAAW,MAAM,IAAc,oBAAoB,SAAS,6BAA6B;AAC/F,UAAM,QAAQ,SAAS,WAAW,KAAK,CAAC,MAAM,EAAE,OAAO,WAAW;AAClE,QAAI,CAAC,OAAO;AACV,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,eAAeA,MAAK,MAAM,eAAe;AAC/C,IAAAG,eAAc,cAAc,KAAK,UAAU,EAAE,YAAY,SAAS,WAAW,IAAI,CAAC,EAAE,IAAAC,KAAI,OAAO,OAAO,EAAE,IAAAA,KAAI,OAAO,EAAE,EAAE,CAAC,CAAC;AACzH,UAAM,MAAMJ,MAAK,MAAM,SAAS;AAChC,QAAI;AACF,0BAAoB,EAAE,MAAM,QAAQ,IAAI,GAAG,cAAc,IAAI,CAAC;AAAA,IAChE,SAAS,OAAO;AACd,YAAM,IAAI,kBAAkB,kCAAkC,6BAA8B,MAAgB,OAAO,EAAE;AAAA,IACvH;AAEA,UAAM,kBAAkB,KAAK,MAAMK,cAAaL,MAAK,KAAK,mBAAmB,GAAG,MAAM,CAAC;AACvF,UAAM,OAAO,MAAM,SAAS;AAC5B,UAAM,eAAe,YAAY,EAAE,EAAE,SAAS,WAAW;AACzD,UAAM,QAAQ,oBAAoB,IAAI;AAEtC,UAAM,EAAE,cAAc,WAAW,GAAG,UAAU,IAAI,QAAQ;AAC1D,cAAU,MAAM,QAAQ,UAAU,CAACA,MAAK,KAAK,gBAAgB,KAAK,gBAAgB,KAAK,CAAC,GAAG;AAAA,MACzF,KAAKA,MAAK,KAAK,gBAAgB,GAAG;AAAA,MAClC,KAAK;AAAA,QACH,GAAG;AAAA,QACH,UAAU;AAAA,QACV,MAAM,OAAO,IAAI;AAAA,QACjB,MAAM;AAAA,QACN,UAAU;AAAA,QACV,cAAc;AAAA,QACd,uBAAuB;AAAA,QACvB,qBAAqB;AAAA,QACrB,4BAA4B;AAAA,MAC9B;AAAA,MACA,OAAO,CAAC,UAAU,WAAW,SAAS;AAAA,IACxC,CAAC;AACD,QAAI,CAAE,MAAM,UAAU,GAAG,KAAK,GAAG,kBAAkB,WAAW,GAAM,GAAI;AACtE,YAAM,IAAI,kBAAkB,2CAA2C,sDAAsD;AAAA,IAC/H;AAEA,UAAM,SAAS,MAAM,MAAM,GAAG,KAAK,GAAG,kBAAkB,UAAU;AAAA,MAChE,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oBAAoB,wBAAwB,aAAa;AAAA,MACpF,MAAM,KAAK,UAAU,EAAE,aAAa,OAAO,MAAM,aAAa,CAAC;AAAA,IACjE,CAAC;AACD,QAAI,CAAC,OAAO,IAAI;AACd,YAAM,IAAI,kBAAkB,qCAAqC,mDAAmD,OAAO,MAAM,IAAI;AAAA,IACvI;AACA,UAAM,EAAE,GAAG,IAAK,MAAM,OAAO,KAAK;AAGlC,UAAM,SAAS,MAAM;AAAA,MACnB,GAAG,KAAK,GAAG,kBAAkB,cAAc,mBAAmB,EAAE,CAAC;AAAA,MACjE;AAAA,MACA,SAAS;AAAA,IACX;AAEA,UAAM,UAAUA,MAAK,MAAM,YAAY;AACvC,QAAIE,WAAU,OAAO,CAAC,QAAQ,SAAS,MAAM,KAAK,GAAG,GAAG,EAAE,OAAO,UAAU,CAAC,EAAE,WAAW,GAAG;AAC1F,YAAM,IAAI,kBAAkB,wCAAwC,4CAA4C;AAAA,IAClH;AACA,UAAM,QAAQG,cAAa,OAAO;AAClC,YAAQ,KAAK;AACb,cAAU;AAEV,UAAM,UAAU,MAAM,aAAa,GAAG;AACtC,UAAM,SAAS;AAAA,MACb,UAAU;AAAA,QACR,QAAQ,OAAO,cAAc,WAAW,IAAI,WAAW;AAAA,QACvD,cAAc,OAAO;AAAA,QACrB,eAAe,OAAO;AAAA,MACxB;AAAA,MACA,SAAS;AAAA,QACP,QAAQ,OAAO,kBAAkB,KAAK,OAAO,kBAAkB,IAAI,WAAW;AAAA,QAC9E,eAAe,OAAO;AAAA,QACtB,eAAe,OAAO;AAAA,MACxB;AAAA,MACA,QAAQ,EAAE,QAAQ,oBAAoB,gBAAgB,MAAM,WAAW,OAAO,QAAQ;AAAA,IACxF;AAGA,UAAM,SAAS,MAAM,IAA8C,oBAAoB,SAAS,yBAAyB,EAAE,QAAQ,OAAO,CAAC;AAC3I,UAAM,MAAM,MAAM,MAAM,OAAO,WAAW,EAAE,QAAQ,OAAO,MAAM,OAAO,SAAS,EAAE,gBAAgB,mBAAmB,EAAE,CAAC;AACzH,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI,kBAAkB,0CAA0C,4CAA4C,IAAI,MAAM,IAAI;AAAA,IAClI;AACA,UAAM,IAAI,oBAAoB,SAAS,8BAA8B;AAAA,MACnE,QAAQ;AAAA,MACR,MAAM,EAAE,WAAW,OAAO,WAAW,WAAW,OAAO,WAAW,UAAU,UAAU,OAAO,KAAK,CAAC,IAAI,WAAW,MAAM,WAAW;AAAA,IACrI,CAAC;AAED,UAAM,iBAAiB,UAAU,OAAO,UAAU,EAAE,WAAW,WAAW,OAAO,WAAW,OAAO,CAAC,CAAC,CAAC;AACtG,UAAM,IAAI,GAAG,SAAS,aAAa;AAAA,MACjC,QAAQ;AAAA,MACR,OAAO,MAAO;AAAA,MACd,MAAM;AAAA,QACJ,aAAa,OAAO;AAAA,QACpB,aAAa,OAAO;AAAA,QACpB,kBAAkB,OAAO;AAAA,QACzB,WAAW,OAAO;AAAA,QAClB,oBAAoB,OAAO;AAAA,QAC3B,YAAY,OAAO;AAAA,QACnB,mBAAmB,OAAO;AAAA,QAC1B,kBAAkB,OAAO;AAAA,QACzB,WAAW,OAAO;AAAA,QAClB,aAAa,OAAO;AAAA,QACpB,oBAAoB,OAAO;AAAA,QAC3B,iBAAiB,OAAO;AAAA,QACxB;AAAA,QACA;AAAA,MACF;AAAA,IACF,CAAC;AAED,UAAM,SAAS,OAAO,SAAS,WAAW,YAAY,OAAO,QAAQ,WAAW;AAChF,YAAQ,IAAI,4BAA4B,SAAS,WAAW,QAAQ,QAAQ,WAAW,EAAE;AACzF,QAAI,CAAC,QAAQ;AACX,UAAI,OAAO,cAAc,OAAQ,SAAQ,IAAI,wCAAwC,OAAO,cAAc,KAAK,IAAI,CAAC,EAAE;AACtH,iBAAW,WAAW,OAAO,SAAU,SAAQ,IAAI,KAAK,OAAO,EAAE;AAAA,IACnE;AAAA,EACF,SAAS,OAAO;AACd,UAAM,OAAO,iBAAiB,oBAAoB,MAAM,OAAO;AAC/D,UAAM,UAAW,MAAgB,QAAQ,MAAM,GAAG,GAAI;AAGtD,UAAM,WAAW,SAAS,MAAM,aAAa,EAAE,MAAM,CAAC,eAAe;AACnE,cAAQ,MAAM,oEAAqE,WAAqB,OAAO,EAAE;AACjH,aAAO;AAAA,IACT,CAAC;AACD,QAAI,UAAU;AACZ,YAAM,IAAI,GAAG,SAAS,SAAS;AAAA,QAC7B,QAAQ;AAAA,QACR,OAAO,SAAS;AAAA,QAChB,MAAM,EAAE,aAAa,WAAW,MAAM,cAAc,QAAQ;AAAA,MAC9D,CAAC,EAAE,MAAM,CAAC,gBAAgB,QAAQ,MAAM,+CAAgD,YAAsB,OAAO,EAAE,CAAC;AAAA,IAC1H;AACA,UAAM,IAAI,WAAW,GAAG,IAAI,KAAK,OAAO,EAAE;AAAA,EAC5C,UAAE;AACA,aAAS,KAAK;AACd,IAAAC,QAAO,MAAM,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EAC/C;AACF;;;ACzXA,SAAS,IAAI,MAAkC;AAC7C,QAAM,QAAQ,QAAQ,KAAK,QAAQ,KAAK,IAAI,EAAE;AAC9C,SAAO,UAAU,KAAK,SAAY,QAAQ,KAAK,QAAQ,CAAC;AAC1D;AAEA,eAAe,OAAO;AACpB,QAAM,UAAU,QAAQ,KAAK,CAAC;AAC9B,MAAI,YAAY,SAAS;AACvB,UAAM,eAAe,IAAI,UAAU;AACnC,UAAM,MAAM,IAAI,KAAK;AACrB,QAAI,CAAC,gBAAgB,CAAC,IAAK,OAAM,IAAI,WAAW,uEAAuE;AACvH,YAAQ,IAAI,KAAK,UAAU,oBAAoB,EAAE,MAAM,IAAI,KAAK,KAAK,QAAQ,IAAI,GAAG,cAAc,IAAI,CAAC,CAAC,CAAC;AACzG;AAAA,EACF;AACA,MAAI,YAAY,YAAY;AAC1B,UAAM,kBAAkB;AACxB;AAAA,EACF;AACA,QAAM,IAAI,WAAW,sCAAsC;AAC7D;AAMA,KAAK,EAAE,MAAM,CAAC,UAAU;AACtB,UAAQ,MAAM,iBAAiB,iBAAiB,aAAa,MAAM,UAAW,MAAgB,SAAS,OAAO,KAAK,CAAC,EAAE;AACtH,UAAQ,WAAW;AACrB,CAAC;","names":["require","spawnSync","readFileSync","rmSync","writeFileSync","createRequire","dirname","join","resolve","require","createRequire","join","dirname","spawnSync","writeFileSync","id","readFileSync","rmSync"]}
@@ -0,0 +1,45 @@
1
+ declare const PREVIEW_BASE = "/__bettercms/component-preview";
2
+ declare const PREVIEW_ROUTES: {
3
+ readonly runtime: "/__bettercms/component-preview/__bcms/runtime";
4
+ readonly session: "/__bettercms/component-preview/__bcms/session";
5
+ readonly props: "/__bettercms/component-preview/__bcms/props";
6
+ readonly render: "/__bettercms/component-preview/__bcms/render";
7
+ readonly health: "/__bettercms/component-preview/__bcms/health";
8
+ };
9
+ /** What the declared adapter path is. The one route the dashboard ever frames. */
10
+ declare const PREVIEW_RUNTIME_PATH: "/__bettercms/component-preview/__bcms/runtime";
11
+ type PreviewRuntimeEnv = {
12
+ jwksUrl: string;
13
+ dashboardOrigin: string;
14
+ /** The origin the platform serves this runtime from. Null only where no proxy sits in front. */
15
+ previewOrigin: string | null;
16
+ /** Set only in CI, so the validator can render without a dashboard session. Never in a container. */
17
+ validatorKey: string | null;
18
+ };
19
+ /**
20
+ * Headers for the generated render page.
21
+ *
22
+ * 🔴 `frame-ancestors 'self'` ALONE BLOCKS THE RENDER. The directive is checked against EVERY ancestor,
23
+ * not only the direct parent: the render frame sits inside the runtime page (same origin) inside the
24
+ * dashboard. With only 'self', the browser refused the frame, painted its error page, and still fired
25
+ * `load` — so the component never appeared while the channel reported ready. The dashboard's origin is
26
+ * runtime configuration, which is why this is a function and not a constant.
27
+ */
28
+ declare function renderHeaders(): Record<string, string>;
29
+ /** The attribute a render page carries, so the runtime page can tell a real render from an error page. */
30
+ declare const RENDER_MARKER = "data-bcms-preview-render";
31
+ declare function previewRuntimeEnv(): PreviewRuntimeEnv | {
32
+ error: string;
33
+ };
34
+ type RegistryCheck = (componentId: string) => boolean;
35
+ declare function handleSession(request: Request, has: RegistryCheck): Promise<Response>;
36
+ declare function handleProps(request: Request, has: RegistryCheck): Promise<Response>;
37
+ /** For the generated render page. Null for an unknown or expired id — the page answers 404. */
38
+ declare function renderEntry(id: string | null | undefined): {
39
+ componentId: string;
40
+ props: Record<string, unknown>;
41
+ } | null;
42
+ declare function handleHealth(): Response;
43
+ declare function handleRuntime(shellSource: string): Response;
44
+
45
+ export { PREVIEW_BASE, PREVIEW_ROUTES, PREVIEW_RUNTIME_PATH, type PreviewRuntimeEnv, RENDER_MARKER, type RegistryCheck, handleHealth, handleProps, handleRuntime, handleSession, previewRuntimeEnv, renderEntry, renderHeaders };
package/dist/server.js ADDED
@@ -0,0 +1,367 @@
1
+ // ../component-output/dist/index.js
2
+ var COMPONENT_OUTPUT_TOKEN_PREFIX = "v2";
3
+ var COMPONENT_OUTPUT_SIGNED_VERSION = "component-output.v2";
4
+ var TOKEN_SEGMENTS = 4;
5
+ var keyCache = /* @__PURE__ */ new Map();
6
+ var missCache = /* @__PURE__ */ new Map();
7
+ var MISS_TTL_MS = 6e4;
8
+ var KEY_TTL_MS = 36e5;
9
+ async function importJwk(jwk) {
10
+ if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || typeof jwk.x !== "string") return null;
11
+ try {
12
+ return await crypto.subtle.importKey(
13
+ "jwk",
14
+ { kty: jwk.kty, crv: jwk.crv, x: jwk.x },
15
+ { name: "Ed25519" },
16
+ false,
17
+ ["verify"]
18
+ );
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ async function keyFor(jwksUrl, kid, fetchImpl, now) {
24
+ const entry = keyCache.get(jwksUrl);
25
+ if (entry && now - entry.fetchedAt < KEY_TTL_MS) {
26
+ const cached = entry.keys.get(kid);
27
+ if (cached) return cached;
28
+ }
29
+ const missedAt = missCache.get(`${jwksUrl}#${kid}`);
30
+ if (missedAt !== void 0 && now - missedAt < MISS_TTL_MS) return null;
31
+ let document;
32
+ try {
33
+ const response = await fetchImpl(jwksUrl, { headers: { accept: "application/json" } });
34
+ if (!response.ok) return "unavailable";
35
+ document = await response.json();
36
+ } catch {
37
+ return "unavailable";
38
+ }
39
+ if (!Array.isArray(document.keys)) return "unavailable";
40
+ const imported = /* @__PURE__ */ new Map();
41
+ for (const jwk of document.keys) {
42
+ const key = await importJwk(jwk);
43
+ if (key && typeof jwk.kid === "string") imported.set(jwk.kid, key);
44
+ }
45
+ keyCache.set(jwksUrl, { keys: imported, fetchedAt: now });
46
+ const found = imported.get(kid) ?? null;
47
+ if (!found) missCache.set(`${jwksUrl}#${kid}`, now);
48
+ return found;
49
+ }
50
+ function bufferSource(bytes) {
51
+ const copy = new ArrayBuffer(bytes.byteLength);
52
+ new Uint8Array(copy).set(bytes);
53
+ return copy;
54
+ }
55
+ function base64urlToBytes(value) {
56
+ if (!/^[A-Za-z0-9_-]*$/.test(value)) return null;
57
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - value.length % 4) % 4);
58
+ try {
59
+ const binary = atob(padded);
60
+ const bytes = new Uint8Array(binary.length);
61
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
62
+ return bytes;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+ function nonEmptyString(value) {
68
+ return typeof value === "string" && value.length > 0;
69
+ }
70
+ var EVIDENCE_KEYS = [
71
+ "candidateId",
72
+ "evidenceId",
73
+ "familyManifestId",
74
+ "familyManifestHash",
75
+ "variantBindingHash",
76
+ "evidenceDigest",
77
+ "evidenceTupleHash",
78
+ "commitSha"
79
+ ];
80
+ function claimsAreWellFormed(value) {
81
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
82
+ const claims = value;
83
+ if (claims.protocol !== "bcms-component-runtime-v1") return false;
84
+ for (const key of [
85
+ "sessionId",
86
+ "nonce",
87
+ "projectId",
88
+ "componentId",
89
+ "schemaHash",
90
+ "brandContractHash",
91
+ "familyKey",
92
+ "familyContractHash",
93
+ "targetBindingDigest",
94
+ "adapterHash",
95
+ "previewOrigin",
96
+ "expiresAt"
97
+ ]) {
98
+ if (!nonEmptyString(claims[key])) return false;
99
+ }
100
+ if (!Array.isArray(claims.nativeViewports)) return false;
101
+ if (claims.claim === "preview") {
102
+ return EVIDENCE_KEYS.every((key) => !(key in claims));
103
+ }
104
+ if (claims.claim === "validated") {
105
+ return EVIDENCE_KEYS.every((key) => nonEmptyString(claims[key]));
106
+ }
107
+ return false;
108
+ }
109
+ function componentRefusalCause(reason) {
110
+ return reason === "JWKS_UNAVAILABLE" ? "keys-unreachable" : "session-refused";
111
+ }
112
+ async function verifyComponentSession(input) {
113
+ if (!nonEmptyString(input.token)) return { ok: false, reason: "TOKEN_MISSING" };
114
+ const parts = input.token.split(".");
115
+ if (parts.length !== TOKEN_SEGMENTS || parts[0] !== COMPONENT_OUTPUT_TOKEN_PREFIX) {
116
+ return { ok: false, reason: "TOKEN_MALFORMED" };
117
+ }
118
+ const [, kid, body, signature] = parts;
119
+ if (!nonEmptyString(kid) || !nonEmptyString(body) || !nonEmptyString(signature)) {
120
+ return { ok: false, reason: "TOKEN_MALFORMED" };
121
+ }
122
+ const signatureBytes = base64urlToBytes(signature);
123
+ const bodyBytes = base64urlToBytes(body);
124
+ if (!signatureBytes || !bodyBytes) return { ok: false, reason: "TOKEN_MALFORMED" };
125
+ const now = input.now ?? Date.now();
126
+ const key = await keyFor(input.jwksUrl, kid, input.fetchImpl ?? fetch, now);
127
+ if (key === "unavailable") return { ok: false, reason: "JWKS_UNAVAILABLE" };
128
+ if (!key) return { ok: false, reason: "KEY_NOT_FOUND" };
129
+ const message = new TextEncoder().encode(`${COMPONENT_OUTPUT_SIGNED_VERSION}.${kid}.${body}`);
130
+ const valid = await crypto.subtle.verify(
131
+ { name: "Ed25519" },
132
+ key,
133
+ bufferSource(signatureBytes),
134
+ bufferSource(message)
135
+ );
136
+ if (!valid) return { ok: false, reason: "SIGNATURE_INVALID" };
137
+ let decoded;
138
+ try {
139
+ decoded = JSON.parse(new TextDecoder().decode(bodyBytes));
140
+ } catch {
141
+ return { ok: false, reason: "CLAIMS_MALFORMED" };
142
+ }
143
+ if (!decoded || typeof decoded !== "object") return { ok: false, reason: "CLAIMS_MALFORMED" };
144
+ const claim = decoded.claim;
145
+ if (claim !== "preview" && claim !== "validated") return { ok: false, reason: "CLAIM_UNKNOWN" };
146
+ if (!claimsAreWellFormed(decoded)) return { ok: false, reason: "CLAIMS_MALFORMED" };
147
+ const claims = decoded;
148
+ const expiry = Date.parse(claims.expiresAt);
149
+ if (!Number.isFinite(expiry) || expiry <= now) {
150
+ return { ok: false, reason: "SESSION_EXPIRED" };
151
+ }
152
+ if (claims.componentId !== input.componentId) return { ok: false, reason: "COMPONENT_MISMATCH" };
153
+ if (claims.previewOrigin !== input.expectedOrigin) return { ok: false, reason: "ORIGIN_MISMATCH" };
154
+ return { ok: true, claims };
155
+ }
156
+
157
+ // src/server.ts
158
+ var PREVIEW_BASE = "/__bettercms/component-preview";
159
+ var PREVIEW_ROUTES = {
160
+ runtime: `${PREVIEW_BASE}/__bcms/runtime`,
161
+ session: `${PREVIEW_BASE}/__bcms/session`,
162
+ props: `${PREVIEW_BASE}/__bcms/props`,
163
+ render: `${PREVIEW_BASE}/__bcms/render`,
164
+ health: `${PREVIEW_BASE}/__bcms/health`
165
+ };
166
+ var PREVIEW_RUNTIME_PATH = PREVIEW_ROUTES.runtime;
167
+ var MAX_ENTRIES = 256;
168
+ var RENDER_TTL_MS = 10 * 6e4;
169
+ var MAX_PROPS_BYTES = 1e6;
170
+ var store = globalThis.__bcmsPreviewRuntime ??= { sessions: /* @__PURE__ */ new Map(), renders: /* @__PURE__ */ new Map() };
171
+ var BASE_HEADERS = {
172
+ "cache-control": "no-store",
173
+ // The session token rides in the runtime URL's query. No request this runtime makes may carry it
174
+ // onward in a Referer.
175
+ "referrer-policy": "no-referrer",
176
+ "x-robots-tag": "noindex, nofollow",
177
+ "x-content-type-options": "nosniff"
178
+ };
179
+ function renderHeaders() {
180
+ const config = previewRuntimeEnv();
181
+ const ancestors = "error" in config ? "'self'" : `'self' ${config.dashboardOrigin}`;
182
+ return { ...BASE_HEADERS, "content-security-policy": `frame-ancestors ${ancestors}` };
183
+ }
184
+ var RENDER_MARKER = "data-bcms-preview-render";
185
+ function env(name) {
186
+ const value = globalThis.process?.env?.[name];
187
+ return value && value.trim() ? value.trim() : null;
188
+ }
189
+ function previewRuntimeEnv() {
190
+ const apiUrl = env("BCMS_API_URL");
191
+ const dashboardOrigin = env("BCMS_DASHBOARD_ORIGIN");
192
+ if (!apiUrl) return { error: "BCMS_API_URL is not set" };
193
+ if (!dashboardOrigin) return { error: "BCMS_DASHBOARD_ORIGIN is not set" };
194
+ return {
195
+ jwksUrl: new URL("/.well-known/bcms-component-output.json", apiUrl).toString(),
196
+ dashboardOrigin: new URL(dashboardOrigin).origin,
197
+ previewOrigin: env("BCMS_PREVIEW_ORIGIN") ? new URL(env("BCMS_PREVIEW_ORIGIN")).origin : null,
198
+ validatorKey: env("BCMS_PREVIEW_VALIDATOR_KEY")
199
+ };
200
+ }
201
+ function json(body, status = 200, extra = {}) {
202
+ return new Response(JSON.stringify(body), {
203
+ status,
204
+ headers: { ...BASE_HEADERS, "content-type": "application/json; charset=utf-8", ...extra }
205
+ });
206
+ }
207
+ function randomKey() {
208
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
209
+ let binary = "";
210
+ for (const byte of bytes) binary += String.fromCharCode(byte);
211
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
212
+ }
213
+ function prune(map, now) {
214
+ for (const [key, entry] of map) if (entry.expiresAt <= now) map.delete(key);
215
+ while (map.size > MAX_ENTRIES) map.delete(map.keys().next().value);
216
+ }
217
+ function requestOrigin(request) {
218
+ const url = new URL(request.url);
219
+ const proto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim() || url.protocol.slice(0, -1);
220
+ const host = request.headers.get("x-forwarded-host")?.split(",")[0]?.trim() || request.headers.get("host") || url.host;
221
+ return `${proto}://${host}`;
222
+ }
223
+ function tokenComponentId(token) {
224
+ const body = token.split(".")[2];
225
+ if (!body) return null;
226
+ try {
227
+ const padded = body.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(body.length / 4) * 4, "=");
228
+ const decoded = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(padded), (c) => c.charCodeAt(0))));
229
+ return typeof decoded?.componentId === "string" ? decoded.componentId : null;
230
+ } catch {
231
+ return null;
232
+ }
233
+ }
234
+ function sameSecret(a, b) {
235
+ if (a.length !== b.length) return false;
236
+ let diff = 0;
237
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
238
+ return diff === 0;
239
+ }
240
+ async function handleSession(request, has) {
241
+ if (request.method !== "POST") return json({ error: "METHOD_NOT_ALLOWED" }, 405);
242
+ const config = previewRuntimeEnv();
243
+ if ("error" in config) {
244
+ console.error(`[bcms-preview] runtime misconfigured: ${config.error}`);
245
+ return json({ ok: false, cause: "session-refused" }, 503);
246
+ }
247
+ let token;
248
+ try {
249
+ token = (await request.json()).token;
250
+ } catch {
251
+ return json({ ok: false, cause: "session-refused" }, 400);
252
+ }
253
+ if (typeof token !== "string" || token.length > 16384) return json({ ok: false, cause: "session-refused" }, 400);
254
+ const componentId = tokenComponentId(token);
255
+ if (!componentId) return json({ ok: false, cause: "session-refused" }, 400);
256
+ const result = await verifyComponentSession({
257
+ token,
258
+ jwksUrl: config.jwksUrl,
259
+ componentId,
260
+ expectedOrigin: config.previewOrigin ?? requestOrigin(request)
261
+ });
262
+ if (!result.ok) {
263
+ console.warn(`[bcms-preview] session refused: ${result.reason} (component ${componentId})`);
264
+ return json({ ok: false, cause: componentRefusalCause(result.reason) }, 403);
265
+ }
266
+ if (!has(result.claims.componentId)) {
267
+ console.warn(`[bcms-preview] session refused: component ${componentId} is not in this preview build`);
268
+ return json({ ok: false, cause: "session-refused" }, 404);
269
+ }
270
+ const now = Date.now();
271
+ prune(store.sessions, now);
272
+ const renderKey = randomKey();
273
+ store.sessions.set(renderKey, { componentId, expiresAt: Date.parse(result.claims.expiresAt) });
274
+ return json({ ok: true, claims: result.claims, renderKey });
275
+ }
276
+ async function handleProps(request, has) {
277
+ if (request.method !== "POST") return json({ error: "METHOD_NOT_ALLOWED" }, 405);
278
+ const raw = await request.text();
279
+ if (raw.length > MAX_PROPS_BYTES) return json({ error: "PROPS_TOO_LARGE" }, 413);
280
+ let body;
281
+ try {
282
+ body = JSON.parse(raw);
283
+ } catch {
284
+ return json({ error: "PROPS_INVALID" }, 400);
285
+ }
286
+ if (!body.props || typeof body.props !== "object" || Array.isArray(body.props)) {
287
+ return json({ error: "PROPS_INVALID" }, 400);
288
+ }
289
+ const now = Date.now();
290
+ let componentId = null;
291
+ const validatorHeader = request.headers.get("x-bcms-validator-key");
292
+ if (validatorHeader) {
293
+ const config = previewRuntimeEnv();
294
+ if ("error" in config || !config.validatorKey || !sameSecret(validatorHeader, config.validatorKey)) {
295
+ return json({ error: "VALIDATOR_KEY_INVALID" }, 403);
296
+ }
297
+ componentId = typeof body.componentId === "string" ? body.componentId : null;
298
+ } else if (typeof body.renderKey === "string") {
299
+ const session = store.sessions.get(body.renderKey);
300
+ if (session && session.expiresAt > now) componentId = session.componentId;
301
+ }
302
+ if (!componentId) return json({ error: "SESSION_REQUIRED" }, 403);
303
+ if (!has(componentId)) return json({ error: "COMPONENT_NOT_REGISTERED" }, 404);
304
+ prune(store.renders, now);
305
+ const id = randomKey();
306
+ store.renders.set(id, { componentId, props: body.props, expiresAt: now + RENDER_TTL_MS });
307
+ return json({ id });
308
+ }
309
+ function renderEntry(id) {
310
+ if (!id) return null;
311
+ const entry = store.renders.get(id);
312
+ if (!entry || entry.expiresAt <= Date.now()) return null;
313
+ return { componentId: entry.componentId, props: entry.props };
314
+ }
315
+ function handleHealth() {
316
+ return json({ ok: true });
317
+ }
318
+ var escapeForScript = (value) => value.replace(/<\/script/gi, "<\\/script").replace(/<!--/g, "<\\!--");
319
+ function handleRuntime(shellSource) {
320
+ const config = previewRuntimeEnv();
321
+ if ("error" in config) {
322
+ console.error(`[bcms-preview] runtime misconfigured: ${config.error}`);
323
+ return new Response("Preview runtime is not configured.", { status: 503, headers: BASE_HEADERS });
324
+ }
325
+ const pageConfig = JSON.stringify({ dashboardOrigin: config.dashboardOrigin, routes: PREVIEW_ROUTES }).replace(/</g, "\\u003c");
326
+ const html = `<!doctype html>
327
+ <html lang="en">
328
+ <head>
329
+ <meta charset="utf-8">
330
+ <meta name="viewport" content="width=device-width, initial-scale=1">
331
+ <meta name="referrer" content="no-referrer">
332
+ <title>Component preview</title>
333
+ <style>
334
+ html,body{margin:0;height:100%;background:transparent}
335
+ #bcms-frame{display:block;border:0;width:100%;height:100%}
336
+ #bcms-status{margin:0;padding:16px;font:13px/1.5 system-ui,sans-serif;color:#555}
337
+ </style>
338
+ </head>
339
+ <body>
340
+ <p id="bcms-status" role="status">Connecting to BetterCMS\u2026</p>
341
+ <iframe id="bcms-frame" title="Component" hidden></iframe>
342
+ <script type="application/json" id="bcms-preview-config">${pageConfig}</script>
343
+ <script>${escapeForScript(shellSource)}</script>
344
+ </body>
345
+ </html>`;
346
+ return new Response(html, {
347
+ status: 200,
348
+ headers: {
349
+ ...BASE_HEADERS,
350
+ "content-type": "text/html; charset=utf-8",
351
+ "content-security-policy": `frame-ancestors ${config.dashboardOrigin}`
352
+ }
353
+ });
354
+ }
355
+ export {
356
+ PREVIEW_BASE,
357
+ PREVIEW_ROUTES,
358
+ PREVIEW_RUNTIME_PATH,
359
+ RENDER_MARKER,
360
+ handleHealth,
361
+ handleProps,
362
+ handleRuntime,
363
+ handleSession,
364
+ previewRuntimeEnv,
365
+ renderEntry,
366
+ renderHeaders
367
+ };
@@ -0,0 +1 @@
1
+ "use strict";(()=>{function J(e){let o=new ArrayBuffer(e.byteLength);return new Uint8Array(o).set(e),o}function x(e){let{protocol:o,...r}=e;return{...r,runtimeProtocol:o,nativeViewports:e.nativeViewports.map(n=>({...n}))}}function O(e){if(e===null||typeof e=="boolean"||typeof e=="string")return JSON.stringify(e);if(typeof e=="number"){if(!Number.isFinite(e))throw new TypeError("Canonical JSON only supports finite numbers");if(Object.is(e,-0))return"0";let o=JSON.stringify(e),r=/^(-?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/.exec(o);if(!r)return o;let[,n,t,c="",f]=r,a=`${t}${c}`,d=t.length+Number(f);return d<=0?`${n}0.${"0".repeat(-d)}${a}`:d>=a.length?`${n}${a}${"0".repeat(d-a.length)}`:`${n}${a.slice(0,d)}.${a.slice(d)}`}if(Array.isArray(e))return`[${e.map(o=>{try{return O(o)}catch{return"null"}}).join(",")}]`;if(e&&typeof e=="object"){let o=Object.entries(e).filter(([,n])=>n!==void 0&&typeof n!="function").sort(([n],[t])=>n<t?-1:n>t?1:0),r=[];for(let[n,t]of o)try{r.push(`${JSON.stringify(n)}:${O(t)}`)}catch{}return`{${r.join(",")}}`}throw new TypeError("Unsupported canonical JSON value")}async function D(e){let o=new TextEncoder().encode(O(e)),r=await crypto.subtle.digest("SHA-256",J(o));return`sha256:${[...new Uint8Array(r)].map(t=>t.toString(16).padStart(2,"0")).join("")}`}var m="bcms-component-preview/2";function v(e){let{claims:o,dashboardOrigin:r,onProps:n,onStatus:t=()=>{},onError:c=()=>{},retryIntervalMs:f=400,maxAnnounces:a=10,ackDeadlineMs:d=250}=e,R=x(o),p=null,E=!1,T=0,$=0,y=null,l=s=>{p?.postMessage({protocol:m,binding:R,...s})},k=()=>{y!==null&&clearInterval(y),y=null};if(typeof window>"u"||window.parent===window)return t({kind:"not-embedded",dashboardOrigin:r}),{dispose:()=>{}};let P=()=>{if(!E){if(T>=a){k(),console.warn(`[bcms] no BetterCMS dashboard answered at ${r}. If the dashboard is on another origin, dashboardOrigin is misconfigured.`),t({kind:"unanswered",dashboardOrigin:r});return}T+=1,window.parent.postMessage({protocol:m,kind:"runtime:ready",binding:R},r)}},_=async s=>{let i=s.data;if(i?.protocol!==m||i.kind!=="props")return;let u=i.propsRevision;if(typeof u!="number"||u<=$)return;let b=await D(i.props);if(b!==i.propsHash){l({kind:"runtime:error",error:{code:"PROPS_HASH_MISMATCH",message:"Props did not match their hash."}});return}$=u;try{await n(i.props)}catch(H){console.error("[bcms] component render failed:",H),l({kind:"runtime:error",error:{code:"RENDER_FAILED",message:"The component could not be rendered."}});return}let I=!1,L=()=>{I||(I=!0,l({kind:"props:ack",propsRevision:u,propsHash:b}))};requestAnimationFrame(()=>requestAnimationFrame(L)),setTimeout(L,d)},C=s=>{if(s.origin!==r){let b=s.data;s.source===window.parent&&b?.kind==="runtime:connect"&&console.warn(`[bcms] a BetterCMS dashboard at ${s.origin} tried to connect, but dashboardOrigin is ${r}. Refusing \u2014 set dashboardOrigin to ${s.origin} if that is your dashboard.`);return}if(s.source!==window.parent)return;let i=s.data;if(i?.protocol!==m||i.kind!=="runtime:connect")return;let u=s.ports[0];u&&(E=!0,k(),p=u,p.onmessage=_,p.start?.(),t({kind:"connected"}),l({kind:"runtime:connected"}))},M=s=>{let i={code:"RUNTIME_ERROR",message:String(s.message).slice(0,300)};c(i),l({kind:"runtime:error",error:i})},N=s=>{let i={code:"UNHANDLED_REJECTION",message:String(s.reason).slice(0,300)};c(i),l({kind:"runtime:error",error:i})};return t({kind:"connecting",dashboardOrigin:r}),window.addEventListener("message",C),window.addEventListener("error",M),window.addEventListener("unhandledrejection",N),P(),y=setInterval(P,f),{dispose:()=>{k(),window.removeEventListener("message",C),window.removeEventListener("error",M),window.removeEventListener("unhandledrejection",N),p?.close(),p=null,E=!1}}}function A(e){typeof window>"u"||window.parent===window||window.parent.postMessage({protocol:m,kind:"runtime:refused",cause:e.cause},e.dashboardOrigin)}var U=15e3,S=document.getElementById("bcms-status"),g=document.getElementById("bcms-frame"),w=JSON.parse(document.getElementById("bcms-preview-config").textContent),h=e=>{S.textContent=e,S.hidden=!1},B=(e,o)=>fetch(e,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(o),credentials:"omit",cache:"no-store"}),F=50;function j(){try{let e=g.contentDocument;return!e||e.readyState==="loading"||e.location.href==="about:blank"?!1:!!e.querySelector("[data-bcms-preview-render]")}catch{return!1}}function q(e){return new Promise((o,r)=>{let n=!1,t=a=>{if(!n){if(n=!0,clearInterval(f),clearTimeout(c),g.onload=null,a){r(a);return}S.hidden=!0,g.hidden=!1,o()}},c=setTimeout(()=>t(new Error("render timed out")),U),f=setInterval(()=>{j()&&t()},F);g.onload=()=>t(j()?void 0:new Error("the render page did not render (blocked, not found, or failed)")),g.src=`${w.routes.render}?id=${encodeURIComponent(e)}`})}async function K(){let e=new URL(location.href).searchParams.get("bcmsSession");history.replaceState(null,"",location.pathname);let r=await(await B(w.routes.session,{token:e})).json().catch(()=>null);if(!r||!r.ok){let n=r&&!r.ok&&r.cause==="keys-unreachable"?"keys-unreachable":"session-refused";A({dashboardOrigin:w.dashboardOrigin,cause:n}),h("This preview session was refused. The runtime log records why.");return}v({claims:r.claims,dashboardOrigin:w.dashboardOrigin,onProps:async n=>{let t=await B(w.routes.props,{renderKey:r.renderKey,props:n});if(!t.ok)throw new Error(`props were refused (HTTP ${t.status})`);let{id:c}=await t.json();await q(c)},onStatus:n=>{n.kind!=="connected"&&(n.kind==="connecting"?h("Connecting to BetterCMS\u2026"):n.kind==="unanswered"?h(`${n.dashboardOrigin} did not answer.`):h("This page is meant to be opened from BetterCMS."))}})}K().catch(e=>{console.error("[bcms-preview]",e),h("The preview could not start. The browser console records why.")});})();
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@bettercms-ai/preview-runtime",
3
+ "version": "0.1.0",
4
+ "description": "Zero-config BetterCMS component previews: builds a preview runtime for Next.js and Astro apps and validates components in CI.",
5
+ "type": "module",
6
+ "bin": {
7
+ "bcms-preview": "./dist/cli.js"
8
+ },
9
+ "exports": {
10
+ "./server": {
11
+ "types": "./dist/server.d.ts",
12
+ "import": "./dist/server.js",
13
+ "default": "./dist/server.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "sideEffects": false,
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "prepublishOnly": "bun run build"
24
+ },
25
+ "dependencies": {
26
+ "@bettercms-ai/component-output": "^0.2.0",
27
+ "playwright": "^1.55.0"
28
+ },
29
+ "devDependencies": {
30
+ "tsup": "^8.5.1"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "registry": "https://registry.npmjs.org/"
35
+ }
36
+ }