@bettercms-ai/preview-runtime 0.3.0 → 0.4.1
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 +9 -0
- package/dist/cli.js +372 -161
- package/dist/cli.js.map +1 -1
- package/dist/scope-client.global.js +1 -1
- package/dist/server.js +5 -2
- package/dist/shell.global.js +2 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -60,6 +60,15 @@ real Next app yet; Astro is verified.
|
|
|
60
60
|
A failed validation reports what broke — the app server's own error lines first — and the dashboard shows
|
|
61
61
|
it under "Runtime + console".
|
|
62
62
|
|
|
63
|
+
## Batch validation
|
|
64
|
+
|
|
65
|
+
One run can validate several components against a single build. When the dispatch carries a `batch`
|
|
66
|
+
(`BCMS_BATCH`, a JSON array of `{ requestId, componentId, familyKey, nativeViewports }`), the validator builds and
|
|
67
|
+
uploads the runtime once, starts one browser, and validates each component in turn — a component that cannot be
|
|
68
|
+
validated is reported for that component and the rest carry on. Without `BCMS_BATCH` it validates the single
|
|
69
|
+
component named by `BCMS_REQUEST_ID` / `BCMS_COMPONENT_ID` / `BCMS_FAMILY_KEY` / `BCMS_NATIVE_VIEWPORTS`, as
|
|
70
|
+
before. The step fails when any component could not be validated.
|
|
71
|
+
|
|
63
72
|
## How a preview renders
|
|
64
73
|
|
|
65
74
|
- **Astro**: built with `output: "server"` and `@astrojs/node`, so `.astro` components render on the
|
package/dist/cli.js
CHANGED
|
@@ -412,6 +412,28 @@ import { tmpdir } from "os";
|
|
|
412
412
|
import { dirname as dirname2, join as join2 } from "path";
|
|
413
413
|
var PREVIEW_ROUTE_BASE = "/__bettercms/component-preview/__bcms";
|
|
414
414
|
var RUNTIME_PATH = `${PREVIEW_ROUTE_BASE}/runtime`;
|
|
415
|
+
var API_TIMEOUT_MS = 6e4;
|
|
416
|
+
var LOCAL_FETCH_TIMEOUT_MS = 3e4;
|
|
417
|
+
var HEALTH_PROBE_TIMEOUT_MS = 5e3;
|
|
418
|
+
var RENDER_TIMEOUT_PER_VIEWPORT_MS = 9e4;
|
|
419
|
+
var BUNDLE_UPLOAD_TIMEOUT_MS = 5 * 6e4;
|
|
420
|
+
var DEFAULT_BATCH_BUDGET_MS = 15 * 6e4;
|
|
421
|
+
async function bounded(what, ms, run2) {
|
|
422
|
+
const controller = new AbortController();
|
|
423
|
+
let timer;
|
|
424
|
+
const expired = new Promise((_, reject) => {
|
|
425
|
+
timer = setTimeout(() => {
|
|
426
|
+
controller.abort();
|
|
427
|
+
const limit = ms >= 1e3 ? `${Math.round(ms / 1e3)} seconds` : `${ms} ms`;
|
|
428
|
+
reject(new ValidationFailure("COMPONENT_VALIDATION_STEP_TIMEOUT", `${what} did not finish within ${limit}.`));
|
|
429
|
+
}, ms);
|
|
430
|
+
});
|
|
431
|
+
try {
|
|
432
|
+
return await Promise.race([run2(controller.signal), expired]);
|
|
433
|
+
} finally {
|
|
434
|
+
clearTimeout(timer);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
415
437
|
var ValidationFailure = class extends Error {
|
|
416
438
|
constructor(code, message) {
|
|
417
439
|
super(message);
|
|
@@ -420,11 +442,49 @@ var ValidationFailure = class extends Error {
|
|
|
420
442
|
}
|
|
421
443
|
code;
|
|
422
444
|
};
|
|
423
|
-
function required(name) {
|
|
424
|
-
const value =
|
|
445
|
+
function required(name, env = process.env) {
|
|
446
|
+
const value = env[name]?.trim();
|
|
425
447
|
if (!value) throw new CliFailure(`${name} is not set. This command runs inside the BetterCMS component validation workflow.`);
|
|
426
448
|
return value;
|
|
427
449
|
}
|
|
450
|
+
var isViewport = (value) => !!value && typeof value === "object" && typeof value.name === "string" && Number.isFinite(value.width) && Number.isFinite(value.height);
|
|
451
|
+
var isBatchItem = (value) => {
|
|
452
|
+
if (!value || typeof value !== "object") return false;
|
|
453
|
+
const item = value;
|
|
454
|
+
return ["requestId", "componentId", "familyKey"].every((key) => typeof item[key] === "string" && item[key].trim() !== "") && Array.isArray(item.nativeViewports) && item.nativeViewports.length > 0 && item.nativeViewports.every(isViewport);
|
|
455
|
+
};
|
|
456
|
+
var invalidBatch = (detail) => new CliFailure(`COMPONENT_VALIDATION_BATCH_INVALID: ${detail}`);
|
|
457
|
+
function resolveBatch(env = process.env) {
|
|
458
|
+
const raw = env.BCMS_BATCH?.trim();
|
|
459
|
+
if (raw && raw !== "null") {
|
|
460
|
+
let parsed;
|
|
461
|
+
try {
|
|
462
|
+
parsed = JSON.parse(raw);
|
|
463
|
+
} catch {
|
|
464
|
+
throw invalidBatch("BCMS_BATCH is not valid JSON.");
|
|
465
|
+
}
|
|
466
|
+
if (!Array.isArray(parsed) || parsed.length === 0 || !parsed.every(isBatchItem)) {
|
|
467
|
+
throw invalidBatch("BCMS_BATCH must be a non-empty array of { requestId, componentId, familyKey, nativeViewports }.");
|
|
468
|
+
}
|
|
469
|
+
if (new Set(parsed.map((item2) => item2.requestId)).size !== parsed.length) {
|
|
470
|
+
throw invalidBatch("BCMS_BATCH names the same request twice.");
|
|
471
|
+
}
|
|
472
|
+
return parsed;
|
|
473
|
+
}
|
|
474
|
+
const requestId = required("BCMS_REQUEST_ID", env);
|
|
475
|
+
const componentId = required("BCMS_COMPONENT_ID", env);
|
|
476
|
+
const familyKey = required("BCMS_FAMILY_KEY", env);
|
|
477
|
+
let nativeViewports;
|
|
478
|
+
try {
|
|
479
|
+
nativeViewports = JSON.parse(required("BCMS_NATIVE_VIEWPORTS", env));
|
|
480
|
+
} catch (error) {
|
|
481
|
+
if (error instanceof CliFailure) throw error;
|
|
482
|
+
throw invalidBatch("BCMS_NATIVE_VIEWPORTS is not valid JSON.");
|
|
483
|
+
}
|
|
484
|
+
const item = { requestId, componentId, familyKey, nativeViewports };
|
|
485
|
+
if (!isBatchItem(item)) throw invalidBatch("BCMS_NATIVE_VIEWPORTS must be a non-empty array of { name, width, height }.");
|
|
486
|
+
return [item];
|
|
487
|
+
}
|
|
428
488
|
var sha256 = (data) => createHash("sha256").update(data).digest("hex");
|
|
429
489
|
function canonical(value) {
|
|
430
490
|
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
@@ -433,6 +493,191 @@ function canonical(value) {
|
|
|
433
493
|
}
|
|
434
494
|
return JSON.stringify(value);
|
|
435
495
|
}
|
|
496
|
+
var toCliFailure = (error) => new CliFailure(`${error instanceof ValidationFailure ? error.code : "COMPONENT_VALIDATION_FAILED"}: ${error.message.slice(0, 2e3)}`);
|
|
497
|
+
async function runValidationBatch(batch, ctx, deps) {
|
|
498
|
+
const now = deps.now ?? Date.now;
|
|
499
|
+
const deadline = now() + (deps.budgetMs ?? DEFAULT_BATCH_BUDGET_MS);
|
|
500
|
+
const propsMs = deps.timeouts?.propsMs ?? LOCAL_FETCH_TIMEOUT_MS;
|
|
501
|
+
const renderPerViewportMs = deps.timeouts?.renderPerViewportMs ?? RENDER_TIMEOUT_PER_VIEWPORT_MS;
|
|
502
|
+
const apiMs = deps.timeouts?.apiMs ?? API_TIMEOUT_MS;
|
|
503
|
+
const seconds = (ms) => `${Math.ceil(Math.max(ms, 0) / 1e3)}s`;
|
|
504
|
+
const log = deps.log ?? ((line) => console.log(line));
|
|
505
|
+
const logError = deps.error ?? ((line) => console.error(line));
|
|
506
|
+
const requestPath = (item) => `/api/v1/projects/${ctx.projectId}/component-implementation/implementation-requests/${item.requestId}`;
|
|
507
|
+
const completed = [];
|
|
508
|
+
const failed = [];
|
|
509
|
+
const claim = (item) => deps.api(`${requestPath(item)}/claim`, {
|
|
510
|
+
method: "POST",
|
|
511
|
+
body: {
|
|
512
|
+
componentId: item.componentId,
|
|
513
|
+
commitSha: ctx.commitSha,
|
|
514
|
+
adapter: {
|
|
515
|
+
protocol: "bcms-component-runtime-v1",
|
|
516
|
+
kind: "project-route",
|
|
517
|
+
path: RUNTIME_PATH,
|
|
518
|
+
familyKey: item.familyKey,
|
|
519
|
+
previewOrigin: ctx.previewOrigin,
|
|
520
|
+
nativeViewports: item.nativeViewports
|
|
521
|
+
},
|
|
522
|
+
providerRunId: ctx.run.providerRunId,
|
|
523
|
+
providerRunAttempt: ctx.run.providerRunAttempt,
|
|
524
|
+
providerRunUrl: ctx.run.providerRunUrl,
|
|
525
|
+
workflowRef: ctx.run.workflowRef,
|
|
526
|
+
// The server caps this at the request's own expiry and at ten minutes.
|
|
527
|
+
credentialExpiresAt: new Date(Date.now() + 10 * 6e4 - 5e3).toISOString()
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
const report = async (item, held, error) => {
|
|
531
|
+
const code = error instanceof ValidationFailure ? error.code : "COMPONENT_VALIDATION_FAILED";
|
|
532
|
+
const message = error.message.slice(0, 2e3);
|
|
533
|
+
failed.push(item.componentId);
|
|
534
|
+
logError(`bcms-preview: ${code}: ${message} (component ${item.componentId})`);
|
|
535
|
+
const reported = held ?? await claim(item).catch((claimError) => {
|
|
536
|
+
logError(`bcms-preview: could not claim request ${item.requestId} to report the failure: ${claimError.message}`);
|
|
537
|
+
return null;
|
|
538
|
+
});
|
|
539
|
+
if (!reported) return;
|
|
540
|
+
await deps.api(`${requestPath(item)}/fail`, {
|
|
541
|
+
method: "POST",
|
|
542
|
+
claim: reported.claimCapability,
|
|
543
|
+
body: { componentId: item.componentId, errorCode: code, errorMessage: message }
|
|
544
|
+
}).catch((reportError) => logError(`bcms-preview: could not report the failure: ${reportError.message}`));
|
|
545
|
+
};
|
|
546
|
+
let manifest;
|
|
547
|
+
try {
|
|
548
|
+
manifest = await deps.api(`/api/v1/projects/${ctx.projectId}/component-preview/manifest?kinds=page`);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
for (const item of batch) await report(item, null, error);
|
|
551
|
+
throw toCliFailure(error);
|
|
552
|
+
}
|
|
553
|
+
const hintFor = (componentId) => {
|
|
554
|
+
const entry = manifest.components.find((c) => c.id === componentId);
|
|
555
|
+
return entry?.fallback && entry.source.kind !== "page" ? [`Clear the recorded source: this component renders from page ${entry.fallback.route} without one.`] : [];
|
|
556
|
+
};
|
|
557
|
+
const pending = [];
|
|
558
|
+
for (const item of batch) {
|
|
559
|
+
if (manifest.components.some((c) => c.id === item.componentId)) {
|
|
560
|
+
pending.push(item);
|
|
561
|
+
} else {
|
|
562
|
+
await report(item, null, new ValidationFailure(
|
|
563
|
+
"COMPONENT_SOURCE_NOT_RECORDED",
|
|
564
|
+
"No file is recorded and this component is neither placed on a page nor used in the layout, so there is nothing to render. Place it, add it to the layout, or record its file."
|
|
565
|
+
));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (pending.length === 0) return { completed, failed, exitCode: 1 };
|
|
569
|
+
let runtime = null;
|
|
570
|
+
try {
|
|
571
|
+
runtime = await deps.prepare(manifest);
|
|
572
|
+
const upload = await deps.api(`/api/v1/projects/${ctx.projectId}/artifacts/upload-url`, { method: "POST" });
|
|
573
|
+
const put = await deps.putBundle(upload.uploadUrl, runtime.bundle);
|
|
574
|
+
if (!put.ok) {
|
|
575
|
+
throw new ValidationFailure("COMPONENT_PREVIEW_BUNDLE_UPLOAD_FAILED", `Storage refused the preview bundle (HTTP ${put.status}).`);
|
|
576
|
+
}
|
|
577
|
+
await deps.api(`/api/v1/projects/${ctx.projectId}/component-preview/bundles`, {
|
|
578
|
+
method: "POST",
|
|
579
|
+
body: { commitSha: ctx.commitSha, uploadKey: upload.uploadKey, checksum: `sha256:${sha256(runtime.bundle)}`, sizeBytes: runtime.bundle.byteLength }
|
|
580
|
+
});
|
|
581
|
+
} catch (error) {
|
|
582
|
+
await runtime?.close().catch(() => {
|
|
583
|
+
});
|
|
584
|
+
for (const item of pending) {
|
|
585
|
+
const hint = error instanceof ValidationFailure && error.code === "COMPONENT_PREVIEW_BUILD_FAILED" ? hintFor(item.componentId) : [];
|
|
586
|
+
await report(item, null, hint.length ? new ValidationFailure(error.code, [error.message, ...hint].join(" ")) : error);
|
|
587
|
+
}
|
|
588
|
+
throw toCliFailure(error);
|
|
589
|
+
}
|
|
590
|
+
const ready = runtime;
|
|
591
|
+
try {
|
|
592
|
+
for (const item of pending) {
|
|
593
|
+
const worstCase = propsMs + renderPerViewportMs * item.nativeViewports.length + 2 * apiMs;
|
|
594
|
+
const left = deadline - now();
|
|
595
|
+
if (left < worstCase) {
|
|
596
|
+
await report(item, null, new ValidationFailure(
|
|
597
|
+
"COMPONENT_VALIDATION_BATCH_TIMEOUT",
|
|
598
|
+
`The validation batch did not have enough time left for this component (needs up to ${seconds(worstCase)}, ${seconds(left)} left). Request validation again.`
|
|
599
|
+
));
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const entry = manifest.components.find((c) => c.id === item.componentId);
|
|
603
|
+
const hint = hintFor(item.componentId);
|
|
604
|
+
let held = null;
|
|
605
|
+
try {
|
|
606
|
+
const mark = ready.serverErrorCount();
|
|
607
|
+
const renderId = await bounded(
|
|
608
|
+
`The preview runtime's props endpoint for ${item.componentId}`,
|
|
609
|
+
propsMs,
|
|
610
|
+
(signal) => ready.storeProps(item.componentId, entry.defaultProps, signal)
|
|
611
|
+
);
|
|
612
|
+
const render = await bounded(
|
|
613
|
+
`Rendering ${item.componentId}`,
|
|
614
|
+
renderPerViewportMs * item.nativeViewports.length,
|
|
615
|
+
(signal) => ready.render(
|
|
616
|
+
renderId,
|
|
617
|
+
item.nativeViewports,
|
|
618
|
+
manifest.brandTokenNames,
|
|
619
|
+
// A page-kind render lifts a section too, and stamps its root.
|
|
620
|
+
{ section: ready.kinds[item.componentId] !== "file" },
|
|
621
|
+
signal
|
|
622
|
+
)
|
|
623
|
+
);
|
|
624
|
+
const serverErrors = ready.serverErrorsSince(mark).slice(0, 5);
|
|
625
|
+
held = await claim(item);
|
|
626
|
+
const target = held.request;
|
|
627
|
+
const checks = {
|
|
628
|
+
brandKit: {
|
|
629
|
+
status: render.missingTokens.length === 0 ? "passed" : "failed",
|
|
630
|
+
contractHash: target.brandContractHash,
|
|
631
|
+
missingTokens: render.missingTokens
|
|
632
|
+
},
|
|
633
|
+
runtime: {
|
|
634
|
+
status: render.runtimeErrors === 0 && render.consoleErrors === 0 ? "passed" : "failed",
|
|
635
|
+
runtimeErrors: render.runtimeErrors,
|
|
636
|
+
consoleErrors: render.consoleErrors,
|
|
637
|
+
// Only on a failure, and bounded: the server's own error lines first (the real cause), then what the
|
|
638
|
+
// browser saw. The database stores them and the panel shows the first one.
|
|
639
|
+
...render.runtimeErrors + render.consoleErrors > 0 ? { problems: [...hint, ...serverErrors.map((line) => `server: ${line}`), ...render.problems].slice(0, 10).map((line) => line.slice(0, 200)) } : {}
|
|
640
|
+
},
|
|
641
|
+
visual: { status: "baseline-missing", reviewRequired: true, viewports: render.results }
|
|
642
|
+
};
|
|
643
|
+
const evidenceDigest = `sha256:${sha256(canonical({ requestId: item.requestId, commitSha: target.commitSha, checks }))}`;
|
|
644
|
+
await deps.api(`${requestPath(item)}/complete`, {
|
|
645
|
+
method: "POST",
|
|
646
|
+
claim: held.claimCapability,
|
|
647
|
+
body: {
|
|
648
|
+
componentId: target.componentId,
|
|
649
|
+
candidateId: target.candidateId,
|
|
650
|
+
componentVersion: target.componentVersion,
|
|
651
|
+
familyKey: target.familyKey,
|
|
652
|
+
familyContractHash: target.familyContractHash,
|
|
653
|
+
schemaHash: target.schemaHash,
|
|
654
|
+
brandContractHash: target.brandContractHash,
|
|
655
|
+
dependenciesHash: target.dependenciesHash,
|
|
656
|
+
commitSha: target.commitSha,
|
|
657
|
+
adapterHash: target.adapterHash,
|
|
658
|
+
familyManifestHash: target.familyManifestHash,
|
|
659
|
+
nativeViewports: target.nativeViewports,
|
|
660
|
+
checks,
|
|
661
|
+
evidenceDigest
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
completed.push(item.componentId);
|
|
665
|
+
const passed = checks.brandKit.status === "passed" && checks.runtime.status === "passed";
|
|
666
|
+
log(`bcms-preview: validation ${passed ? "PASSED" : "FAILED"} for ${item.componentId}`);
|
|
667
|
+
if (!passed) {
|
|
668
|
+
if (render.missingTokens.length) log(` brand tokens used but not defined: ${render.missingTokens.join(", ")}`);
|
|
669
|
+
for (const problem of [...hint, ...render.problems]) log(` ${problem}`);
|
|
670
|
+
}
|
|
671
|
+
} catch (error) {
|
|
672
|
+
await report(item, held, error);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
} finally {
|
|
676
|
+
await ready.close().catch(() => {
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
return { completed, failed, exitCode: failed.length > 0 ? 1 : 0 };
|
|
680
|
+
}
|
|
436
681
|
function freePort() {
|
|
437
682
|
return new Promise((resolve2, reject) => {
|
|
438
683
|
const server = createServer();
|
|
@@ -448,7 +693,7 @@ async function waitForOk(url, timeoutMs) {
|
|
|
448
693
|
const deadline = Date.now() + timeoutMs;
|
|
449
694
|
while (Date.now() < deadline) {
|
|
450
695
|
try {
|
|
451
|
-
if ((await fetch(url)).ok) return true;
|
|
696
|
+
if ((await fetch(url, { signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS) })).ok) return true;
|
|
452
697
|
} catch {
|
|
453
698
|
}
|
|
454
699
|
await new Promise((r) => setTimeout(r, 500));
|
|
@@ -465,18 +710,22 @@ function ensureBrowser() {
|
|
|
465
710
|
throw new ValidationFailure("COMPONENT_VALIDATION_BROWSER_UNAVAILABLE", "A headless browser could not be installed on this runner.");
|
|
466
711
|
}
|
|
467
712
|
}
|
|
468
|
-
async function renderInBrowser(url, viewports, tokenNames, options) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
713
|
+
async function renderInBrowser(browser, url, viewports, tokenNames, options, signal) {
|
|
714
|
+
let consoleErrors = 0;
|
|
715
|
+
let runtimeErrors = 0;
|
|
716
|
+
const missingTokens = /* @__PURE__ */ new Set();
|
|
717
|
+
const results = [];
|
|
718
|
+
const problems = [];
|
|
719
|
+
for (const viewport of viewports) {
|
|
720
|
+
if (signal?.aborted) break;
|
|
721
|
+
const page = await browser.newPage({ viewport: { width: viewport.width, height: viewport.height } });
|
|
722
|
+
const onAbort = () => {
|
|
723
|
+
page.close().catch(() => {
|
|
724
|
+
});
|
|
725
|
+
};
|
|
726
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
727
|
+
if (signal?.aborted) onAbort();
|
|
728
|
+
try {
|
|
480
729
|
page.on("console", (message) => {
|
|
481
730
|
if (message.type() === "error") {
|
|
482
731
|
consoleErrors += 1;
|
|
@@ -524,34 +773,51 @@ async function renderInBrowser(url, viewports, tokenNames, options) {
|
|
|
524
773
|
status: "baseline-missing",
|
|
525
774
|
candidateDigest: `sha256:${sha256(png)}`
|
|
526
775
|
});
|
|
527
|
-
|
|
776
|
+
} finally {
|
|
777
|
+
signal?.removeEventListener("abort", onAbort);
|
|
778
|
+
await page.close().catch(() => {
|
|
779
|
+
});
|
|
528
780
|
}
|
|
529
|
-
return { results, consoleErrors, runtimeErrors, missingTokens: [...missingTokens].sort(), problems };
|
|
530
|
-
} finally {
|
|
531
|
-
await browser.close();
|
|
532
781
|
}
|
|
782
|
+
return { results, consoleErrors, runtimeErrors, missingTokens: [...missingTokens].sort(), problems };
|
|
533
783
|
}
|
|
534
784
|
async function validateComponent() {
|
|
535
785
|
const apiUrl = required("BCMS_API_URL");
|
|
536
786
|
const apiKey = required("BCMS_API_KEY");
|
|
537
|
-
const
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
787
|
+
const batch = resolveBatch();
|
|
788
|
+
const ctx = {
|
|
789
|
+
projectId: required("BCMS_PROJECT_ID"),
|
|
790
|
+
commitSha: required("BCMS_COMMIT_SHA"),
|
|
791
|
+
previewOrigin: required("BCMS_PREVIEW_ORIGIN"),
|
|
792
|
+
run: {
|
|
793
|
+
providerRunId: required("BCMS_RUN_ID"),
|
|
794
|
+
providerRunAttempt: Number(process.env.BCMS_RUN_ATTEMPT) || 1,
|
|
795
|
+
providerRunUrl: required("BCMS_RUN_URL"),
|
|
796
|
+
workflowRef: required("BCMS_WORKFLOW_REF")
|
|
797
|
+
}
|
|
798
|
+
};
|
|
544
799
|
const api = async (path, init = {}) => {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
800
|
+
let response;
|
|
801
|
+
let text;
|
|
802
|
+
try {
|
|
803
|
+
response = await fetch(new URL(path, apiUrl), {
|
|
804
|
+
method: init.method ?? "GET",
|
|
805
|
+
headers: {
|
|
806
|
+
authorization: `Bearer ${apiKey}`,
|
|
807
|
+
"content-type": "application/json",
|
|
808
|
+
...init.claim ? { "x-bcms-component-claim": init.claim } : {}
|
|
809
|
+
},
|
|
810
|
+
...init.body === void 0 ? {} : { body: JSON.stringify(init.body) },
|
|
811
|
+
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
|
812
|
+
});
|
|
813
|
+
text = await response.text();
|
|
814
|
+
} catch (error) {
|
|
815
|
+
const name = error.name;
|
|
816
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
817
|
+
throw new ValidationFailure("COMPONENT_VALIDATION_API_TIMEOUT", `${init.method ?? "GET"} ${path} did not answer within ${API_TIMEOUT_MS / 1e3} seconds.`);
|
|
818
|
+
}
|
|
819
|
+
throw error;
|
|
820
|
+
}
|
|
555
821
|
let parsed = null;
|
|
556
822
|
try {
|
|
557
823
|
parsed = JSON.parse(text);
|
|
@@ -565,38 +831,10 @@ async function validateComponent() {
|
|
|
565
831
|
}
|
|
566
832
|
return (parsed && "data" in parsed ? parsed.data : parsed) ?? {};
|
|
567
833
|
};
|
|
568
|
-
const claimPath = `/api/v1/projects/${projectId}/component-implementation/implementation-requests/${requestId}`;
|
|
569
|
-
let claim = null;
|
|
570
|
-
const claimRequest = async () => {
|
|
571
|
-
claim = await api(`${claimPath}/claim`, {
|
|
572
|
-
method: "POST",
|
|
573
|
-
body: {
|
|
574
|
-
componentId,
|
|
575
|
-
commitSha,
|
|
576
|
-
adapter: { protocol: "bcms-component-runtime-v1", kind: "project-route", path: RUNTIME_PATH, familyKey, previewOrigin, nativeViewports },
|
|
577
|
-
providerRunId: required("BCMS_RUN_ID"),
|
|
578
|
-
providerRunAttempt: Number(process.env.BCMS_RUN_ATTEMPT) || 1,
|
|
579
|
-
providerRunUrl: required("BCMS_RUN_URL"),
|
|
580
|
-
workflowRef: required("BCMS_WORKFLOW_REF"),
|
|
581
|
-
// The server caps this at the request's own expiry and at ten minutes.
|
|
582
|
-
credentialExpiresAt: new Date(Date.now() + 10 * 6e4 - 5e3).toISOString()
|
|
583
|
-
}
|
|
584
|
-
});
|
|
585
|
-
return claim;
|
|
586
|
-
};
|
|
587
834
|
const work = mkdtempSync(join2(tmpdir(), "bcms-validate-"));
|
|
588
|
-
|
|
589
|
-
try {
|
|
590
|
-
const manifest = await api(`/api/v1/projects/${projectId}/component-preview/manifest?kinds=page`);
|
|
591
|
-
const entry = manifest.components.find((c) => c.id === componentId);
|
|
592
|
-
if (!entry) {
|
|
593
|
-
throw new ValidationFailure(
|
|
594
|
-
"COMPONENT_SOURCE_NOT_RECORDED",
|
|
595
|
-
"No file is recorded and this component is neither placed on a page nor used in the layout, so there is nothing to render. Place it, add it to the layout, or record its file."
|
|
596
|
-
);
|
|
597
|
-
}
|
|
835
|
+
const prepare = async (manifest) => {
|
|
598
836
|
const manifestPath = join2(work, "manifest.json");
|
|
599
|
-
writeFileSync2(manifestPath, JSON.stringify({ components: manifest.components.map(({ id
|
|
837
|
+
writeFileSync2(manifestPath, JSON.stringify({ components: manifest.components.map(({ id, source, fallback }) => ({ id, source, ...fallback ? { fallback } : {} })) }));
|
|
600
838
|
const out = join2(work, "runtime");
|
|
601
839
|
let built;
|
|
602
840
|
try {
|
|
@@ -604,12 +842,17 @@ async function validateComponent() {
|
|
|
604
842
|
} catch (error) {
|
|
605
843
|
throw new ValidationFailure("COMPONENT_PREVIEW_BUILD_FAILED", `The preview build failed: ${error.message}`);
|
|
606
844
|
}
|
|
845
|
+
const tarball = join2(work, "bundle.tgz");
|
|
846
|
+
if (spawnSync2("tar", ["-czf", tarball, "-C", out, "."], { stdio: "inherit" }).status !== 0) {
|
|
847
|
+
throw new ValidationFailure("COMPONENT_PREVIEW_BUNDLE_PACK_FAILED", "The preview runtime could not be packaged.");
|
|
848
|
+
}
|
|
849
|
+
const bundle = readFileSync2(tarball);
|
|
607
850
|
const runtimeManifest = JSON.parse(readFileSync2(join2(out, "bcms-runtime.json"), "utf8"));
|
|
608
851
|
const port = await freePort();
|
|
609
852
|
const validatorKey = randomBytes(32).toString("base64url");
|
|
610
853
|
const local = `http://127.0.0.1:${port}`;
|
|
611
854
|
const { BCMS_API_KEY: _withheld, ...inherited } = process.env;
|
|
612
|
-
|
|
855
|
+
const child = spawn(process.execPath, [join2(out, runtimeManifest.dir, runtimeManifest.entry)], {
|
|
613
856
|
cwd: join2(out, runtimeManifest.dir),
|
|
614
857
|
env: {
|
|
615
858
|
...inherited,
|
|
@@ -629,108 +872,76 @@ async function validateComponent() {
|
|
|
629
872
|
stream.write(chunk);
|
|
630
873
|
for (const line of chunk.toString("utf8").split("\n")) {
|
|
631
874
|
const clean = line.replace(/\x1b\[[0-9;]*m/g, "").trim();
|
|
632
|
-
if (serverErrors.length <
|
|
875
|
+
if (serverErrors.length < 500 && /\b(error|exception)\b/i.test(clean)) serverErrors.push(clean.slice(0, 200));
|
|
633
876
|
}
|
|
634
877
|
};
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const { id } = await stored.json();
|
|
649
|
-
const render = await renderInBrowser(
|
|
650
|
-
`${local}${PREVIEW_ROUTE_BASE}/render?id=${encodeURIComponent(id)}`,
|
|
651
|
-
nativeViewports,
|
|
652
|
-
manifest.brandTokenNames,
|
|
653
|
-
// A page-kind render lifts a section too, and stamps its root.
|
|
654
|
-
{ section: built.kinds[componentId] !== "file" }
|
|
655
|
-
);
|
|
656
|
-
const tarball = join2(work, "bundle.tgz");
|
|
657
|
-
if (spawnSync2("tar", ["-czf", tarball, "-C", out, "."], { stdio: "inherit" }).status !== 0) {
|
|
658
|
-
throw new ValidationFailure("COMPONENT_PREVIEW_BUNDLE_PACK_FAILED", "The preview runtime could not be packaged.");
|
|
878
|
+
child.stdout?.on("data", collect(process.stdout));
|
|
879
|
+
child.stderr?.on("data", collect(process.stderr));
|
|
880
|
+
let browser = null;
|
|
881
|
+
try {
|
|
882
|
+
if (!await waitForOk(`${local}${PREVIEW_ROUTE_BASE}/health`, 6e4)) {
|
|
883
|
+
throw new ValidationFailure("COMPONENT_PREVIEW_RUNTIME_DID_NOT_START", "The preview runtime did not start within 60 seconds.");
|
|
884
|
+
}
|
|
885
|
+
ensureBrowser();
|
|
886
|
+
const { chromium } = await import("playwright");
|
|
887
|
+
browser = await chromium.launch();
|
|
888
|
+
} catch (error) {
|
|
889
|
+
child.kill();
|
|
890
|
+
throw error;
|
|
659
891
|
}
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
// Only on a failure, and bounded: the server's own error lines first (the real cause), then what the
|
|
676
|
-
// browser saw. The database stores them and the panel shows the first one.
|
|
677
|
-
...render.runtimeErrors + render.consoleErrors > 0 ? { problems: [...hint, ...serverErrors.map((line) => `server: ${line}`), ...render.problems].slice(0, 10).map((line) => line.slice(0, 200)) } : {}
|
|
892
|
+
const shared = browser;
|
|
893
|
+
return {
|
|
894
|
+
kinds: built.kinds,
|
|
895
|
+
bundle,
|
|
896
|
+
storeProps: async (componentId, props, signal) => {
|
|
897
|
+
const stored = await fetch(`${local}${PREVIEW_ROUTE_BASE}/props`, {
|
|
898
|
+
method: "POST",
|
|
899
|
+
headers: { "content-type": "application/json", "x-bcms-validator-key": validatorKey },
|
|
900
|
+
body: JSON.stringify({ componentId, props }),
|
|
901
|
+
signal
|
|
902
|
+
});
|
|
903
|
+
if (!stored.ok) {
|
|
904
|
+
throw new ValidationFailure("COMPONENT_PREVIEW_RUNTIME_REFUSED", `The preview runtime refused the component (HTTP ${stored.status}).`);
|
|
905
|
+
}
|
|
906
|
+
return (await stored.json()).id;
|
|
678
907
|
},
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
await api(`/api/v1/projects/${projectId}/component-preview/bundles`, {
|
|
687
|
-
method: "POST",
|
|
688
|
-
body: { commitSha: target.commitSha, uploadKey: upload.uploadKey, checksum: `sha256:${sha256(bytes)}`, sizeBytes: bytes.byteLength }
|
|
689
|
-
});
|
|
690
|
-
const evidenceDigest = `sha256:${sha256(canonical({ requestId, commitSha: target.commitSha, checks }))}`;
|
|
691
|
-
await api(`${claimPath}/complete`, {
|
|
692
|
-
method: "POST",
|
|
693
|
-
claim: claim.claimCapability,
|
|
694
|
-
body: {
|
|
695
|
-
componentId: target.componentId,
|
|
696
|
-
candidateId: target.candidateId,
|
|
697
|
-
componentVersion: target.componentVersion,
|
|
698
|
-
familyKey: target.familyKey,
|
|
699
|
-
familyContractHash: target.familyContractHash,
|
|
700
|
-
schemaHash: target.schemaHash,
|
|
701
|
-
brandContractHash: target.brandContractHash,
|
|
702
|
-
dependenciesHash: target.dependenciesHash,
|
|
703
|
-
commitSha: target.commitSha,
|
|
704
|
-
adapterHash: target.adapterHash,
|
|
705
|
-
familyManifestHash: target.familyManifestHash,
|
|
706
|
-
nativeViewports: target.nativeViewports,
|
|
707
|
-
checks,
|
|
708
|
-
evidenceDigest
|
|
908
|
+
render: (renderId, viewports, tokenNames, options, signal) => renderInBrowser(shared, `${local}${PREVIEW_ROUTE_BASE}/render?id=${encodeURIComponent(renderId)}`, viewports, tokenNames, options, signal),
|
|
909
|
+
serverErrorCount: () => serverErrors.length,
|
|
910
|
+
serverErrorsSince: (mark) => serverErrors.slice(mark),
|
|
911
|
+
close: async () => {
|
|
912
|
+
child.kill();
|
|
913
|
+
await shared.close().catch(() => {
|
|
914
|
+
});
|
|
709
915
|
}
|
|
916
|
+
};
|
|
917
|
+
};
|
|
918
|
+
const budget = Number(process.env.BCMS_BATCH_BUDGET_MS);
|
|
919
|
+
try {
|
|
920
|
+
const result = await runValidationBatch(batch, ctx, {
|
|
921
|
+
api,
|
|
922
|
+
prepare,
|
|
923
|
+
putBundle: async (uploadUrl, bytes) => {
|
|
924
|
+
try {
|
|
925
|
+
const put = await fetch(uploadUrl, {
|
|
926
|
+
method: "PUT",
|
|
927
|
+
body: new Uint8Array(bytes),
|
|
928
|
+
headers: { "content-type": "application/gzip" },
|
|
929
|
+
signal: AbortSignal.timeout(BUNDLE_UPLOAD_TIMEOUT_MS)
|
|
930
|
+
});
|
|
931
|
+
return { ok: put.ok, status: put.status };
|
|
932
|
+
} catch (error) {
|
|
933
|
+
const name = error.name;
|
|
934
|
+
if (name !== "TimeoutError" && name !== "AbortError") throw error;
|
|
935
|
+
throw new ValidationFailure("COMPONENT_PREVIEW_BUNDLE_UPLOAD_FAILED", `Storage did not accept the preview bundle within ${BUNDLE_UPLOAD_TIMEOUT_MS / 6e4} minutes.`);
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
budgetMs: Number.isFinite(budget) && budget > 0 ? budget : DEFAULT_BATCH_BUDGET_MS
|
|
710
939
|
});
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (!passed) {
|
|
714
|
-
if (render.missingTokens.length) console.log(` brand tokens used but not defined: ${render.missingTokens.join(", ")}`);
|
|
715
|
-
for (const problem of [...hint, ...render.problems]) console.log(` ${problem}`);
|
|
716
|
-
}
|
|
717
|
-
} catch (error) {
|
|
718
|
-
const code = error instanceof ValidationFailure ? error.code : "COMPONENT_VALIDATION_FAILED";
|
|
719
|
-
const message = error.message.slice(0, 2e3);
|
|
720
|
-
const reported = claim ?? await claimRequest().catch((claimError) => {
|
|
721
|
-
console.error(`bcms-preview: could not claim the request to report the failure: ${claimError.message}`);
|
|
722
|
-
return null;
|
|
723
|
-
});
|
|
724
|
-
if (reported) {
|
|
725
|
-
await api(`${claimPath}/fail`, {
|
|
726
|
-
method: "POST",
|
|
727
|
-
claim: reported.claimCapability,
|
|
728
|
-
body: { componentId, errorCode: code, errorMessage: message }
|
|
729
|
-
}).catch((reportError) => console.error(`bcms-preview: could not report the failure: ${reportError.message}`));
|
|
940
|
+
if (batch.length > 1) {
|
|
941
|
+
console.log(`bcms-preview: ${result.completed.length} of ${batch.length} components completed, ${result.failed.length} could not be validated`);
|
|
730
942
|
}
|
|
731
|
-
|
|
943
|
+
if (result.exitCode) process.exitCode = 1;
|
|
732
944
|
} finally {
|
|
733
|
-
runtime?.kill();
|
|
734
945
|
rmSync2(work, { recursive: true, force: true });
|
|
735
946
|
}
|
|
736
947
|
}
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +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\n/**\n * `file`: a component module whose props ARE the component's fields — rendered with the props spread.\n * `section`: a section `npx @bettercms-ai/convert --componentize` extracted from a page. Its props are always\n * `{ blockId, bind, overrides, page }`, so it is rendered with the live props as `overrides`.\n * `page`: no file at all — the component is a placement on a page, or a section of the layout. The runtime\n * renders that page and keeps only the component's section (see scope.ts). Derived by the platform, never recorded.\n */\ntype SourceKind = \"file\" | \"section\" | \"page\";\ntype FileSource = { path: string; export?: string; kind?: \"file\" | \"section\" };\ntype PageSource = {\n kind: \"page\";\n route: string;\n blockId?: string;\n groupKey?: string | null;\n source?: Record<string, string> | null;\n layoutSectionId?: string;\n landmark?: \"header\" | \"footer\" | \"nav\" | null;\n bindings?: Record<string, string>;\n};\ntype ManifestEntry = { id: string; source: FileSource | PageSource };\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/** Every section file the codemod writes carries this (`// @bettercms-ai/convert section v2`, earlier `v1`). */\nconst SECTION_MARKER_PREFIX = \"// @bettercms-ai/convert section v\";\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\nconst LANDMARKS = new Set([\"header\", \"footer\", \"nav\"]);\nconst isStringRecord = (value: unknown) =>\n !!value && typeof value === \"object\" && !Array.isArray(value) && Object.values(value).every((v) => typeof v === \"string\");\n\n/** A page source travels into a generated module as data: a same-origin path and plain strings, nothing else. */\nfunction pageSource(id: string, source: PageSource): PageSource {\n const { route, blockId, groupKey, layoutSectionId, landmark } = source;\n if (typeof route !== \"string\" || !route.startsWith(\"/\") || route.startsWith(\"//\") || /[\\s?#\\\\]/.test(route) || route.split(\"/\").includes(\"..\")) {\n fail(`component ${id} has an unsafe page route: ${String(route)}`);\n }\n if (typeof blockId === \"string\" && blockId) {\n if (groupKey != null && typeof groupKey !== \"string\") fail(`component ${id}: groupKey must be a string`);\n if (source.source != null && !isStringRecord(source.source)) fail(`component ${id}: source must map prop keys to page paths`);\n return { kind: \"page\", route, blockId, groupKey: groupKey ?? null, source: source.source ?? null };\n }\n if (typeof layoutSectionId === \"string\" && layoutSectionId) {\n if (landmark != null && !LANDMARKS.has(landmark)) fail(`component ${id}: unknown landmark ${String(landmark)}`);\n if (source.bindings != null && !isStringRecord(source.bindings)) fail(`component ${id}: bindings must map input ids to layout field ids`);\n return { kind: \"page\", route, layoutSectionId, landmark: landmark ?? null, bindings: source.bindings ?? {} };\n }\n fail(`component ${id}: a page source names neither a placement (blockId) nor a layout section (layoutSectionId)`);\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 if (entry.source?.kind === \"page\") return { id: entry.id, source: pageSource(entry.id, entry.source) };\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 // A section is recognised by its own marker as well as by the manifest: a source recorded before kinds\n // existed defaults to `file`, and rendering a section as a file hands it none of its copy.\n const head = readFileSync(join(root, path), \"utf8\").slice(0, 512);\n const kind: SourceKind = entry.source.kind === \"section\" || head.includes(SECTION_MARKER_PREFIX) ? \"section\" : \"file\";\n return { id: entry.id, source: { path, export: named ?? \"default\", kind } };\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 file component, one data entry per page component, keyed by component id. */\nexport function registrySource(fromDir: string, root: string, entries: ManifestEntry[]): string {\n const imports: string[] = [];\n const keys: string[] = [];\n const kinds: string[] = [];\n const pages: string[] = [];\n entries.forEach((entry, index) => {\n const source = entry.source;\n kinds.push(` ${JSON.stringify(entry.id)}: ${JSON.stringify(source.kind ?? \"file\")},`);\n if (source.kind === \"page\") {\n pages.push(` ${JSON.stringify(entry.id)}: ${JSON.stringify(source)},`);\n return;\n }\n let specifier = relative(fromDir, join(root, 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(source.export === undefined || source.export === \"default\"\n ? `import ${local} from ${JSON.stringify(specifier)};`\n : `import { ${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 kinds: Record<string, \"file\" | \"section\" | \"page\"> = {\\n${kinds.join(\"\\n\")}\\n};\\n\\nexport const pages: Record<string, any> = {\\n${pages.join(\"\\n\")}\\n};\\n\\nconst own = (map: object, componentId: string) => Object.prototype.hasOwnProperty.call(map, componentId);\\nexport const has = (componentId: string): boolean => own(registry, componentId) || own(pages, 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 writeFileSync(join(dir, \"scope.ts\"), `export default ${JSON.stringify(readFileSync(join(here, \"scope-client.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 [major, minor] = astroVersion.split(\".\").map(Number);\n // @astrojs/node 11.1.3+ calls `app.getLogger()`, which Astro only has from 7.3 (its peer range still says\n // ^7.2.1), so on 7.0–7.2 the server crashed at startup: \"app.getLogger is not a function\".\n const range = major === 7 && minor! < 3 ? \">=11.0.0 <11.1.3\" : ASTRO_NODE_ADAPTER[String(major)];\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, renderPageSection } from \"./server.mjs\";\nimport { registry, kinds, pages } from \"./registry\";\nimport scope from \"./scope\";\nexport const prerender = false;\nconst headers = renderHeaders();\nconst entry = renderEntry(Astro.url.searchParams.get(\"id\"));\nif (entry && pages[entry.componentId]) return await renderPageSection(Astro.request, entry, pages[entry.componentId], scope);\nconst Component = entry ? registry[entry.componentId] : undefined;\nconst section = entry ? kinds[entry.componentId] === \"section\" : false;\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>{section ? <Component blockId=\"bcms-preview\" overrides={entry.props} page={{}} /> : <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 route(\"render-page\", `import { renderEntry, renderPageSection } from \"../server.mjs\";\\nimport { pages } from \"../registry\";\\nimport scope from \"../scope\";\\nexport function GET(request: Request) {\\n const entry = renderEntry(new URL(request.url).searchParams.get(\"id\"));\\n const page = entry ? pages[entry.componentId] : undefined;\\n if (!entry || !page) return new Response(\"Not found\", { status: 404 });\\n return renderPageSection(request, entry, page, scope);\\n}\\n`);\n mkdirSync(join(gen, \"render\"), { recursive: true });\n writeFileSync(join(gen, \"render\", \"page.tsx\"), `import { notFound, redirect } from \"next/navigation\";\nimport { renderEntry } from \"../server.mjs\";\nimport { registry, kinds, pages } 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 // A page-kind component answers with a whole document, which a page inside the root layout cannot be.\n // Relative on purpose: Next prefixes basePath onto a \"/\"-rooted redirect, and the browser resolves this one.\n if (entry && pages[entry.componentId]) redirect(\\`render-page?id=\\${encodeURIComponent(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 {kinds[entry.componentId] === \"section\"\n ? <Component blockId=\"bcms-preview\" overrides={entry.props} page={{}} />\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 {\n framework,\n out,\n components: entries.map((e) => e.id),\n kinds: Object.fromEntries(entries.map((e) => [e.id, e.source.kind ?? \"file\"])) as Record<string, SourceKind>,\n };\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: {\n id: string;\n /** `{ path, export, kind? }` for a file or section; `{ kind: \"page\", route, … }` for a placement or layout section. */\n source: Record<string, unknown>;\n defaultProps: Record<string, unknown>;\n /** On an explicitly recorded source: the page this component would render from without it. */\n fallback?: { route: string };\n }[];\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[], options: { section: boolean }) {\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 } else if (options.section && (await page.locator(\"[data-bcms-block]\").count()) === 0) {\n // The codemod stamps data-bcms-block on every section root, and the editor resolves a section's\n // fields by walking up to it: a section without one renders but cannot be edited.\n runtimeErrors += 1;\n problems.push(`${viewport.name}: the section rendered without its [data-bcms-block] root`);\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 // `kinds=page`: this validator builds page-kind entries. A 0.2.0 validator never asks, and never gets one.\n const manifest = await api<Manifest>(`/api/v1/projects/${projectId}/component-preview/manifest?kinds=page`);\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 and this component is neither placed on a page nor used in the layout, so there is nothing to render. Place it, add it to the layout, or record its file.\",\n );\n }\n\n const manifestPath = join(work, \"manifest.json\");\n writeFileSync(manifestPath, JSON.stringify({ components: manifest.components.map(({ id, source, fallback }) => ({ id, source, ...(fallback ? { fallback } : {}) })) }));\n const out = join(work, \"runtime\");\n let built: ReturnType<typeof buildPreviewRuntime>;\n try {\n built = 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\", \"pipe\", \"pipe\"],\n });\n /**\n * The app's own server errors (`TypeError: Cannot read properties of undefined`) are the real reason a\n * render fails — the browser only sees \"500\". Still echoed to the CI log; the first few are kept for the\n * evidence so the panel can say what broke.\n */\n const serverErrors: string[] = [];\n const collect = (stream: NodeJS.WriteStream) => (chunk: Buffer) => {\n stream.write(chunk);\n for (const line of chunk.toString(\"utf8\").split(\"\\n\")) {\n const clean = line.replace(/\\x1b\\[[0-9;]*m/g, \"\").trim();\n if (serverErrors.length < 5 && /\\b(error|exception)\\b/i.test(clean)) serverErrors.push(clean.slice(0, 200));\n }\n };\n runtime.stdout?.on(\"data\", collect(process.stdout));\n runtime.stderr?.on(\"data\", collect(process.stderr));\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 // A page-kind render lifts a section too, and stamps its root.\n { section: built.kinds[componentId] !== \"file\" },\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 // A recorded file that fails while the component also renders from its page: the fix is to drop the file.\n const hint = entry.fallback && built.kinds[componentId] === \"file\"\n ? [`Clear the recorded source: this component renders from page ${entry.fallback.route} without one.`]\n : [];\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 // Only on a failure, and bounded: the server's own error lines first (the real cause), then what the\n // browser saw. The database stores them and the panel shows the first one.\n ...(render.runtimeErrors + render.consoleErrors > 0\n ? { problems: [...hint, ...serverErrors.map((line) => `server: ${line}`), ...render.problems].slice(0, 10).map((line) => line.slice(0, 200)) }\n : {}),\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 [...hint, ...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":"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","names":["require","spawnSync","readFileSync","rmSync","writeFileSync","createRequire","dirname","join","resolve","require","createRequire","join","dirname","spawnSync","writeFileSync","id","readFileSync","rmSync"]}
|
|
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\n/**\n * `file`: a component module whose props ARE the component's fields — rendered with the props spread.\n * `section`: a section `npx @bettercms-ai/convert --componentize` extracted from a page. Its props are always\n * `{ blockId, bind, overrides, page }`, so it is rendered with the live props as `overrides`.\n * `page`: no file at all — the component is a placement on a page, or a section of the layout. The runtime\n * renders that page and keeps only the component's section (see scope.ts). Derived by the platform, never recorded.\n */\ntype SourceKind = \"file\" | \"section\" | \"page\";\ntype FileSource = { path: string; export?: string; kind?: \"file\" | \"section\" };\ntype PageSource = {\n kind: \"page\";\n route: string;\n blockId?: string;\n groupKey?: string | null;\n source?: Record<string, string> | null;\n layoutSectionId?: string;\n landmark?: \"header\" | \"footer\" | \"nav\" | null;\n bindings?: Record<string, string>;\n};\ntype ManifestEntry = { id: string; source: FileSource | PageSource };\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/** Every section file the codemod writes carries this (`// @bettercms-ai/convert section v2`, earlier `v1`). */\nconst SECTION_MARKER_PREFIX = \"// @bettercms-ai/convert section v\";\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\nconst LANDMARKS = new Set([\"header\", \"footer\", \"nav\"]);\nconst isStringRecord = (value: unknown) =>\n !!value && typeof value === \"object\" && !Array.isArray(value) && Object.values(value).every((v) => typeof v === \"string\");\n\n/** A page source travels into a generated module as data: a same-origin path and plain strings, nothing else. */\nfunction pageSource(id: string, source: PageSource): PageSource {\n const { route, blockId, groupKey, layoutSectionId, landmark } = source;\n if (typeof route !== \"string\" || !route.startsWith(\"/\") || route.startsWith(\"//\") || /[\\s?#\\\\]/.test(route) || route.split(\"/\").includes(\"..\")) {\n fail(`component ${id} has an unsafe page route: ${String(route)}`);\n }\n if (typeof blockId === \"string\" && blockId) {\n if (groupKey != null && typeof groupKey !== \"string\") fail(`component ${id}: groupKey must be a string`);\n if (source.source != null && !isStringRecord(source.source)) fail(`component ${id}: source must map prop keys to page paths`);\n return { kind: \"page\", route, blockId, groupKey: groupKey ?? null, source: source.source ?? null };\n }\n if (typeof layoutSectionId === \"string\" && layoutSectionId) {\n if (landmark != null && !LANDMARKS.has(landmark)) fail(`component ${id}: unknown landmark ${String(landmark)}`);\n if (source.bindings != null && !isStringRecord(source.bindings)) fail(`component ${id}: bindings must map input ids to layout field ids`);\n return { kind: \"page\", route, layoutSectionId, landmark: landmark ?? null, bindings: source.bindings ?? {} };\n }\n fail(`component ${id}: a page source names neither a placement (blockId) nor a layout section (layoutSectionId)`);\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 if (entry.source?.kind === \"page\") return { id: entry.id, source: pageSource(entry.id, entry.source) };\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 // A section is recognised by its own marker as well as by the manifest: a source recorded before kinds\n // existed defaults to `file`, and rendering a section as a file hands it none of its copy.\n const head = readFileSync(join(root, path), \"utf8\").slice(0, 512);\n const kind: SourceKind = entry.source.kind === \"section\" || head.includes(SECTION_MARKER_PREFIX) ? \"section\" : \"file\";\n return { id: entry.id, source: { path, export: named ?? \"default\", kind } };\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 file component, one data entry per page component, keyed by component id. */\nexport function registrySource(fromDir: string, root: string, entries: ManifestEntry[]): string {\n const imports: string[] = [];\n const keys: string[] = [];\n const kinds: string[] = [];\n const pages: string[] = [];\n entries.forEach((entry, index) => {\n const source = entry.source;\n kinds.push(` ${JSON.stringify(entry.id)}: ${JSON.stringify(source.kind ?? \"file\")},`);\n if (source.kind === \"page\") {\n pages.push(` ${JSON.stringify(entry.id)}: ${JSON.stringify(source)},`);\n return;\n }\n let specifier = relative(fromDir, join(root, 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(source.export === undefined || source.export === \"default\"\n ? `import ${local} from ${JSON.stringify(specifier)};`\n : `import { ${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 kinds: Record<string, \"file\" | \"section\" | \"page\"> = {\\n${kinds.join(\"\\n\")}\\n};\\n\\nexport const pages: Record<string, any> = {\\n${pages.join(\"\\n\")}\\n};\\n\\nconst own = (map: object, componentId: string) => Object.prototype.hasOwnProperty.call(map, componentId);\\nexport const has = (componentId: string): boolean => own(registry, componentId) || own(pages, 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 writeFileSync(join(dir, \"scope.ts\"), `export default ${JSON.stringify(readFileSync(join(here, \"scope-client.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 [major, minor] = astroVersion.split(\".\").map(Number);\n // @astrojs/node 11.1.3+ calls `app.getLogger()`, which Astro only has from 7.3 (its peer range still says\n // ^7.2.1), so on 7.0–7.2 the server crashed at startup: \"app.getLogger is not a function\".\n const range = major === 7 && minor! < 3 ? \">=11.0.0 <11.1.3\" : ASTRO_NODE_ADAPTER[String(major)];\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, renderPageSection } from \"./server.mjs\";\nimport { registry, kinds, pages } from \"./registry\";\nimport scope from \"./scope\";\nexport const prerender = false;\nconst headers = renderHeaders();\nconst entry = renderEntry(Astro.url.searchParams.get(\"id\"));\nif (entry && pages[entry.componentId]) return await renderPageSection(Astro.request, entry, pages[entry.componentId], scope);\nconst Component = entry ? registry[entry.componentId] : undefined;\nconst section = entry ? kinds[entry.componentId] === \"section\" : false;\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>{section ? <Component blockId=\"bcms-preview\" overrides={entry.props} page={{}} /> : <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 route(\"render-page\", `import { renderEntry, renderPageSection } from \"../server.mjs\";\\nimport { pages } from \"../registry\";\\nimport scope from \"../scope\";\\nexport function GET(request: Request) {\\n const entry = renderEntry(new URL(request.url).searchParams.get(\"id\"));\\n const page = entry ? pages[entry.componentId] : undefined;\\n if (!entry || !page) return new Response(\"Not found\", { status: 404 });\\n return renderPageSection(request, entry, page, scope);\\n}\\n`);\n mkdirSync(join(gen, \"render\"), { recursive: true });\n writeFileSync(join(gen, \"render\", \"page.tsx\"), `import { notFound, redirect } from \"next/navigation\";\nimport { renderEntry } from \"../server.mjs\";\nimport { registry, kinds, pages } 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 // A page-kind component answers with a whole document, which a page inside the root layout cannot be.\n // Relative on purpose: Next prefixes basePath onto a \"/\"-rooted redirect, and the browser resolves this one.\n if (entry && pages[entry.componentId]) redirect(\\`render-page?id=\\${encodeURIComponent(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 {kinds[entry.componentId] === \"section\"\n ? <Component blockId=\"bcms-preview\" overrides={entry.props} page={{}} />\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 {\n framework,\n out,\n components: entries.map((e) => e.id),\n kinds: Object.fromEntries(entries.map((e) => [e.id, e.source.kind ?? \"file\"])) as Record<string, SourceKind>,\n };\n}\n","/**\n * `bcms-preview validate` — validates components in CI with nothing configured in the repository.\n *\n * Run by `.github/workflows/bcms-component-validation.yml`, which BetterCMS commits and dispatches. One run\n * validates one component or a batch of them, and builds once either way: fetch the manifest, build a preview\n * runtime from the app as it is, package and upload that runtime, start it and one browser — then, for each\n * component in turn, render it with its default props at every native viewport, check brand tokens and the\n * console, claim its request 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. In a\n * batch one component's failure is reported for that component and the rest carry on; a failure that\n * prevents every result (the build, the runtime, the upload) is reported for every component.\n */\nimport { spawn, spawnSync } 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 type { Browser } from \"playwright\";\nimport { buildPreviewRuntime, CliFailure } from \"./build\";\n\nconst PREVIEW_ROUTE_BASE = \"/__bettercms/component-preview/__bcms\";\nconst RUNTIME_PATH = `${PREVIEW_ROUTE_BASE}/runtime`;\n\n/** Deadlines, so a hung API, app or browser fails the component it hangs instead of the whole 25-minute job. */\nconst API_TIMEOUT_MS = 60_000;\nconst LOCAL_FETCH_TIMEOUT_MS = 30_000;\nconst HEALTH_PROBE_TIMEOUT_MS = 5_000;\nconst RENDER_TIMEOUT_PER_VIEWPORT_MS = 90_000;\nconst BUNDLE_UPLOAD_TIMEOUT_MS = 5 * 60_000;\n/**\n * Ends well before the workflow's 25-minute job timeout — the clock starts only after checkout, install and `npx`\n * — so components not reached are reported, not killed.\n */\nexport const DEFAULT_BATCH_BUDGET_MS = 15 * 60_000;\n\nexport type Viewport = { name: string; width: number; height: number };\n/** One component to validate: what the dispatch names for it. */\nexport type BatchItem = { requestId: string; componentId: string; familyKey: string; nativeViewports: Viewport[] };\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 Claim = { request: RequestTarget; claimCapability: string };\nexport type Manifest = {\n components: {\n id: string;\n /** `{ path, export, kind? }` for a file or section; `{ kind: \"page\", route, … }` for a placement or layout section. */\n source: Record<string, unknown>;\n defaultProps: Record<string, unknown>;\n /** On an explicitly recorded source: the page this component would render from without it. */\n fallback?: { route: string };\n }[];\n brandTokenNames: string[];\n};\nexport type RenderResult = {\n results: { name: string; width: number; height: number; status: \"baseline-missing\"; candidateDigest: string }[];\n consoleErrors: number;\n runtimeErrors: number;\n missingTokens: string[];\n problems: string[];\n};\nexport type Api = <T>(path: string, init?: { method?: string; body?: unknown; claim?: string }) => Promise<T>;\n/** The built, packaged and started runtime every component in the run renders through. */\nexport type PreparedRuntime = {\n kinds: Record<string, string>;\n bundle: Buffer;\n storeProps(componentId: string, props: Record<string, unknown>, signal: AbortSignal): Promise<string>;\n render(renderId: string, viewports: Viewport[], tokenNames: string[], options: { section: boolean }, signal: AbortSignal): Promise<RenderResult>;\n /** The app server's own error lines so far — a component's are the ones logged while it rendered. */\n serverErrorCount(): number;\n serverErrorsSince(mark: number): string[];\n close(): Promise<void>;\n};\nexport type BatchContext = {\n projectId: string;\n commitSha: string;\n previewOrigin: string;\n run: { providerRunId: string; providerRunAttempt: number; providerRunUrl: string; workflowRef: string };\n};\nexport type BatchDeps = {\n api: Api;\n prepare(manifest: Manifest): Promise<PreparedRuntime>;\n putBundle(uploadUrl: string, bytes: Buffer): Promise<{ ok: boolean; status: number }>;\n log?(line: string): void;\n error?(line: string): void;\n /** Clock, budget and step deadlines; defaults are the constants above. Overridden by tests. */\n now?(): number;\n budgetMs?: number;\n timeouts?: { propsMs?: number; renderPerViewportMs?: number; apiMs?: number };\n};\n\n/** Runs `run` with a deadline: past it the signal aborts and the step fails with a message naming it. */\nasync function bounded<T>(what: string, ms: number, run: (signal: AbortSignal) => Promise<T>): Promise<T> {\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n const expired = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n controller.abort();\n const limit = ms >= 1000 ? `${Math.round(ms / 1000)} seconds` : `${ms} ms`;\n reject(new ValidationFailure(\"COMPONENT_VALIDATION_STEP_TIMEOUT\", `${what} did not finish within ${limit}.`));\n }, ms);\n });\n try {\n return await Promise.race([run(controller.signal), expired]);\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport class ValidationFailure extends Error {\n constructor(readonly code: string, message: string) {\n super(message);\n this.name = \"ValidationFailure\";\n }\n}\n\nfunction required(name: string, env: Record<string, string | undefined> = process.env): string {\n const value = 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 isViewport = (value: unknown): value is Viewport =>\n !!value && typeof value === \"object\"\n && typeof (value as Viewport).name === \"string\"\n && Number.isFinite((value as Viewport).width)\n && Number.isFinite((value as Viewport).height);\n\nconst isBatchItem = (value: unknown): value is BatchItem => {\n if (!value || typeof value !== \"object\") return false;\n const item = value as Record<string, unknown>;\n return [\"requestId\", \"componentId\", \"familyKey\"].every((key) => typeof item[key] === \"string\" && (item[key] as string).trim() !== \"\")\n && Array.isArray(item.nativeViewports) && item.nativeViewports.length > 0 && item.nativeViewports.every(isViewport);\n};\n\nconst invalidBatch = (detail: string) => new CliFailure(`COMPONENT_VALIDATION_BATCH_INVALID: ${detail}`);\n\n/**\n * What this run validates. `BCMS_BATCH` (the dispatch's `batch`, JSON) names several components; a dispatch\n * without one — `toJSON` of an absent key is `null` — names one through the single-component variables, which is\n * every dispatch a BetterCMS backend made before batches existed.\n */\nexport function resolveBatch(env: Record<string, string | undefined> = process.env): BatchItem[] {\n const raw = env.BCMS_BATCH?.trim();\n if (raw && raw !== \"null\") {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw invalidBatch(\"BCMS_BATCH is not valid JSON.\");\n }\n if (!Array.isArray(parsed) || parsed.length === 0 || !parsed.every(isBatchItem)) {\n throw invalidBatch(\"BCMS_BATCH must be a non-empty array of { requestId, componentId, familyKey, nativeViewports }.\");\n }\n if (new Set(parsed.map((item) => item.requestId)).size !== parsed.length) {\n throw invalidBatch(\"BCMS_BATCH names the same request twice.\");\n }\n return parsed;\n }\n const requestId = required(\"BCMS_REQUEST_ID\", env);\n const componentId = required(\"BCMS_COMPONENT_ID\", env);\n const familyKey = required(\"BCMS_FAMILY_KEY\", env);\n let nativeViewports: unknown;\n try {\n nativeViewports = JSON.parse(required(\"BCMS_NATIVE_VIEWPORTS\", env));\n } catch (error) {\n if (error instanceof CliFailure) throw error;\n throw invalidBatch(\"BCMS_NATIVE_VIEWPORTS is not valid JSON.\");\n }\n const item = { requestId, componentId, familyKey, nativeViewports };\n if (!isBatchItem(item)) throw invalidBatch(\"BCMS_NATIVE_VIEWPORTS must be a non-empty array of { name, width, height }.\");\n return [item];\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\nconst toCliFailure = (error: unknown) =>\n new CliFailure(`${error instanceof ValidationFailure ? error.code : \"COMPONENT_VALIDATION_FAILED\"}: ${(error as Error).message.slice(0, 2000)}`);\n\n/**\n * Validates every component in `batch` against one build. Returns which components were reported as completed\n * and which failed; `exitCode` is 1 when any component could not produce a result (a completion whose checks\n * failed is still a result). Throws only when nothing could be validated at all, after reporting every\n * component's failure.\n */\nexport async function runValidationBatch(batch: BatchItem[], ctx: BatchContext, deps: BatchDeps) {\n const now = deps.now ?? Date.now;\n // Counted from the start, so the build and the upload spend the same budget the job timeout does.\n const deadline = now() + (deps.budgetMs ?? DEFAULT_BATCH_BUDGET_MS);\n const propsMs = deps.timeouts?.propsMs ?? LOCAL_FETCH_TIMEOUT_MS;\n const renderPerViewportMs = deps.timeouts?.renderPerViewportMs ?? RENDER_TIMEOUT_PER_VIEWPORT_MS;\n const apiMs = deps.timeouts?.apiMs ?? API_TIMEOUT_MS;\n const seconds = (ms: number) => `${Math.ceil(Math.max(ms, 0) / 1000)}s`;\n const log = deps.log ?? ((line: string) => console.log(line));\n const logError = deps.error ?? ((line: string) => console.error(line));\n const requestPath = (item: BatchItem) =>\n `/api/v1/projects/${ctx.projectId}/component-implementation/implementation-requests/${item.requestId}`;\n const completed: string[] = [];\n const failed: string[] = [];\n\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; each component's claim is followed only by its completion.\n */\n const claim = (item: BatchItem) => deps.api<Claim>(`${requestPath(item)}/claim`, {\n method: \"POST\",\n body: {\n componentId: item.componentId,\n commitSha: ctx.commitSha,\n adapter: {\n protocol: \"bcms-component-runtime-v1\",\n kind: \"project-route\",\n path: RUNTIME_PATH,\n familyKey: item.familyKey,\n previewOrigin: ctx.previewOrigin,\n nativeViewports: item.nativeViewports,\n },\n providerRunId: ctx.run.providerRunId,\n providerRunAttempt: ctx.run.providerRunAttempt,\n providerRunUrl: ctx.run.providerRunUrl,\n workflowRef: ctx.run.workflowRef,\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\n // A failure before the claim is still reported: claim, then fail at once, so the dashboard names the reason\n // instead of showing \"running\" until the request expires.\n const report = async (item: BatchItem, held: Claim | null, error: unknown) => {\n const code = error instanceof ValidationFailure ? error.code : \"COMPONENT_VALIDATION_FAILED\";\n const message = (error as Error).message.slice(0, 2000);\n failed.push(item.componentId);\n logError(`bcms-preview: ${code}: ${message} (component ${item.componentId})`);\n const reported = held ?? await claim(item).catch((claimError) => {\n logError(`bcms-preview: could not claim request ${item.requestId} to report the failure: ${(claimError as Error).message}`);\n return null;\n });\n if (!reported) return;\n await deps.api(`${requestPath(item)}/fail`, {\n method: \"POST\",\n claim: reported.claimCapability,\n body: { componentId: item.componentId, errorCode: code, errorMessage: message },\n }).catch((reportError) => logError(`bcms-preview: could not report the failure: ${(reportError as Error).message}`));\n };\n\n let manifest: Manifest;\n try {\n // `kinds=page`: this validator builds page-kind entries. A 0.2.0 validator never asks, and never gets one.\n manifest = await deps.api<Manifest>(`/api/v1/projects/${ctx.projectId}/component-preview/manifest?kinds=page`);\n } catch (error) {\n for (const item of batch) await report(item, null, error);\n throw toCliFailure(error);\n }\n\n // A recorded file that fails while the component also renders from its page: the fix is to drop the file.\n // Known before the build, so a file that does not even build carries it too.\n const hintFor = (componentId: string) => {\n const entry = manifest.components.find((c) => c.id === componentId);\n return entry?.fallback && (entry.source as { kind?: unknown }).kind !== \"page\"\n ? [`Clear the recorded source: this component renders from page ${entry.fallback.route} without one.`]\n : [];\n };\n\n const pending: BatchItem[] = [];\n for (const item of batch) {\n if (manifest.components.some((c) => c.id === item.componentId)) {\n pending.push(item);\n } else {\n await report(item, null, new ValidationFailure(\n \"COMPONENT_SOURCE_NOT_RECORDED\",\n \"No file is recorded and this component is neither placed on a page nor used in the layout, so there is nothing to render. Place it, add it to the layout, or record its file.\",\n ));\n }\n }\n if (pending.length === 0) return { completed, failed, exitCode: 1 as const };\n\n let runtime: PreparedRuntime | null = null;\n try {\n runtime = await deps.prepare(manifest);\n // Uploaded before any claim: the upload needs only the key, and validated evidence pointing at a runtime\n // nobody can start renders nothing.\n const upload = await deps.api<{ uploadUrl: string; uploadKey: string }>(`/api/v1/projects/${ctx.projectId}/artifacts/upload-url`, { method: \"POST\" });\n const put = await deps.putBundle(upload.uploadUrl, runtime.bundle);\n if (!put.ok) {\n throw new ValidationFailure(\"COMPONENT_PREVIEW_BUNDLE_UPLOAD_FAILED\", `Storage refused the preview bundle (HTTP ${put.status}).`);\n }\n await deps.api(`/api/v1/projects/${ctx.projectId}/component-preview/bundles`, {\n method: \"POST\",\n body: { commitSha: ctx.commitSha, uploadKey: upload.uploadKey, checksum: `sha256:${sha256(runtime.bundle)}`, sizeBytes: runtime.bundle.byteLength },\n });\n } catch (error) {\n await runtime?.close().catch(() => {});\n for (const item of pending) {\n const hint = error instanceof ValidationFailure && error.code === \"COMPONENT_PREVIEW_BUILD_FAILED\" ? hintFor(item.componentId) : [];\n await report(item, null, hint.length ? new ValidationFailure((error as ValidationFailure).code, [(error as Error).message, ...hint].join(\" \")) : error);\n }\n throw toCliFailure(error);\n }\n\n const ready = runtime;\n try {\n for (const item of pending) {\n // Started only when this component's own worst case fits: props, every viewport's render, a claim and a\n // completion. One that does not fit is reported now; a smaller one after it may still fit.\n const worstCase = propsMs + renderPerViewportMs * item.nativeViewports.length + 2 * apiMs;\n const left = deadline - now();\n if (left < worstCase) {\n await report(item, null, new ValidationFailure(\n \"COMPONENT_VALIDATION_BATCH_TIMEOUT\",\n `The validation batch did not have enough time left for this component (needs up to ${seconds(worstCase)}, ${seconds(left)} left). Request validation again.`,\n ));\n continue;\n }\n const entry = manifest.components.find((c) => c.id === item.componentId)!;\n const hint = hintFor(item.componentId);\n let held: Claim | null = null;\n try {\n const mark = ready.serverErrorCount();\n const renderId = await bounded(\n `The preview runtime's props endpoint for ${item.componentId}`,\n propsMs,\n (signal) => ready.storeProps(item.componentId, entry.defaultProps, signal),\n );\n // The viewports the dispatch declared are exactly the ones the claim records as the adapter's.\n const render = await bounded(\n `Rendering ${item.componentId}`,\n renderPerViewportMs * item.nativeViewports.length,\n (signal) => ready.render(\n renderId,\n item.nativeViewports,\n manifest.brandTokenNames,\n // A page-kind render lifts a section too, and stamps its root.\n { section: ready.kinds[item.componentId] !== \"file\" },\n signal,\n ),\n );\n /**\n * The app's own server errors (`TypeError: Cannot read properties of undefined`) are the real reason a\n * render fails — the browser only sees \"500\". The first few logged while this component rendered are\n * kept for its evidence so the panel can say what broke.\n */\n const serverErrors = ready.serverErrorsSince(mark).slice(0, 5);\n\n held = await claim(item);\n const target = held.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 // Only on a failure, and bounded: the server's own error lines first (the real cause), then what the\n // browser saw. The database stores them and the panel shows the first one.\n ...(render.runtimeErrors + render.consoleErrors > 0\n ? { problems: [...hint, ...serverErrors.map((line) => `server: ${line}`), ...render.problems].slice(0, 10).map((line) => line.slice(0, 200)) }\n : {}),\n },\n visual: { status: \"baseline-missing\", reviewRequired: true, viewports: render.results },\n };\n\n const evidenceDigest = `sha256:${sha256(canonical({ requestId: item.requestId, commitSha: target.commitSha, checks }))}`;\n await deps.api(`${requestPath(item)}/complete`, {\n method: \"POST\",\n claim: held.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 completed.push(item.componentId);\n\n const passed = checks.brandKit.status === \"passed\" && checks.runtime.status === \"passed\";\n log(`bcms-preview: validation ${passed ? \"PASSED\" : \"FAILED\"} for ${item.componentId}`);\n if (!passed) {\n if (render.missingTokens.length) log(` brand tokens used but not defined: ${render.missingTokens.join(\", \")}`);\n for (const problem of [...hint, ...render.problems]) log(` ${problem}`);\n }\n } catch (error) {\n await report(item, held, error);\n }\n }\n } finally {\n await ready.close().catch(() => {});\n }\n return { completed, failed, exitCode: failed.length > 0 ? (1 as const) : (0 as const) };\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, { signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS) })).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\nexport async function renderInBrowser(browser: Browser, url: string, viewports: Viewport[], tokenNames: string[], options: { section: boolean }, signal?: AbortSignal): Promise<RenderResult> {\n let consoleErrors = 0;\n let runtimeErrors = 0;\n const missingTokens = new Set<string>();\n const results: RenderResult[\"results\"] = [];\n const problems: string[] = [];\n for (const viewport of viewports) {\n // Past its deadline the step has already failed; do not keep opening pages for it.\n if (signal?.aborted) break;\n const page = await browser.newPage({ viewport: { width: viewport.width, height: viewport.height } });\n // 🔴 A page stuck in goto/evaluate/screenshot past its deadline keeps running in the shared browser, and the\n // app errors it causes would land in the NEXT component's evidence. Closing it ends the stuck call.\n const onAbort = () => {\n page.close().catch(() => {});\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n if (signal?.aborted) onAbort();\n try {\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 } else if (options.section && (await page.locator(\"[data-bcms-block]\").count()) === 0) {\n // The codemod stamps data-bcms-block on every section root, and the editor resolves a section's\n // fields by walking up to it: a section without one renders but cannot be edited.\n runtimeErrors += 1;\n problems.push(`${viewport.name}: the section rendered without its [data-bcms-block] root`);\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 } finally {\n signal?.removeEventListener(\"abort\", onAbort);\n await page.close().catch(() => {});\n }\n }\n return { results, consoleErrors, runtimeErrors, missingTokens: [...missingTokens].sort(), problems };\n}\n\nexport async function validateComponent() {\n const apiUrl = required(\"BCMS_API_URL\");\n const apiKey = required(\"BCMS_API_KEY\");\n const batch = resolveBatch();\n const ctx: BatchContext = {\n projectId: required(\"BCMS_PROJECT_ID\"),\n commitSha: required(\"BCMS_COMMIT_SHA\"),\n previewOrigin: required(\"BCMS_PREVIEW_ORIGIN\"),\n run: {\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 },\n };\n\n const api: Api = async <T>(path: string, init: { method?: string; body?: unknown; claim?: string } = {}): Promise<T> => {\n let response: Response;\n let text: string;\n try {\n 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 signal: AbortSignal.timeout(API_TIMEOUT_MS),\n });\n text = await response.text();\n } catch (error) {\n const name = (error as Error).name;\n if (name === \"TimeoutError\" || name === \"AbortError\") {\n throw new ValidationFailure(\"COMPONENT_VALIDATION_API_TIMEOUT\", `${init.method ?? \"GET\"} ${path} did not answer within ${API_TIMEOUT_MS / 1000} seconds.`);\n }\n throw error;\n }\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 work = mkdtempSync(join(tmpdir(), \"bcms-validate-\"));\n\n const prepare = async (manifest: Manifest): Promise<PreparedRuntime> => {\n const manifestPath = join(work, \"manifest.json\");\n writeFileSync(manifestPath, JSON.stringify({ components: manifest.components.map(({ id, source, fallback }) => ({ id, source, ...(fallback ? { fallback } : {}) })) }));\n const out = join(work, \"runtime\");\n let built: ReturnType<typeof buildPreviewRuntime>;\n try {\n built = 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 // Packed before the runtime starts, so the bundle is exactly the tree that was built.\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 bundle = readFileSync(tarball);\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 const child = 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\", \"pipe\", \"pipe\"],\n });\n // Still echoed to the CI log; every line that looks like an error is kept (bounded) so each component's\n // evidence can carry the ones logged while it rendered.\n const serverErrors: string[] = [];\n const collect = (stream: NodeJS.WriteStream) => (chunk: Buffer) => {\n stream.write(chunk);\n for (const line of chunk.toString(\"utf8\").split(\"\\n\")) {\n const clean = line.replace(/\\x1b\\[[0-9;]*m/g, \"\").trim();\n if (serverErrors.length < 500 && /\\b(error|exception)\\b/i.test(clean)) serverErrors.push(clean.slice(0, 200));\n }\n };\n child.stdout?.on(\"data\", collect(process.stdout));\n child.stderr?.on(\"data\", collect(process.stderr));\n\n let browser: Browser | null = null;\n try {\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 ensureBrowser();\n const { chromium } = await import(\"playwright\");\n browser = await chromium.launch();\n } catch (error) {\n child.kill();\n throw error;\n }\n const shared = browser;\n\n return {\n kinds: built.kinds,\n bundle,\n storeProps: async (componentId, props, signal) => {\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 }),\n signal,\n });\n if (!stored.ok) {\n throw new ValidationFailure(\"COMPONENT_PREVIEW_RUNTIME_REFUSED\", `The preview runtime refused the component (HTTP ${stored.status}).`);\n }\n return ((await stored.json()) as { id: string }).id;\n },\n render: (renderId, viewports, tokenNames, options, signal) =>\n renderInBrowser(shared, `${local}${PREVIEW_ROUTE_BASE}/render?id=${encodeURIComponent(renderId)}`, viewports, tokenNames, options, signal),\n serverErrorCount: () => serverErrors.length,\n serverErrorsSince: (mark) => serverErrors.slice(mark),\n close: async () => {\n child.kill();\n await shared.close().catch(() => {});\n },\n };\n };\n\n const budget = Number(process.env.BCMS_BATCH_BUDGET_MS);\n try {\n const result = await runValidationBatch(batch, ctx, {\n api,\n prepare,\n putBundle: async (uploadUrl, bytes) => {\n try {\n const put = await fetch(uploadUrl, {\n method: \"PUT\",\n body: new Uint8Array(bytes),\n headers: { \"content-type\": \"application/gzip\" },\n signal: AbortSignal.timeout(BUNDLE_UPLOAD_TIMEOUT_MS),\n });\n return { ok: put.ok, status: put.status };\n } catch (error) {\n const name = (error as Error).name;\n if (name !== \"TimeoutError\" && name !== \"AbortError\") throw error;\n throw new ValidationFailure(\"COMPONENT_PREVIEW_BUNDLE_UPLOAD_FAILED\", `Storage did not accept the preview bundle within ${BUNDLE_UPLOAD_TIMEOUT_MS / 60_000} minutes.`);\n }\n },\n budgetMs: Number.isFinite(budget) && budget > 0 ? budget : DEFAULT_BATCH_BUDGET_MS,\n });\n if (batch.length > 1) {\n console.log(`bcms-preview: ${result.completed.length} of ${batch.length} components completed, ${result.failed.length} could not be validated`);\n }\n if (result.exitCode) process.exitCode = 1;\n } finally {\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;AAyB9B,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;AAEnB,IAAM,wBAAwB;AAOvB,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;AAEA,IAAM,YAAY,oBAAI,IAAI,CAAC,UAAU,UAAU,KAAK,CAAC;AACrD,IAAM,iBAAiB,CAAC,UACtB,CAAC,CAAC,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,KAAK,OAAO,OAAO,KAAK,EAAE,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ;AAG1H,SAAS,WAAW,IAAY,QAAgC;AAC9D,QAAM,EAAE,OAAO,SAAS,UAAU,iBAAiB,SAAS,IAAI;AAChE,MAAI,OAAO,UAAU,YAAY,CAAC,MAAM,WAAW,GAAG,KAAK,MAAM,WAAW,IAAI,KAAK,WAAW,KAAK,KAAK,KAAK,MAAM,MAAM,GAAG,EAAE,SAAS,IAAI,GAAG;AAC9I,SAAK,aAAa,EAAE,8BAA8B,OAAO,KAAK,CAAC,EAAE;AAAA,EACnE;AACA,MAAI,OAAO,YAAY,YAAY,SAAS;AAC1C,QAAI,YAAY,QAAQ,OAAO,aAAa,SAAU,MAAK,aAAa,EAAE,6BAA6B;AACvG,QAAI,OAAO,UAAU,QAAQ,CAAC,eAAe,OAAO,MAAM,EAAG,MAAK,aAAa,EAAE,2CAA2C;AAC5H,WAAO,EAAE,MAAM,QAAQ,OAAO,SAAS,UAAU,YAAY,MAAM,QAAQ,OAAO,UAAU,KAAK;AAAA,EACnG;AACA,MAAI,OAAO,oBAAoB,YAAY,iBAAiB;AAC1D,QAAI,YAAY,QAAQ,CAAC,UAAU,IAAI,QAAQ,EAAG,MAAK,aAAa,EAAE,sBAAsB,OAAO,QAAQ,CAAC,EAAE;AAC9G,QAAI,OAAO,YAAY,QAAQ,CAAC,eAAe,OAAO,QAAQ,EAAG,MAAK,aAAa,EAAE,mDAAmD;AACxI,WAAO,EAAE,MAAM,QAAQ,OAAO,iBAAiB,UAAU,YAAY,MAAM,UAAU,OAAO,YAAY,CAAC,EAAE;AAAA,EAC7G;AACA,OAAK,aAAa,EAAE,4FAA4F;AAClH;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,QAAI,MAAM,QAAQ,SAAS,OAAQ,QAAO,EAAE,IAAI,MAAM,IAAI,QAAQ,WAAW,MAAM,IAAI,MAAM,MAAM,EAAE;AACrG,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;AAGA,UAAM,OAAO,aAAa,KAAK,MAAM,IAAI,GAAG,MAAM,EAAE,MAAM,GAAG,GAAG;AAChE,UAAM,OAAmB,MAAM,OAAO,SAAS,aAAa,KAAK,SAAS,qBAAqB,IAAI,YAAY;AAC/G,WAAO,EAAE,IAAI,MAAM,IAAI,QAAQ,EAAE,MAAM,QAAQ,SAAS,WAAW,KAAK,EAAE;AAAA,EAC5E,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;AAGO,SAAS,eAAe,SAAiB,MAAc,SAAkC;AAC9F,QAAM,UAAoB,CAAC;AAC3B,QAAM,OAAiB,CAAC;AACxB,QAAM,QAAkB,CAAC;AACzB,QAAM,QAAkB,CAAC;AACzB,UAAQ,QAAQ,CAAC,OAAO,UAAU;AAChC,UAAM,SAAS,MAAM;AACrB,UAAM,KAAK,KAAK,KAAK,UAAU,MAAM,EAAE,CAAC,KAAK,KAAK,UAAU,OAAO,QAAQ,MAAM,CAAC,GAAG;AACrF,QAAI,OAAO,SAAS,QAAQ;AAC1B,YAAM,KAAK,KAAK,KAAK,UAAU,MAAM,EAAE,CAAC,KAAK,KAAK,UAAU,MAAM,CAAC,GAAG;AACtE;AAAA,IACF;AACA,QAAI,YAAY,SAAS,SAAS,KAAK,MAAM,OAAO,IAAI,CAAC,EAAE,MAAM,GAAG,EAAE,KAAK,GAAG;AAC9E,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,OAAO,WAAW,UAAa,OAAO,WAAW,YAC1D,UAAU,KAAK,SAAS,KAAK,UAAU,SAAS,CAAC,MACjD,YAAY,OAAO,MAAM,OAAO,KAAK,WAAW,KAAK,UAAU,SAAS,CAAC,GAAG;AAChF,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,EAAgF,MAAM,KAAK,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA,EAAwD,MAAM,KAAK,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAC5Q;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;AAC/H,gBAAc,KAAK,KAAK,UAAU,GAAG,kBAAkB,KAAK,UAAU,aAAa,KAAK,MAAM,wBAAwB,GAAG,MAAM,CAAC,CAAC;AAAA,CAAK;AACxI;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,CAAC,OAAO,KAAK,IAAI,aAAa,MAAM,GAAG,EAAE,IAAI,MAAM;AAGzD,QAAM,QAAQ,UAAU,KAAK,QAAS,IAAI,qBAAqB,mBAAmB,OAAO,KAAK,CAAC;AAC/F,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;AAAA;AAAA;AAAA,CAiBP;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,UAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAkc;AACvd,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;AAAA;AAAA;AAAA;AAAA;AAAA,CAwBlD;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;AAAA,IACL;AAAA,IACA;AAAA,IACA,YAAY,QAAQ,IAAI,CAAC,MAAM,EAAE,EAAE;AAAA,IACnC,OAAO,OAAO,YAAY,QAAQ,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,QAAQ,MAAM,CAAC,CAAC;AAAA,EAC/E;AACF;;;AC9ZA,SAAS,OAAO,aAAAC,kBAAiB;AACjC,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;AAI9B,IAAM,qBAAqB;AAC3B,IAAM,eAAe,GAAG,kBAAkB;AAG1C,IAAM,iBAAiB;AACvB,IAAM,yBAAyB;AAC/B,IAAM,0BAA0B;AAChC,IAAM,iCAAiC;AACvC,IAAM,2BAA2B,IAAI;AAK9B,IAAM,0BAA0B,KAAK;AAqE5C,eAAe,QAAW,MAAc,IAAYC,MAAsD;AACxG,QAAM,aAAa,IAAI,gBAAgB;AACvC,MAAI;AACJ,QAAM,UAAU,IAAI,QAAe,CAAC,GAAG,WAAW;AAChD,YAAQ,WAAW,MAAM;AACvB,iBAAW,MAAM;AACjB,YAAM,QAAQ,MAAM,MAAO,GAAG,KAAK,MAAM,KAAK,GAAI,CAAC,aAAa,GAAG,EAAE;AACrE,aAAO,IAAI,kBAAkB,qCAAqC,GAAG,IAAI,0BAA0B,KAAK,GAAG,CAAC;AAAA,IAC9G,GAAG,EAAE;AAAA,EACP,CAAC;AACD,MAAI;AACF,WAAO,MAAM,QAAQ,KAAK,CAACA,KAAI,WAAW,MAAM,GAAG,OAAO,CAAC;AAAA,EAC7D,UAAE;AACA,iBAAa,KAAK;AAAA,EACpB;AACF;AAEO,IAAM,oBAAN,cAAgC,MAAM;AAAA,EAC3C,YAAqB,MAAc,SAAiB;AAClD,UAAM,OAAO;AADM;AAEnB,SAAK,OAAO;AAAA,EACd;AAAA,EAHqB;AAIvB;AAEA,SAAS,SAAS,MAAc,MAA0C,QAAQ,KAAa;AAC7F,QAAM,QAAQ,IAAI,IAAI,GAAG,KAAK;AAC9B,MAAI,CAAC,MAAO,OAAM,IAAI,WAAW,GAAG,IAAI,oFAAoF;AAC5H,SAAO;AACT;AAEA,IAAM,aAAa,CAAC,UAClB,CAAC,CAAC,SAAS,OAAO,UAAU,YACzB,OAAQ,MAAmB,SAAS,YACpC,OAAO,SAAU,MAAmB,KAAK,KACzC,OAAO,SAAU,MAAmB,MAAM;AAE/C,IAAM,cAAc,CAAC,UAAuC;AAC1D,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,OAAO;AACb,SAAO,CAAC,aAAa,eAAe,WAAW,EAAE,MAAM,CAAC,QAAQ,OAAO,KAAK,GAAG,MAAM,YAAa,KAAK,GAAG,EAAa,KAAK,MAAM,EAAE,KAC/H,MAAM,QAAQ,KAAK,eAAe,KAAK,KAAK,gBAAgB,SAAS,KAAK,KAAK,gBAAgB,MAAM,UAAU;AACtH;AAEA,IAAM,eAAe,CAAC,WAAmB,IAAI,WAAW,uCAAuC,MAAM,EAAE;AAOhG,SAAS,aAAa,MAA0C,QAAQ,KAAkB;AAC/F,QAAM,MAAM,IAAI,YAAY,KAAK;AACjC,MAAI,OAAO,QAAQ,QAAQ;AACzB,QAAI;AACJ,QAAI;AACF,eAAS,KAAK,MAAM,GAAG;AAAA,IACzB,QAAQ;AACN,YAAM,aAAa,+BAA+B;AAAA,IACpD;AACA,QAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,OAAO,WAAW,KAAK,CAAC,OAAO,MAAM,WAAW,GAAG;AAC/E,YAAM,aAAa,iGAAiG;AAAA,IACtH;AACA,QAAI,IAAI,IAAI,OAAO,IAAI,CAACC,UAASA,MAAK,SAAS,CAAC,EAAE,SAAS,OAAO,QAAQ;AACxE,YAAM,aAAa,0CAA0C;AAAA,IAC/D;AACA,WAAO;AAAA,EACT;AACA,QAAM,YAAY,SAAS,mBAAmB,GAAG;AACjD,QAAM,cAAc,SAAS,qBAAqB,GAAG;AACrD,QAAM,YAAY,SAAS,mBAAmB,GAAG;AACjD,MAAI;AACJ,MAAI;AACF,sBAAkB,KAAK,MAAM,SAAS,yBAAyB,GAAG,CAAC;AAAA,EACrE,SAAS,OAAO;AACd,QAAI,iBAAiB,WAAY,OAAM;AACvC,UAAM,aAAa,0CAA0C;AAAA,EAC/D;AACA,QAAM,OAAO,EAAE,WAAW,aAAa,WAAW,gBAAgB;AAClE,MAAI,CAAC,YAAY,IAAI,EAAG,OAAM,aAAa,6EAA6E;AACxH,SAAO,CAAC,IAAI;AACd;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,IAAM,eAAe,CAAC,UACpB,IAAI,WAAW,GAAG,iBAAiB,oBAAoB,MAAM,OAAO,6BAA6B,KAAM,MAAgB,QAAQ,MAAM,GAAG,GAAI,CAAC,EAAE;AAQjJ,eAAsB,mBAAmB,OAAoB,KAAmB,MAAiB;AAC/F,QAAM,MAAM,KAAK,OAAO,KAAK;AAE7B,QAAM,WAAW,IAAI,KAAK,KAAK,YAAY;AAC3C,QAAM,UAAU,KAAK,UAAU,WAAW;AAC1C,QAAM,sBAAsB,KAAK,UAAU,uBAAuB;AAClE,QAAM,QAAQ,KAAK,UAAU,SAAS;AACtC,QAAM,UAAU,CAAC,OAAe,GAAG,KAAK,KAAK,KAAK,IAAI,IAAI,CAAC,IAAI,GAAI,CAAC;AACpE,QAAM,MAAM,KAAK,QAAQ,CAAC,SAAiB,QAAQ,IAAI,IAAI;AAC3D,QAAM,WAAW,KAAK,UAAU,CAAC,SAAiB,QAAQ,MAAM,IAAI;AACpE,QAAM,cAAc,CAAC,SACnB,oBAAoB,IAAI,SAAS,qDAAqD,KAAK,SAAS;AACtG,QAAM,YAAsB,CAAC;AAC7B,QAAM,SAAmB,CAAC;AAQ1B,QAAM,QAAQ,CAAC,SAAoB,KAAK,IAAW,GAAG,YAAY,IAAI,CAAC,UAAU;AAAA,IAC/E,QAAQ;AAAA,IACR,MAAM;AAAA,MACJ,aAAa,KAAK;AAAA,MAClB,WAAW,IAAI;AAAA,MACf,SAAS;AAAA,QACP,UAAU;AAAA,QACV,MAAM;AAAA,QACN,MAAM;AAAA,QACN,WAAW,KAAK;AAAA,QAChB,eAAe,IAAI;AAAA,QACnB,iBAAiB,KAAK;AAAA,MACxB;AAAA,MACA,eAAe,IAAI,IAAI;AAAA,MACvB,oBAAoB,IAAI,IAAI;AAAA,MAC5B,gBAAgB,IAAI,IAAI;AAAA,MACxB,aAAa,IAAI,IAAI;AAAA;AAAA,MAErB,qBAAqB,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,MAAS,GAAK,EAAE,YAAY;AAAA,IAC9E;AAAA,EACF,CAAC;AAID,QAAM,SAAS,OAAO,MAAiB,MAAoB,UAAmB;AAC5E,UAAM,OAAO,iBAAiB,oBAAoB,MAAM,OAAO;AAC/D,UAAM,UAAW,MAAgB,QAAQ,MAAM,GAAG,GAAI;AACtD,WAAO,KAAK,KAAK,WAAW;AAC5B,aAAS,iBAAiB,IAAI,KAAK,OAAO,eAAe,KAAK,WAAW,GAAG;AAC5E,UAAM,WAAW,QAAQ,MAAM,MAAM,IAAI,EAAE,MAAM,CAAC,eAAe;AAC/D,eAAS,yCAAyC,KAAK,SAAS,2BAA4B,WAAqB,OAAO,EAAE;AAC1H,aAAO;AAAA,IACT,CAAC;AACD,QAAI,CAAC,SAAU;AACf,UAAM,KAAK,IAAI,GAAG,YAAY,IAAI,CAAC,SAAS;AAAA,MAC1C,QAAQ;AAAA,MACR,OAAO,SAAS;AAAA,MAChB,MAAM,EAAE,aAAa,KAAK,aAAa,WAAW,MAAM,cAAc,QAAQ;AAAA,IAChF,CAAC,EAAE,MAAM,CAAC,gBAAgB,SAAS,+CAAgD,YAAsB,OAAO,EAAE,CAAC;AAAA,EACrH;AAEA,MAAI;AACJ,MAAI;AAEF,eAAW,MAAM,KAAK,IAAc,oBAAoB,IAAI,SAAS,wCAAwC;AAAA,EAC/G,SAAS,OAAO;AACd,eAAW,QAAQ,MAAO,OAAM,OAAO,MAAM,MAAM,KAAK;AACxD,UAAM,aAAa,KAAK;AAAA,EAC1B;AAIA,QAAM,UAAU,CAAC,gBAAwB;AACvC,UAAM,QAAQ,SAAS,WAAW,KAAK,CAAC,MAAM,EAAE,OAAO,WAAW;AAClE,WAAO,OAAO,YAAa,MAAM,OAA8B,SAAS,SACpE,CAAC,+DAA+D,MAAM,SAAS,KAAK,eAAe,IACnG,CAAC;AAAA,EACP;AAEA,QAAM,UAAuB,CAAC;AAC9B,aAAW,QAAQ,OAAO;AACxB,QAAI,SAAS,WAAW,KAAK,CAAC,MAAM,EAAE,OAAO,KAAK,WAAW,GAAG;AAC9D,cAAQ,KAAK,IAAI;AAAA,IACnB,OAAO;AACL,YAAM,OAAO,MAAM,MAAM,IAAI;AAAA,QAC3B;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACA,MAAI,QAAQ,WAAW,EAAG,QAAO,EAAE,WAAW,QAAQ,UAAU,EAAW;AAE3E,MAAI,UAAkC;AACtC,MAAI;AACF,cAAU,MAAM,KAAK,QAAQ,QAAQ;AAGrC,UAAM,SAAS,MAAM,KAAK,IAA8C,oBAAoB,IAAI,SAAS,yBAAyB,EAAE,QAAQ,OAAO,CAAC;AACpJ,UAAM,MAAM,MAAM,KAAK,UAAU,OAAO,WAAW,QAAQ,MAAM;AACjE,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI,kBAAkB,0CAA0C,4CAA4C,IAAI,MAAM,IAAI;AAAA,IAClI;AACA,UAAM,KAAK,IAAI,oBAAoB,IAAI,SAAS,8BAA8B;AAAA,MAC5E,QAAQ;AAAA,MACR,MAAM,EAAE,WAAW,IAAI,WAAW,WAAW,OAAO,WAAW,UAAU,UAAU,OAAO,QAAQ,MAAM,CAAC,IAAI,WAAW,QAAQ,OAAO,WAAW;AAAA,IACpJ,CAAC;AAAA,EACH,SAAS,OAAO;AACd,UAAM,SAAS,MAAM,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AACrC,eAAW,QAAQ,SAAS;AAC1B,YAAM,OAAO,iBAAiB,qBAAqB,MAAM,SAAS,mCAAmC,QAAQ,KAAK,WAAW,IAAI,CAAC;AAClI,YAAM,OAAO,MAAM,MAAM,KAAK,SAAS,IAAI,kBAAmB,MAA4B,MAAM,CAAE,MAAgB,SAAS,GAAG,IAAI,EAAE,KAAK,GAAG,CAAC,IAAI,KAAK;AAAA,IACxJ;AACA,UAAM,aAAa,KAAK;AAAA,EAC1B;AAEA,QAAM,QAAQ;AACd,MAAI;AACF,eAAW,QAAQ,SAAS;AAG1B,YAAM,YAAY,UAAU,sBAAsB,KAAK,gBAAgB,SAAS,IAAI;AACpF,YAAM,OAAO,WAAW,IAAI;AAC5B,UAAI,OAAO,WAAW;AACpB,cAAM,OAAO,MAAM,MAAM,IAAI;AAAA,UAC3B;AAAA,UACA,sFAAsF,QAAQ,SAAS,CAAC,KAAK,QAAQ,IAAI,CAAC;AAAA,QAC5H,CAAC;AACD;AAAA,MACF;AACA,YAAM,QAAQ,SAAS,WAAW,KAAK,CAAC,MAAM,EAAE,OAAO,KAAK,WAAW;AACvE,YAAM,OAAO,QAAQ,KAAK,WAAW;AACrC,UAAI,OAAqB;AACzB,UAAI;AACF,cAAM,OAAO,MAAM,iBAAiB;AACpC,cAAM,WAAW,MAAM;AAAA,UACrB,4CAA4C,KAAK,WAAW;AAAA,UAC5D;AAAA,UACA,CAAC,WAAW,MAAM,WAAW,KAAK,aAAa,MAAM,cAAc,MAAM;AAAA,QAC3E;AAEA,cAAM,SAAS,MAAM;AAAA,UACnB,aAAa,KAAK,WAAW;AAAA,UAC7B,sBAAsB,KAAK,gBAAgB;AAAA,UAC3C,CAAC,WAAW,MAAM;AAAA,YAChB;AAAA,YACA,KAAK;AAAA,YACL,SAAS;AAAA;AAAA,YAET,EAAE,SAAS,MAAM,MAAM,KAAK,WAAW,MAAM,OAAO;AAAA,YACpD;AAAA,UACF;AAAA,QACF;AAMA,cAAM,eAAe,MAAM,kBAAkB,IAAI,EAAE,MAAM,GAAG,CAAC;AAE7D,eAAO,MAAM,MAAM,IAAI;AACvB,cAAM,SAAS,KAAK;AACpB,cAAM,SAAS;AAAA,UACb,UAAU;AAAA,YACR,QAAQ,OAAO,cAAc,WAAW,IAAI,WAAW;AAAA,YACvD,cAAc,OAAO;AAAA,YACrB,eAAe,OAAO;AAAA,UACxB;AAAA,UACA,SAAS;AAAA,YACP,QAAQ,OAAO,kBAAkB,KAAK,OAAO,kBAAkB,IAAI,WAAW;AAAA,YAC9E,eAAe,OAAO;AAAA,YACtB,eAAe,OAAO;AAAA;AAAA;AAAA,YAGtB,GAAI,OAAO,gBAAgB,OAAO,gBAAgB,IAC9C,EAAE,UAAU,CAAC,GAAG,MAAM,GAAG,aAAa,IAAI,CAAC,SAAS,WAAW,IAAI,EAAE,GAAG,GAAG,OAAO,QAAQ,EAAE,MAAM,GAAG,EAAE,EAAE,IAAI,CAAC,SAAS,KAAK,MAAM,GAAG,GAAG,CAAC,EAAE,IAC3I,CAAC;AAAA,UACP;AAAA,UACA,QAAQ,EAAE,QAAQ,oBAAoB,gBAAgB,MAAM,WAAW,OAAO,QAAQ;AAAA,QACxF;AAEA,cAAM,iBAAiB,UAAU,OAAO,UAAU,EAAE,WAAW,KAAK,WAAW,WAAW,OAAO,WAAW,OAAO,CAAC,CAAC,CAAC;AACtH,cAAM,KAAK,IAAI,GAAG,YAAY,IAAI,CAAC,aAAa;AAAA,UAC9C,QAAQ;AAAA,UACR,OAAO,KAAK;AAAA,UACZ,MAAM;AAAA,YACJ,aAAa,OAAO;AAAA,YACpB,aAAa,OAAO;AAAA,YACpB,kBAAkB,OAAO;AAAA,YACzB,WAAW,OAAO;AAAA,YAClB,oBAAoB,OAAO;AAAA,YAC3B,YAAY,OAAO;AAAA,YACnB,mBAAmB,OAAO;AAAA,YAC1B,kBAAkB,OAAO;AAAA,YACzB,WAAW,OAAO;AAAA,YAClB,aAAa,OAAO;AAAA,YACpB,oBAAoB,OAAO;AAAA,YAC3B,iBAAiB,OAAO;AAAA,YACxB;AAAA,YACA;AAAA,UACF;AAAA,QACF,CAAC;AACD,kBAAU,KAAK,KAAK,WAAW;AAE/B,cAAM,SAAS,OAAO,SAAS,WAAW,YAAY,OAAO,QAAQ,WAAW;AAChF,YAAI,4BAA4B,SAAS,WAAW,QAAQ,QAAQ,KAAK,WAAW,EAAE;AACtF,YAAI,CAAC,QAAQ;AACX,cAAI,OAAO,cAAc,OAAQ,KAAI,wCAAwC,OAAO,cAAc,KAAK,IAAI,CAAC,EAAE;AAC9G,qBAAW,WAAW,CAAC,GAAG,MAAM,GAAG,OAAO,QAAQ,EAAG,KAAI,KAAK,OAAO,EAAE;AAAA,QACzE;AAAA,MACF,SAAS,OAAO;AACd,cAAM,OAAO,MAAM,MAAM,KAAK;AAAA,MAChC;AAAA,IACF;AAAA,EACF,UAAE;AACA,UAAM,MAAM,MAAM,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACpC;AACA,SAAO,EAAE,WAAW,QAAQ,UAAU,OAAO,SAAS,IAAK,IAAe,EAAY;AACxF;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,KAAK,EAAE,QAAQ,YAAY,QAAQ,uBAAuB,EAAE,CAAC,GAAG,GAAI,QAAO;AAAA,IAC9F,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,eAAsB,gBAAgB,SAAkB,KAAa,WAAuB,YAAsB,SAA+B,QAA6C;AAC5L,MAAI,gBAAgB;AACpB,MAAI,gBAAgB;AACpB,QAAM,gBAAgB,oBAAI,IAAY;AACtC,QAAM,UAAmC,CAAC;AAC1C,QAAM,WAAqB,CAAC;AAC5B,aAAW,YAAY,WAAW;AAEhC,QAAI,QAAQ,QAAS;AACrB,UAAM,OAAO,MAAM,QAAQ,QAAQ,EAAE,UAAU,EAAE,OAAO,SAAS,OAAO,QAAQ,SAAS,OAAO,EAAE,CAAC;AAGnG,UAAM,UAAU,MAAM;AACpB,WAAK,MAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IAC7B;AACA,YAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AACzD,QAAI,QAAQ,QAAS,SAAQ;AAC7B,QAAI;AACF,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,WAAW,QAAQ,WAAY,MAAM,KAAK,QAAQ,mBAAmB,EAAE,MAAM,MAAO,GAAG;AAGrF,yBAAiB;AACjB,iBAAS,KAAK,GAAG,SAAS,IAAI,2DAA2D;AAAA,MAC3F;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;AAAA,IACH,UAAE;AACA,cAAQ,oBAAoB,SAAS,OAAO;AAC5C,YAAM,KAAK,MAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IACnC;AAAA,EACF;AACA,SAAO,EAAE,SAAS,eAAe,eAAe,eAAe,CAAC,GAAG,aAAa,EAAE,KAAK,GAAG,SAAS;AACrG;AAEA,eAAsB,oBAAoB;AACxC,QAAM,SAAS,SAAS,cAAc;AACtC,QAAM,SAAS,SAAS,cAAc;AACtC,QAAM,QAAQ,aAAa;AAC3B,QAAM,MAAoB;AAAA,IACxB,WAAW,SAAS,iBAAiB;AAAA,IACrC,WAAW,SAAS,iBAAiB;AAAA,IACrC,eAAe,SAAS,qBAAqB;AAAA,IAC7C,KAAK;AAAA,MACH,eAAe,SAAS,aAAa;AAAA,MACrC,oBAAoB,OAAO,QAAQ,IAAI,gBAAgB,KAAK;AAAA,MAC5D,gBAAgB,SAAS,cAAc;AAAA,MACvC,aAAa,SAAS,mBAAmB;AAAA,IAC3C;AAAA,EACF;AAEA,QAAM,MAAW,OAAU,MAAc,OAA4D,CAAC,MAAkB;AACtH,QAAI;AACJ,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM,MAAM,IAAI,IAAI,MAAM,MAAM,GAAG;AAAA,QAC5C,QAAQ,KAAK,UAAU;AAAA,QACvB,SAAS;AAAA,UACP,eAAe,UAAU,MAAM;AAAA,UAC/B,gBAAgB;AAAA,UAChB,GAAI,KAAK,QAAQ,EAAE,0BAA0B,KAAK,MAAM,IAAI,CAAC;AAAA,QAC/D;AAAA,QACA,GAAI,KAAK,SAAS,SAAY,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,KAAK,IAAI,EAAE;AAAA,QACrE,QAAQ,YAAY,QAAQ,cAAc;AAAA,MAC5C,CAAC;AACD,aAAO,MAAM,SAAS,KAAK;AAAA,IAC7B,SAAS,OAAO;AACd,YAAM,OAAQ,MAAgB;AAC9B,UAAI,SAAS,kBAAkB,SAAS,cAAc;AACpD,cAAM,IAAI,kBAAkB,oCAAoC,GAAG,KAAK,UAAU,KAAK,IAAI,IAAI,0BAA0B,iBAAiB,GAAI,WAAW;AAAA,MAC3J;AACA,YAAM;AAAA,IACR;AACA,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,OAAO,YAAYF,MAAK,OAAO,GAAG,gBAAgB,CAAC;AAEzD,QAAM,UAAU,OAAO,aAAiD;AACtE,UAAM,eAAeA,MAAK,MAAM,eAAe;AAC/C,IAAAG,eAAc,cAAc,KAAK,UAAU,EAAE,YAAY,SAAS,WAAW,IAAI,CAAC,EAAE,IAAI,QAAQ,SAAS,OAAO,EAAE,IAAI,QAAQ,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC,EAAG,EAAE,EAAE,CAAC,CAAC;AACtK,UAAM,MAAMH,MAAK,MAAM,SAAS;AAChC,QAAI;AACJ,QAAI;AACF,cAAQ,oBAAoB,EAAE,MAAM,QAAQ,IAAI,GAAG,cAAc,IAAI,CAAC;AAAA,IACxE,SAAS,OAAO;AACd,YAAM,IAAI,kBAAkB,kCAAkC,6BAA8B,MAAgB,OAAO,EAAE;AAAA,IACvH;AAGA,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,SAASE,cAAa,OAAO;AAEnC,UAAM,kBAAkB,KAAK,MAAMA,cAAaJ,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,UAAM,QAAQ,MAAM,QAAQ,UAAU,CAACA,MAAK,KAAK,gBAAgB,KAAK,gBAAgB,KAAK,CAAC,GAAG;AAAA,MAC7F,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,QAAQ,MAAM;AAAA,IAClC,CAAC;AAGD,UAAM,eAAyB,CAAC;AAChC,UAAM,UAAU,CAAC,WAA+B,CAAC,UAAkB;AACjE,aAAO,MAAM,KAAK;AAClB,iBAAW,QAAQ,MAAM,SAAS,MAAM,EAAE,MAAM,IAAI,GAAG;AACrD,cAAM,QAAQ,KAAK,QAAQ,mBAAmB,EAAE,EAAE,KAAK;AACvD,YAAI,aAAa,SAAS,OAAO,yBAAyB,KAAK,KAAK,EAAG,cAAa,KAAK,MAAM,MAAM,GAAG,GAAG,CAAC;AAAA,MAC9G;AAAA,IACF;AACA,UAAM,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,MAAM,CAAC;AAChD,UAAM,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,MAAM,CAAC;AAEhD,QAAI,UAA0B;AAC9B,QAAI;AACF,UAAI,CAAE,MAAM,UAAU,GAAG,KAAK,GAAG,kBAAkB,WAAW,GAAM,GAAI;AACtE,cAAM,IAAI,kBAAkB,2CAA2C,sDAAsD;AAAA,MAC/H;AACA,oBAAc;AACd,YAAM,EAAE,SAAS,IAAI,MAAM,OAAO,YAAY;AAC9C,gBAAU,MAAM,SAAS,OAAO;AAAA,IAClC,SAAS,OAAO;AACd,YAAM,KAAK;AACX,YAAM;AAAA,IACR;AACA,UAAM,SAAS;AAEf,WAAO;AAAA,MACL,OAAO,MAAM;AAAA,MACb;AAAA,MACA,YAAY,OAAO,aAAa,OAAO,WAAW;AAChD,cAAM,SAAS,MAAM,MAAM,GAAG,KAAK,GAAG,kBAAkB,UAAU;AAAA,UAChE,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,oBAAoB,wBAAwB,aAAa;AAAA,UACpF,MAAM,KAAK,UAAU,EAAE,aAAa,MAAM,CAAC;AAAA,UAC3C;AAAA,QACF,CAAC;AACD,YAAI,CAAC,OAAO,IAAI;AACd,gBAAM,IAAI,kBAAkB,qCAAqC,mDAAmD,OAAO,MAAM,IAAI;AAAA,QACvI;AACA,gBAAS,MAAM,OAAO,KAAK,GAAsB;AAAA,MACnD;AAAA,MACA,QAAQ,CAAC,UAAU,WAAW,YAAY,SAAS,WACjD,gBAAgB,QAAQ,GAAG,KAAK,GAAG,kBAAkB,cAAc,mBAAmB,QAAQ,CAAC,IAAI,WAAW,YAAY,SAAS,MAAM;AAAA,MAC3I,kBAAkB,MAAM,aAAa;AAAA,MACrC,mBAAmB,CAAC,SAAS,aAAa,MAAM,IAAI;AAAA,MACpD,OAAO,YAAY;AACjB,cAAM,KAAK;AACX,cAAM,OAAO,MAAM,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,OAAO,QAAQ,IAAI,oBAAoB;AACtD,MAAI;AACF,UAAM,SAAS,MAAM,mBAAmB,OAAO,KAAK;AAAA,MAClD;AAAA,MACA;AAAA,MACA,WAAW,OAAO,WAAW,UAAU;AACrC,YAAI;AACF,gBAAM,MAAM,MAAM,MAAM,WAAW;AAAA,YACjC,QAAQ;AAAA,YACR,MAAM,IAAI,WAAW,KAAK;AAAA,YAC1B,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,YAC9C,QAAQ,YAAY,QAAQ,wBAAwB;AAAA,UACtD,CAAC;AACD,iBAAO,EAAE,IAAI,IAAI,IAAI,QAAQ,IAAI,OAAO;AAAA,QAC1C,SAAS,OAAO;AACd,gBAAM,OAAQ,MAAgB;AAC9B,cAAI,SAAS,kBAAkB,SAAS,aAAc,OAAM;AAC5D,gBAAM,IAAI,kBAAkB,0CAA0C,oDAAoD,2BAA2B,GAAM,WAAW;AAAA,QACxK;AAAA,MACF;AAAA,MACA,UAAU,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAAA,IAC7D,CAAC;AACD,QAAI,MAAM,SAAS,GAAG;AACpB,cAAQ,IAAI,iBAAiB,OAAO,UAAU,MAAM,OAAO,MAAM,MAAM,0BAA0B,OAAO,OAAO,MAAM,yBAAyB;AAAA,IAChJ;AACA,QAAI,OAAO,SAAU,SAAQ,WAAW;AAAA,EAC1C,UAAE;AACA,IAAAK,QAAO,MAAM,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EAC/C;AACF;;;AC5sBA,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","run","item","resolve","require","createRequire","join","dirname","spawnSync","writeFileSync","readFileSync","rmSync"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";(()=>{var E=["section","article, header, footer, nav, aside"]
|
|
1
|
+
"use strict";(()=>{var E=["section","article, header, footer, nav, aside"],M=["header, footer, nav, aside","section, article"],R=e=>`"${e.replace(/["\\]/g,"\\$&")}"`;function P(e){let t=[],n=e.getAttribute("data-bcms-field");n&&t.push(n);let o=e.getAttribute("data-bcms-layout-field");o&&t.push(o);for(let r of(e.getAttribute("data-bcms-props")??"").split(";")){let s=r.split("|");s.length===3&&s.every(Boolean)&&t.push(s[0])}return t}function h(e){let t=e[0]??null;for(let n of e.slice(1))for(;t&&!t.contains(n);)t=t.parentElement;return t}function $(e,t){for(let n of t){let o=e.closest(n);if(o)return o}for(let n=e;n;n=n.parentElement)if(n.parentElement?.tagName==="MAIN")return n;return e}function S(e,t){return Array.from(e.querySelectorAll("[data-bcms-field], [data-bcms-layout-field], [data-bcms-props]")).filter(n=>P(n).some(t))}function w(e,t){if(t.blockId){let n=e.querySelector(`[data-bcms-block=${R(t.blockId)}]`);if(n)return n;let o=new Set(Object.values(t.source??{})),r=S(e,a=>o.has(a)||!!t.groupKey&&a.startsWith(`${t.groupKey}-`)||a.startsWith(`${t.blockId}__`)),s=h(r);if(s)return $(s,E)}if(t.layoutSectionId){let n=`layout:${t.layoutSectionId}:`,o=h(S(e,r=>r.startsWith(n)));if(o)return $(o,M)}return t.landmark?e.querySelector(t.landmark):null}function A(e,t,n){if(t&&n.set(t,e),!(e===null||typeof e!="object"||typeof e.html=="string"))for(let[o,r]of Object.entries(e))A(r,t?`${t}.${o}`:o,n)}function x(e,t){let n=e.split(".");for(let o=n.length;o>0;o-=1){let r=n.slice(0,o).join("."),s=t?.[r]!==void 0?r:Object.keys(t??{}).find(a=>a.endsWith(`.${r}`));if(s!==void 0)return[t[s],...n.slice(o)].join(".")}return e}function K(e,t){let n=new Map;A(t,"",n);let o=new Map;for(let[r,s]of n)e.layoutSectionId&&o.set(`layout:${e.layoutSectionId}:${x(r,e.bindings)}`,{key:r,value:s}),e.blockId&&(o.set(e.source?.[r]??`${e.groupKey}-${r.replace(/_/g,"-")}`,{key:r,value:s}),o.set(`${e.blockId}__overrides.${r}`,{key:r,value:s}));return o}function y(e){if(e&&typeof e=="object"){let t=e,n=t.text??t.url??t.href??t.src??t.html;return typeof n=="string"?n:""}return String(e)}function I(e,t,n){let o=K(t,n),r=new Set,s=[e,...Array.from(e.querySelectorAll("[data-bcms-field], [data-bcms-layout-field], [data-bcms-props]"))];for(let c of s){let g=c.getAttribute("data-bcms-field")??c.getAttribute("data-bcms-layout-field");if(g&&o.has(g)){let{key:m,value:l}=o.get(g);if(r.add(m),l!=null){let u=l&&typeof l=="object"?l.html:void 0,p=c.getAttribute("data-bcms-kind");typeof u=="string"?c.innerHTML=u:p==="richtext"||p==="document"?c.innerHTML=y(l):c.textContent=y(l)}}for(let m of(c.getAttribute("data-bcms-props")??"").split(";")){let[l,,u]=m.split("|");if(!l||!u||!o.has(l))continue;let{key:p,value:b}=o.get(l);r.add(p),b!=null&&c.setAttribute(u,y(b))}}let a=new Set(Array.from(o.values(),c=>c.key));return{applied:r.size,unmatched:[...a].filter(c=>!r.has(c))}}function k(e){return e.querySelector("[data-bcms-field],[data-bcms-layout-field],[data-bcms-props],[data-bcms-block]")!==null}function v(e,t){return t&&!k(t)?`${e.route} declares no BetterCMS bindings (the site is not converted yet), so nothing marks ${e.blockId?`group ${e.groupKey??e.blockId}`:`layout section ${e.layoutSectionId??e.landmark}`} \u2014 convert the site to preview it`:e.blockId?`nothing on ${e.route} renders group ${e.groupKey??e.blockId}`:`nothing on ${e.route} renders layout section ${e.layoutSectionId??e.landmark}`}var N="data-bcms-preview-render",i=JSON.parse(document.getElementById("bcms-scope-data").textContent),f=document.getElementById("bcms-page"),j=k(f.content),d=w(f.content,i.meta);if(!d)console.error(`[bcms-preview] ${v(i.meta,f.content)}`);else{for(let r of Array.from(f.content.querySelectorAll('style, link[rel="stylesheet"]')))d.contains(r)||document.head.appendChild(r);let{applied:e,unmatched:t}=I(d,i.meta,i.props),n=e+t.length,o=i.meta.blockId?`group ${i.meta.groupKey??i.meta.blockId}`:`layout section ${i.meta.layoutSectionId??i.meta.landmark}`;n>0&&e===0&&!j?console.warn(`[bcms-preview] ${i.meta.route} declares no BetterCMS bindings (site not converted): the section renders, but live edits will not show until the site is converted.`):n>0&&e===0?console.error(`[bcms-preview] none of ${n} props address anything in ${i.meta.route} ${o}: live edits will not show. Re-run the conversion or record the file.`.slice(0,200)):t.length>0&&console.warn(`[bcms-preview] ${t.length} of ${n} props address nothing in ${i.meta.route} ${o}: ${t.join(", ")}`),d.hasAttribute("data-bcms-block")||d.setAttribute("data-bcms-block",i.meta.blockId??`layout:${i.meta.layoutSectionId??i.meta.landmark}`),document.body.appendChild(d),f.remove(),document.documentElement.setAttribute(N,"1")}})();
|
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// ../component-output/
|
|
1
|
+
// ../component-output/src/index.ts
|
|
2
2
|
var COMPONENT_OUTPUT_TOKEN_PREFIX = "v2";
|
|
3
3
|
var COMPONENT_OUTPUT_SIGNED_VERSION = "component-output.v2";
|
|
4
4
|
var TOKEN_SEGMENTS = 4;
|
|
@@ -347,6 +347,8 @@ async function renderPageSection(request, entry, meta, scopeScript) {
|
|
|
347
347
|
page = await fetch(`${origin}${PREVIEW_BASE}${meta.route}`, {
|
|
348
348
|
headers: { accept: "text/html" },
|
|
349
349
|
cache: "no-store",
|
|
350
|
+
// A redirect would silently scope the wrong page (a login wall, a trailing-slash hop): report it instead.
|
|
351
|
+
redirect: "manual",
|
|
350
352
|
signal: AbortSignal.timeout(3e4)
|
|
351
353
|
});
|
|
352
354
|
} catch (error) {
|
|
@@ -354,7 +356,8 @@ async function renderPageSection(request, entry, meta, scopeScript) {
|
|
|
354
356
|
return new Response("The page this component lives on could not be rendered.", { status: 502, headers });
|
|
355
357
|
}
|
|
356
358
|
if (!page.ok) {
|
|
357
|
-
|
|
359
|
+
const location = page.headers.get("location");
|
|
360
|
+
console.error(`[bcms-preview] error: page ${meta.route} answered HTTP ${page.status}${location ? ` \u2192 ${location}` : ""}`);
|
|
358
361
|
return new Response("The page this component lives on could not be rendered.", { status: 502, headers });
|
|
359
362
|
}
|
|
360
363
|
return new Response(scopeDocument(await page.text(), meta, entry.props, scopeScript), { status: 200, headers });
|
package/dist/shell.global.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
"use strict";(()=>{function
|
|
1
|
+
"use strict";(()=>{function z(e){let n=new ArrayBuffer(e.byteLength);return new Uint8Array(n).set(e),n}function L(e){let{protocol:n,...r}=e;return{...r,runtimeProtocol:n,nativeViewports:e.nativeViewports.map(t=>({...t}))}}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 n=JSON.stringify(e),r=/^(-?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/.exec(n);if(!r)return n;let[,t,o,c="",m]=r,a=`${o}${c}`,d=o.length+Number(m);return d<=0?`${t}0.${"0".repeat(-d)}${a}`:d>=a.length?`${t}${a}${"0".repeat(d-a.length)}`:`${t}${a.slice(0,d)}.${a.slice(d)}`}if(Array.isArray(e))return`[${e.map(n=>{try{return O(n)}catch{return"null"}}).join(",")}]`;if(e&&typeof e=="object"){let n=Object.entries(e).filter(([,t])=>t!==void 0&&typeof t!="function").sort(([t],[o])=>t<o?-1:t>o?1:0),r=[];for(let[t,o]of n)try{r.push(`${JSON.stringify(t)}:${O(o)}`)}catch{}return`{${r.join(",")}}`}throw new TypeError("Unsupported canonical JSON value")}async function $(e){let n=new TextEncoder().encode(O(e)),r=await crypto.subtle.digest("SHA-256",z(n));return`sha256:${[...new Uint8Array(r)].map(o=>o.toString(16).padStart(2,"0")).join("")}`}var B=/^(chrome-extension|moz-extension|safari-web-extension|safari-extension):\/\//i,X=/^at .*?\(?([a-z][a-z0-9+.-]*:\/\/[^\s()]+?):\d+(?::\d+)?\)?$/i,Y=/@([a-z][a-z0-9+.-]*:\/\/[^\s()]+?):\d+(?::\d+)?$/i;function D(e){if(!e)return[];let n=[];for(let r of e.split(`
|
|
2
|
+
`)){let t=r.trim(),o=t.startsWith("at ")?X.exec(t):Y.exec(t);o?.[1]&&n.push(o[1])}return n}var K=e=>e.every(n=>B.test(n));function b(e){let n=e?.stack;return typeof n=="string"?n:void 0}function H(e,n){return!e||!B.test(e)?!1:K(D(n))}function F(e){let n=D(e);return n.length>0&&K(n)}var f="bcms-component-preview/2";function U(e){let{claims:n,dashboardOrigin:r,onProps:t,onStatus:o=()=>{},onError:c=()=>{},retryIntervalMs:m=400,maxAnnounces:a=10,ackDeadlineMs:d=250}=e,I=L(n),p=null,k=!1,M=0,N=0,h=null,l=s=>{p?.postMessage({protocol:f,binding:I,...s})},C=()=>{h!==null&&clearInterval(h),h=null};if(typeof window>"u"||window.parent===window)return o({kind:"not-embedded",dashboardOrigin:r}),{dispose:()=>{}};let v=()=>{if(!k){if(M>=a){C(),console.warn(`[bcms] no BetterCMS dashboard answered at ${r}. If the dashboard is on another origin, dashboardOrigin is misconfigured.`),o({kind:"unanswered",dashboardOrigin:r});return}M+=1,window.parent.postMessage({protocol:f,kind:"runtime:ready",binding:I},r)}},W=async s=>{let i=s.data;if(i?.protocol!==f||i.kind!=="props")return;let u=i.propsRevision;if(typeof u!="number"||u<=N)return;let E=await $(i.props);if(E!==i.propsHash){l({kind:"runtime:error",error:{code:"PROPS_HASH_MISMATCH",message:"Props did not match their hash."}});return}N=u;try{await t(i.props)}catch(G){console.error("[bcms] component render failed:",G),l({kind:"runtime:error",error:{code:"RENDER_FAILED",message:"The component could not be rendered."}});return}let x=!1,P=()=>{x||(x=!0,l({kind:"props:ack",propsRevision:u,propsHash:E}))};requestAnimationFrame(()=>requestAnimationFrame(P)),setTimeout(P,d)},T=s=>{if(s.origin!==r){let E=s.data;s.source===window.parent&&E?.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!==f||i.kind!=="runtime:connect")return;let u=s.ports[0];u&&(k=!0,C(),p=u,p.onmessage=W,p.start?.(),o({kind:"connected"}),l({kind:"runtime:connected"}))},A=s=>console.warn(`[bcms] ignored an error from a browser extension: ${String(s).slice(0,300)}`),R=s=>{if(H(s.filename,b(s.error)))return A(s.message);let i={code:"RUNTIME_ERROR",message:String(s.message).slice(0,300)};c(i),l({kind:"runtime:error",error:i})},_=s=>{if(F(b(s.reason)))return A(s.reason);let i={code:"UNHANDLED_REJECTION",message:String(s.reason).slice(0,300)};c(i),l({kind:"runtime:error",error:i})};return o({kind:"connecting",dashboardOrigin:r}),window.addEventListener("message",T),window.addEventListener("error",R),window.addEventListener("unhandledrejection",_),v(),h=setInterval(v,m),{dispose:()=>{C(),window.removeEventListener("message",T),window.removeEventListener("error",R),window.removeEventListener("unhandledrejection",_),p?.close(),p=null,k=!1}}}function V(e){typeof window>"u"||window.parent===window||window.parent.postMessage({protocol:f,kind:"runtime:refused",cause:e.cause},e.dashboardOrigin)}var q=15e3,S=document.getElementById("bcms-status"),g=document.getElementById("bcms-frame"),y=JSON.parse(document.getElementById("bcms-preview-config").textContent),w=e=>{S.textContent=e,S.hidden=!1},J=(e,n)=>fetch(e,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(n),credentials:"omit",cache:"no-store"}),Z=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((n,r)=>{let t=!1,o=a=>{if(!t){if(t=!0,clearInterval(m),clearTimeout(c),g.onload=null,a){r(a);return}S.hidden=!0,g.hidden=!1,n()}},c=setTimeout(()=>o(new Error("render timed out")),q),m=setInterval(()=>{j()&&o()},Z);g.onload=()=>o(j()?void 0:new Error("the render page did not render (blocked, not found, or failed)")),g.src=`${y.routes.render}?id=${encodeURIComponent(e)}`})}async function ee(){let e=new URL(location.href).searchParams.get("bcmsSession");history.replaceState(null,"",location.pathname);let r=await(await J(y.routes.session,{token:e})).json().catch(()=>null);if(!r||!r.ok){let t=r&&!r.ok&&r.cause==="keys-unreachable"?"keys-unreachable":"session-refused";V({dashboardOrigin:y.dashboardOrigin,cause:t}),w("This preview session was refused. The runtime log records why.");return}U({claims:r.claims,dashboardOrigin:y.dashboardOrigin,onProps:async t=>{let o=await J(y.routes.props,{renderKey:r.renderKey,props:t});if(!o.ok)throw new Error(`props were refused (HTTP ${o.status})`);let{id:c}=await o.json();await Q(c)},onStatus:t=>{t.kind!=="connected"&&(t.kind==="connecting"?w("Connecting to BetterCMS\u2026"):t.kind==="unanswered"?w(`${t.dashboardOrigin} did not answer.`):w("This page is meant to be opened from BetterCMS."))}})}ee().catch(e=>{console.error("[bcms-preview]",e),w("The preview could not start. The browser console records why.")});})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bettercms-ai/preview-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Zero-config BetterCMS component previews: builds a preview runtime for Next.js and Astro apps and validates components in CI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"prepublishOnly": "bun run build"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@bettercms-ai/component-output": "^0.2.
|
|
26
|
+
"@bettercms-ai/component-output": "^0.2.1",
|
|
27
27
|
"playwright": "^1.55.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|