Setting the file. One moment. Build Extracted Homepage · Wix Headless Replatform · wix/skills · Skills Docs28.10
Workflow
203
function renderFontFaces
— line 203
This file
- Number
- 28.14
- Position
- 14 of 89
- Type
- JavaScript
- Size
- 15 KB
- Lines
- 216
scripts/build-extracted-homepage.mjs
JavaScript·216 lines·15 KB
(
value
)
=>
String
(value
??
""
).
replace
(
/&/
g
,
"&"
).
replace
(
/</
g
,
"<"
).
replace
(
/>/
g
,
">"
).
replace
(
/"/
g
,
"""
);
9const plain = (value) => String(value ?? "").replace(/\s+/g, " ").trim();
10
11export async function buildExtractedHomepage({ outputDir }) {
12 const docs = path.join(outputDir, "docs", "site-clone");
13 const latest = await readJson(path.join(docs, "extraction", "latest.json"));
14 const extractionDir = path.join(docs, "extraction", latest.captureId);
15 const integrity = await verifyFrozenManifest(extractionDir);
16 if (!integrity.ok) throw new Error(`Frozen extraction integrity failed: ${integrity.failures.join("; ")}`);
17 if (integrity.manifest.manifestHash !== latest.manifestHash) throw new Error("Latest extraction pointer does not match the frozen manifest hash");
18 const index = await readJson(path.join(extractionDir, "spec-index.json"));
19 const specs = new Map();
20 for (const [id, relativePath] of Object.entries(index)) specs.set(id, await readJson(path.join(extractionDir, relativePath)));
21 const plan = await readJson(path.join(docs, "build", "build-plan.json"));
22 if (plan.manifestHash !== integrity.manifest.manifestHash) throw new Error("Build plan is stale for the current frozen manifest");
23 const componentSelection = await readJson(path.join(docs, "build", "component-selection.json"));
24 if (componentSelection.manifestHash !== plan.manifestHash) throw new Error("Component selection is stale for the current frozen manifest");
25 const registryInstallations = [];
26 for (const selection of componentSelection.selections || []) {
27 if (selection.strategy !== "curated-registry") continue;
28 registryInstallations.push({ unitId: selection.unitId, ...(await installRegistrySelection({ selection, outputDir })) });
29 }
30 await writeJson(path.join(docs, "build", "registry-installation.json"), {
31 schemaVersion: plan.schemaVersion,
32 manifestHash: plan.manifestHash,
33 installations: registryInstallations,
34 });
35 const selectionByUnit = new Map((componentSelection.selections || []).map((selection) => [selection.unitId, selection]));
36 const registryImports = registryComponentImports(componentSelection.selections || []);
37
38 const metadata = specs.get("metadata:home")?.document || {};
39 const foundation = specs.get("foundation:home") || {};
40 const chrome = removeContaminatedContent(specs.get("shared-chrome:home") || {});
41 const sections = [...specs.values()].filter((spec) => spec.kind === "unit" && spec.unitKind === "section").sort((a, b) => a.order - b.order);
42 const resolution = await readJson(path.join(extractionDir, "page-resolution.spec.json"));
43 const title = plain(metadata.title || metadata.openGraph?.["og:site_name"] || new URL(resolution.source.resolvedUrl).hostname);
44 const description = plain(metadata.description || "");
45 const logo = chrome.visualAssets?.logos?.find((asset) => asset.usages?.some((usage) => /header/.test(usage.context))) || chrome.visualAssets?.logos?.[0];
46 const logoPath = assetPath(logo);
47 const navigation = chrome.header?.navigation || chrome.header?.header?.navigation || [];
48 const sectionMarkup = sections.map((section) => renderSection(section, specs, selectionByUnit, registryImports.symbols)).join("\n ");
49 const navMarkup = navigation.map((item) => renderNavItem(item)).join("");
50 const fontCss = renderFontFaces(foundation.fonts?.faces || []);
51 const direction = foundation.globalCssIntent?.direction === "rtl" ? "rtl" : "ltr";
52
53 const astro = `---
54import Layout from '../layouts/Layout.astro';
55${registryImports.lines.join("\n")}
56---
57<Layout title=${JSON.stringify(title)} description=${JSON.stringify(description)}>
58 <div data-rp-manifest=${JSON.stringify(plan.manifestHash)} data-rp-status=${JSON.stringify(plan.status)} dir=${JSON.stringify(direction)}>
59 <header class="site-header" data-rp-unit="shared-chrome:home">
60 <a href="/" aria-label=${JSON.stringify(title ? `${title} home` : "Home")}>${logoPath ? `<img src="${safe(logoPath)}" alt="${safe(logo?.accessibleName || title)}" />` : `<span>${safe(title)}</span>`}</a>
61 ${navMarkup ? `<nav aria-label="Primary"><ul>${navMarkup}</ul></nav>` : ""}
62 </header>
63 <main id="content">${sectionMarkup}</main>
64 ${chrome.footer ? `<footer data-rp-unit="shared-chrome:home">${footerContent(chrome.footer)}</footer>` : ""}
65 </div>
66</Layout>
67<style>
68 ${fontCss}
69 :global(*){box-sizing:border-box}:global(body){margin:0;color:var(--rp-foreground,#1d1d1f);background:var(--rp-background,#fff);font-family:var(--rp-font-body,Arial,sans-serif)}
70 .site-header{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:18px clamp(20px,6vw,96px);border-bottom:1px solid var(--rp-border,#ececec)}
71 .site-header img{display:block;max-height:56px;max-width:220px}.site-header a{color:inherit;text-decoration:none}.site-header ul{display:flex;align-items:center;gap:20px;margin:0;padding:0;list-style:none}.site-header li{position:relative}.site-header li ul{display:none;position:absolute;z-index:10;min-width:14rem;padding:1rem;background:var(--rp-background,#fff)}.site-header li:focus-within>ul,.site-header li:hover>ul{display:grid}
72 .rp-section{position:relative;padding:clamp(44px,8vw,112px) clamp(20px,7vw,120px);overflow:hidden}.rp-section h2{margin:0 0 16px;font-size:clamp(1.8rem,3.5vw,3.4rem);line-height:1.12}.rp-section p{max-width:900px;margin:0;line-height:1.7;white-space:pre-line}.rp-section-media{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:center;gap:clamp(28px,7vw,120px)}.rp-section-media>img{width:100%;max-height:640px;object-fit:cover}.rp-items{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr));gap:20px;margin-top:2rem}.rp-item{padding:1rem;border:1px solid var(--rp-border,#ddd)}footer{padding:34px clamp(20px,6vw,96px)}
73 @media(max-width:760px){.site-header{align-items:flex-start}.site-header ul{flex-wrap:wrap}.rp-section-media{grid-template-columns:1fr}}
74</style>
75`;
76 await writeText(path.join(outputDir, "src", "pages", "index.astro"), astro);
77 const ledger = {
78 schemaVersion: plan.schemaVersion,
79 manifestHash: plan.manifestHash,
80 pageKey: "home",
81 units: plan.units.map((unit) => ({
82 id: unit.id,
83 specHash: unit.hash,
84 status: unit.status === "provisional" ? "provisional" : "implemented",
85 cloneRoot: ["foundation", "metadata"].includes(unit.kind) ? null : `[data-rp-unit="${unit.id}"]`,
86 gapRefs: unit.gapRefs,
87 verification: { desktop: "pending", tablet: "pending", mobile: "pending" },
88 })),
89 };
90 await writeJson(path.join(docs, "build", "section-implementation.json"), ledger);
91 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) };
92}
93
94export function renderSection(spec, specs = new Map(), selectionByUnit = new Map(), registrySymbols = new Map()) {
95 const contentOptions = { allowVisibleCode: spec.extensions?.["wix.replatform.content-boundary"]?.visibleCode === true };
96 const contamination = inspectContentObject(spec.content || {}, contentOptions);
97 const content = removeContaminatedContent(spec.content || {}, contentOptions);
98 const effectiveStatus = contamination.contaminated ? "provisional" : spec.status;
99 const heading = safe(plain(content.heading || content.label));
100 const text = safe(plain(content.text));
101 const assets = Array.isArray(spec.assets) ? spec.assets : [];
102 const image = assets.map(assetPath).find(Boolean);
103 const items = Array.isArray(content.items) ? content.items : [];
104 const itemMarkup = items.length ? `<div class="rp-items">${items.map((item) => `<article class="rp-item">${item.heading ? `<h3>${safe(plain(item.heading))}</h3>` : ""}${item.text || item.paragraphs ? `<p>${safe(plain(item.text || item.paragraphs?.join(" ")))}</p>` : ""}</article>`).join("")}</div>` : "";
105 const children = (spec.children || []).map((id) => specs.get(id)).filter(Boolean).sort((a, b) => a.order - b.order);
106 const childMarkup = children.length ? `<div class="rp-components">${children.map((child) => renderComponent(child, specs, selectionByUnit, registrySymbols)).join("")}</div>` : "";
107 const body = `<div>${heading ? `<h2>${heading}</h2>` : ""}${text ? `<p>${text}</p>` : ""}${itemMarkup}${childMarkup}</div>`;
108 const warning = contamination.contaminated ? ' data-rp-content-warning="source-code-contamination"' : "";
109 return `<section id="${safe(spec.id)}" class="rp-section${image ? " rp-section-media" : ""}" data-rp-unit="${safe(spec.id)}" data-rp-classification="${safe(spec.classification?.kind)}" data-rp-status="${safe(effectiveStatus)}"${warning}>${image ? `<img src="${safe(image)}" alt="" loading="lazy" />` : ""}${body}</section>`;
110}
111
112function renderComponent(spec, specs, selectionByUnit, registrySymbols) {
113 const selection = selectionByUnit.get(spec.id);
114 const contentOptions = { allowVisibleCode: spec.extensions?.["wix.replatform.content-boundary"]?.visibleCode === true };
115 const contamination = inspectContentObject(spec.content || {}, contentOptions);
116 const content = removeContaminatedContent(spec.content || {}, contentOptions);
117 const effectiveStatus = contamination.contaminated ? "provisional" : spec.status;
118 const rawLabel = plain(content.label || content.heading || content.text || spec.classification?.kind);
119 const label = safe(rawLabel);
120 let ownMarkup;
121 if (selection?.strategy === "curated-registry" && selection.selected?.name === "button") {
122 const symbol = registrySymbols.get(`${selection.selected.name}@${selection.selected.revision}`);
123 if (!symbol) throw new Error(`Installed registry button has no generated import for ${spec.id}`);
124 ownMarkup = renderRegistryButton({ spec: { ...spec, content }, selection, symbol, label });
125 } else {
126 const tag = spec.classification?.kind === "button" ? "button" : "div";
127 ownMarkup = `<${tag} data-rp-custom-component="${safe(spec.classification?.kind || "static-content")}">${label}</${tag}>`;
128 }
129 const children = (spec.children || []).map((id) => specs.get(id)).filter(Boolean).sort((a, b) => a.order - b.order);
130 const warning = contamination.contaminated ? ' data-rp-content-warning="source-code-contamination"' : "";
131 return `<div data-rp-unit="${safe(spec.id)}" data-rp-status="${safe(effectiveStatus)}"${warning}>${ownMarkup}${children.map((child) => renderComponent(child, specs, selectionByUnit, registrySymbols)).join("")}</div>`;
132}
133
134export function renderRegistryButton({ spec, selection, symbol, label }) {
135 const binding = selection.selected.binding || { axes: {}, composition: "text" };
136 const variant = binding.axes?.variant || "default";
137 const size = binding.axes?.size || "default";
138 const marker = `${safe(selection.selected.name)}@${safe(selection.selected.revision)}`;
139 const props = `variant="${safe(variant)}" size="${safe(size)}" data-rp-registry-item="${marker}"`;
140 const iconSource = assetPath(spec.content?.icon) || (spec.assets || []).map(assetPath).find(Boolean);
141 const icon = iconSource ? `<img src="${safe(iconSource)}" alt="" aria-hidden="true" />` : "";
142 const accessibleName = safe(plain(spec.content?.accessibleName || spec.content?.label || spec.content?.heading || spec.content?.text));
143 if (binding.composition === "as-link") {
144 const href = (spec.content?.links || []).find((link) => link?.href)?.href;
145 if (!href) throw new Error(`Registry button binding for ${spec.id} requires href`);
146 return `<${symbol} asChild ${props}><a href="${safe(href)}">${label}</a></${symbol}>`;
147 }
148 if (binding.composition === "icon-only") {
149 if (!iconSource || !accessibleName) throw new Error(`Registry button binding for ${spec.id} requires icon and accessible name`);
150 return `<${symbol} ${props} aria-label="${accessibleName}">${icon}</${symbol}>`;
151 }
152 if (binding.composition === "leading-icon") return `<${symbol} ${props}>${icon}${label}</${symbol}>`;
153 if (binding.composition === "trailing-icon") return `<${symbol} ${props}>${label}${icon}</${symbol}>`;
154 return `<${symbol} ${props}>${label}</${symbol}>`;
155}
156
157function registryComponentImports(selections) {
158 const lines = [];
159 const symbols = new Map();
160 const seen = new Set();
161 for (const selection of selections) {
162 const selected = selection.selected;
163 if (selection.strategy !== "curated-registry" || !selected || selected.name !== "button") continue;
164 const key = `${selected.name}@${selected.revision}`;
165 if (seen.has(key)) continue;
166 const target = selected.files?.find((file) => file.type === "registry:ui")?.target;
167 if (!target?.startsWith("src/")) throw new Error(`Registry component target must be under src/: ${target}`);
168 const fromPages = `../${target.slice(4)}`.replace(/\.(?:tsx?|jsx?)$/, "");
169 const symbol = `RpRegistry${selected.name.replace(/[^a-z0-9]+/gi, " ").split(/\s+/).filter(Boolean).map((part) => `${part[0].toUpperCase()}${part.slice(1)}`).join("")}`;
170 lines.push(`import { Button as ${symbol} } from ${JSON.stringify(fromPages)};`);
171 symbols.set(key, symbol);
172 seen.add(key);
173 }
174 return { lines, symbols };
175}
176
177function renderNavItem(item) {
178 const label = safe(plain(item.label || item.text));
179 const href = safe(item.href || "#");
180 const children = Array.isArray(item.children) ? item.children : [];
181 return `<li><a href="${href}"${children.length ? ' aria-haspopup="true"' : ""}>${label}</a>${children.length ? `<ul>${children.map(renderNavItem).join("")}</ul>` : ""}</li>`;
182}
183
184function assetPath(value) {
185 if (!value) return "";
186 if (typeof value === "string") return normalizeAssetPath(value);
187 return normalizeAssetPath(value.localPath || value.sourceUrl || value.url || value.src || "");
188}
189
190function normalizeAssetPath(value) {
191 if (!value) return "";
192 if (/^https?:\/\//i.test(value) || value.startsWith("/")) return value;
193 return `/${String(value).replace(/^public\//, "")}`;
194}
195
196function footerContent(footer) {
197 const groups = footer?.groups || footer?.linkGroups || [];
198 const links = groups.flatMap((group) => group.links || []).map((link) => `<a href="${safe(link.href || "#")}">${safe(plain(link.label || link.text))}</a>`).join(" ");
199 const legal = safe(plain(footer?.legalText || footer?.text || ""));
200 return `${links}${legal ? `<p>${legal}</p>` : ""}`;
201}
202
203function renderFontFaces(faces) {
204 return faces.filter((face) => face.family && face.sources?.length).map((face) => {
205 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(",");
206 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};` : ""}}` : "";
207 }).join("\n");
208}
209
210async function main() {
211 const args = parseArgs();
212 if (!args.out) throw new Error("--out is required");
213 console.log(JSON.stringify(await buildExtractedHomepage({ outputDir: path.resolve(String(args.out)) }), null, 2));
214}
215
216if (import.meta.url === `file://${process.argv[1]}`) main().catch((error) => { console.error(error.stack || error.message); process.exit(1); });