#!/usr/bin/env node
import path from "node:path";
import { parseArgs, readJson, writeJson, writeText } from "./lib/common.mjs";
import { verifyFrozenManifest } from "./lib/extraction-contract.mjs";
import { installRegistrySelection } from "./lib/component-registry.mjs";
import { inspectContentObject, removeContaminatedContent } from "./lib/content-boundary.mjs";
const safe = (value) => String(value ?? "").replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
const plain = (value) => String(value ?? "").replace(/\s+/g, " ").trim();
export async function buildExtractedHomepage({ outputDir }) {
const docs = path.join(outputDir, "docs", "site-clone");
const latest = await readJson(path.join(docs, "extraction", "latest.json"));
const extractionDir = path.join(docs, "extraction", latest.captureId);
const integrity = await verifyFrozenManifest(extractionDir);
if (!integrity.ok) throw new Error(`Frozen extraction integrity failed: ${integrity.failures.join("; ")}`);
if (integrity.manifest.manifestHash !== latest.manifestHash) throw new Error("Latest extraction pointer does not match the frozen manifest hash");
const index = await readJson(path.join(extractionDir, "spec-index.json"));
const specs = new Map();
for (const [id, relativePath] of Object.entries(index)) specs.set(id, await readJson(path.join(extractionDir, relativePath)));
const plan = await readJson(path.join(docs, "build", "build-plan.json"));
if (plan.manifestHash !== integrity.manifest.manifestHash) throw new Error("Build plan is stale for the current frozen manifest");
const componentSelection = await readJson(path.join(docs, "build", "component-selection.json"));
if (componentSelection.manifestHash !== plan.manifestHash) throw new Error("Component selection is stale for the current frozen manifest");
const registryInstallations = [];
for (const selection of componentSelection.selections || []) {
if (selection.strategy !== "curated-registry") continue;
registryInstallations.push({ unitId: selection.unitId, ...(await installRegistrySelection({ selection, outputDir })) });
}
await writeJson(path.join(docs, "build", "registry-installation.json"), {
schemaVersion: plan.schemaVersion,
manifestHash: plan.manifestHash,
installations: registryInstallations,
});
const selectionByUnit = new Map((componentSelection.selections || []).map((selection) => [selection.unitId, selection]));
const registryImports = registryComponentImports(componentSelection.selections || []);
const metadata = specs.get("metadata:home")?.document || {};
const foundation = specs.get("foundation:home") || {};
const chrome = removeContaminatedContent(specs.get("shared-chrome:home") || {});
const sections = [...specs.values()].filter((spec) => spec.kind === "unit" && spec.unitKind === "section").sort((a, b) => a.order - b.order);
const resolution = await readJson(path.join(extractionDir, "page-resolution.spec.json"));
const title = plain(metadata.title || metadata.openGraph?.["og:site_name"] || new URL(resolution.source.resolvedUrl).hostname);
const description = plain(metadata.description || "");
const logo = chrome.visualAssets?.logos?.find((asset) => asset.usages?.some((usage) => /header/.test(usage.context))) || chrome.visualAssets?.logos?.[0];
const logoPath = assetPath(logo);
const navigation = chrome.header?.navigation || chrome.header?.header?.navigation || [];
const sectionMarkup = sections.map((section) => renderSection(section, specs, selectionByUnit, registryImports.symbols)).join("\n ");
const navMarkup = navigation.map((item) => renderNavItem(item)).join("");
const fontCss = renderFontFaces(foundation.fonts?.faces || []);
const direction = foundation.globalCssIntent?.direction === "rtl" ? "rtl" : "ltr";
const astro = `---
import Layout from '../layouts/Layout.astro';
${registryImports.lines.join("\n")}
---
${sectionMarkup}
${chrome.footer ? `` : ""}
`;
await writeText(path.join(outputDir, "src", "pages", "index.astro"), astro);
const ledger = {
schemaVersion: plan.schemaVersion,
manifestHash: plan.manifestHash,
pageKey: "home",
units: plan.units.map((unit) => ({
id: unit.id,
specHash: unit.hash,
status: unit.status === "provisional" ? "provisional" : "implemented",
cloneRoot: ["foundation", "metadata"].includes(unit.kind) ? null : `[data-rp-unit="${unit.id}"]`,
gapRefs: unit.gapRefs,
verification: { desktop: "pending", tablet: "pending", mobile: "pending" },
})),
};
await writeJson(path.join(docs, "build", "section-implementation.json"), ledger);
return { outputDir, manifestHash: plan.manifestHash, status: plan.status, sections: sections.length, registryInstallations: registryInstallations.length, provisionalUnits: plan.units.filter((unit) => unit.status === "provisional").map((unit) => unit.id) };
}
export function renderSection(spec, specs = new Map(), selectionByUnit = new Map(), registrySymbols = new Map()) {
const contentOptions = { allowVisibleCode: spec.extensions?.["wix.replatform.content-boundary"]?.visibleCode === true };
const contamination = inspectContentObject(spec.content || {}, contentOptions);
const content = removeContaminatedContent(spec.content || {}, contentOptions);
const effectiveStatus = contamination.contaminated ? "provisional" : spec.status;
const heading = safe(plain(content.heading || content.label));
const text = safe(plain(content.text));
const assets = Array.isArray(spec.assets) ? spec.assets : [];
const image = assets.map(assetPath).find(Boolean);
const items = Array.isArray(content.items) ? content.items : [];
const itemMarkup = items.length ? `
${items.map((item) => `
${item.heading ? `${safe(plain(item.heading))}
` : ""}${item.text || item.paragraphs ? `${safe(plain(item.text || item.paragraphs?.join(" ")))}
` : ""}`).join("")}
` : "";
const children = (spec.children || []).map((id) => specs.get(id)).filter(Boolean).sort((a, b) => a.order - b.order);
const childMarkup = children.length ? `${children.map((child) => renderComponent(child, specs, selectionByUnit, registrySymbols)).join("")}
` : "";
const body = `${heading ? `
${heading}
` : ""}${text ? `
${text}
` : ""}${itemMarkup}${childMarkup}
`;
const warning = contamination.contaminated ? ' data-rp-content-warning="source-code-contamination"' : "";
return `${image ? `
` : ""}${body}`;
}
function renderComponent(spec, specs, selectionByUnit, registrySymbols) {
const selection = selectionByUnit.get(spec.id);
const contentOptions = { allowVisibleCode: spec.extensions?.["wix.replatform.content-boundary"]?.visibleCode === true };
const contamination = inspectContentObject(spec.content || {}, contentOptions);
const content = removeContaminatedContent(spec.content || {}, contentOptions);
const effectiveStatus = contamination.contaminated ? "provisional" : spec.status;
const rawLabel = plain(content.label || content.heading || content.text || spec.classification?.kind);
const label = safe(rawLabel);
let ownMarkup;
if (selection?.strategy === "curated-registry" && selection.selected?.name === "button") {
const symbol = registrySymbols.get(`${selection.selected.name}@${selection.selected.revision}`);
if (!symbol) throw new Error(`Installed registry button has no generated import for ${spec.id}`);
ownMarkup = renderRegistryButton({ spec: { ...spec, content }, selection, symbol, label });
} else {
const tag = spec.classification?.kind === "button" ? "button" : "div";
ownMarkup = `<${tag} data-rp-custom-component="${safe(spec.classification?.kind || "static-content")}">${label}${tag}>`;
}
const children = (spec.children || []).map((id) => specs.get(id)).filter(Boolean).sort((a, b) => a.order - b.order);
const warning = contamination.contaminated ? ' data-rp-content-warning="source-code-contamination"' : "";
return `${ownMarkup}${children.map((child) => renderComponent(child, specs, selectionByUnit, registrySymbols)).join("")}
`;
}
export function renderRegistryButton({ spec, selection, symbol, label }) {
const binding = selection.selected.binding || { axes: {}, composition: "text" };
const variant = binding.axes?.variant || "default";
const size = binding.axes?.size || "default";
const marker = `${safe(selection.selected.name)}@${safe(selection.selected.revision)}`;
const props = `variant="${safe(variant)}" size="${safe(size)}" data-rp-registry-item="${marker}"`;
const iconSource = assetPath(spec.content?.icon) || (spec.assets || []).map(assetPath).find(Boolean);
const icon = iconSource ? `
` : "";
const accessibleName = safe(plain(spec.content?.accessibleName || spec.content?.label || spec.content?.heading || spec.content?.text));
if (binding.composition === "as-link") {
const href = (spec.content?.links || []).find((link) => link?.href)?.href;
if (!href) throw new Error(`Registry button binding for ${spec.id} requires href`);
return `<${symbol} asChild ${props}>${label}${symbol}>`;
}
if (binding.composition === "icon-only") {
if (!iconSource || !accessibleName) throw new Error(`Registry button binding for ${spec.id} requires icon and accessible name`);
return `<${symbol} ${props} aria-label="${accessibleName}">${icon}${symbol}>`;
}
if (binding.composition === "leading-icon") return `<${symbol} ${props}>${icon}${label}${symbol}>`;
if (binding.composition === "trailing-icon") return `<${symbol} ${props}>${label}${icon}${symbol}>`;
return `<${symbol} ${props}>${label}${symbol}>`;
}
function registryComponentImports(selections) {
const lines = [];
const symbols = new Map();
const seen = new Set();
for (const selection of selections) {
const selected = selection.selected;
if (selection.strategy !== "curated-registry" || !selected || selected.name !== "button") continue;
const key = `${selected.name}@${selected.revision}`;
if (seen.has(key)) continue;
const target = selected.files?.find((file) => file.type === "registry:ui")?.target;
if (!target?.startsWith("src/")) throw new Error(`Registry component target must be under src/: ${target}`);
const fromPages = `../${target.slice(4)}`.replace(/\.(?:tsx?|jsx?)$/, "");
const symbol = `RpRegistry${selected.name.replace(/[^a-z0-9]+/gi, " ").split(/\s+/).filter(Boolean).map((part) => `${part[0].toUpperCase()}${part.slice(1)}`).join("")}`;
lines.push(`import { Button as ${symbol} } from ${JSON.stringify(fromPages)};`);
symbols.set(key, symbol);
seen.add(key);
}
return { lines, symbols };
}
function renderNavItem(item) {
const label = safe(plain(item.label || item.text));
const href = safe(item.href || "#");
const children = Array.isArray(item.children) ? item.children : [];
return `${label}${children.length ? `${children.map(renderNavItem).join("")}
` : ""}`;
}
function assetPath(value) {
if (!value) return "";
if (typeof value === "string") return normalizeAssetPath(value);
return normalizeAssetPath(value.localPath || value.sourceUrl || value.url || value.src || "");
}
function normalizeAssetPath(value) {
if (!value) return "";
if (/^https?:\/\//i.test(value) || value.startsWith("/")) return value;
return `/${String(value).replace(/^public\//, "")}`;
}
function footerContent(footer) {
const groups = footer?.groups || footer?.linkGroups || [];
const links = groups.flatMap((group) => group.links || []).map((link) => `${safe(plain(link.label || link.text))}`).join(" ");
const legal = safe(plain(footer?.legalText || footer?.text || ""));
return `${links}${legal ? `${legal}
` : ""}`;
}
function renderFontFaces(faces) {
return faces.filter((face) => face.family && face.sources?.length).map((face) => {
const sources = face.sources.filter((source) => source.localPath || source.url).map((source) => `url(${JSON.stringify(normalizeAssetPath(source.localPath || source.url))})${source.format ? ` format(${JSON.stringify(source.format)})` : ""}`).join(",");
return sources ? `@font-face{font-family:${JSON.stringify(face.family)};src:${sources};font-style:${face.style || "normal"};font-weight:${face.weight || "normal"};${face.unicodeRange ? `unicode-range:${face.unicodeRange};` : ""}}` : "";
}).join("\n");
}
async function main() {
const args = parseArgs();
if (!args.out) throw new Error("--out is required");
console.log(JSON.stringify(await buildExtractedHomepage({ outputDir: path.resolve(String(args.out)) }), null, 2));
}
if (import.meta.url === `file://${process.argv[1]}`) main().catch((error) => { console.error(error.stack || error.message); process.exit(1); });