@reddoorla/maintenance 0.75.0 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/{announce-V6EYPPUR.js → announce-RUT247HS.js} +9 -6
  2. package/dist/{announce-V6EYPPUR.js.map → announce-RUT247HS.js.map} +1 -1
  3. package/dist/{blux-C24OVDDW.js → blux-RIJBRVCF.js} +1867 -277
  4. package/dist/blux-RIJBRVCF.js.map +1 -0
  5. package/dist/chunk-37S6L6G4.js +170 -0
  6. package/dist/chunk-37S6L6G4.js.map +1 -0
  7. package/dist/{chunk-PERP7LSS.js → chunk-3U3IGVAQ.js} +89 -13
  8. package/dist/chunk-3U3IGVAQ.js.map +1 -0
  9. package/dist/{chunk-IDBMYDVH.js → chunk-4VAUSRSS.js} +5 -3
  10. package/dist/{chunk-IDBMYDVH.js.map → chunk-4VAUSRSS.js.map} +1 -1
  11. package/dist/{run-migration-LJNNZWIV.js → chunk-6DOFNDXU.js} +5 -3
  12. package/dist/chunk-6DOFNDXU.js.map +1 -0
  13. package/dist/{chunk-NUIBQPB3.js → chunk-6SZBNALD.js} +62 -9
  14. package/dist/chunk-6SZBNALD.js.map +1 -0
  15. package/dist/{chunk-6OYICQSJ.js → chunk-CZAMYVOP.js} +9 -4
  16. package/dist/chunk-CZAMYVOP.js.map +1 -0
  17. package/dist/chunk-DRDAEELJ.js +197 -0
  18. package/dist/chunk-DRDAEELJ.js.map +1 -0
  19. package/dist/{chunk-KKL3RYFQ.js → chunk-DYKG53IR.js} +2 -2
  20. package/dist/{chunk-HBFMTM2E.js → chunk-JTRTG5CU.js} +3 -3
  21. package/dist/{chunk-JCITWZQR.js → chunk-MNE374CZ.js} +2 -50
  22. package/dist/chunk-MNE374CZ.js.map +1 -0
  23. package/dist/chunk-MPBXOJ3N.js +51 -0
  24. package/dist/chunk-MPBXOJ3N.js.map +1 -0
  25. package/dist/{chunk-I55YWV6T.js → chunk-QWFHC3VH.js} +3 -3
  26. package/dist/{chunk-KLVMJYZZ.js → chunk-SEQA7MC3.js} +2 -2
  27. package/dist/chunk-UWJZXL2D.js +9 -0
  28. package/dist/chunk-UWJZXL2D.js.map +1 -0
  29. package/dist/cli/bin.js +36 -9
  30. package/dist/cli/bin.js.map +1 -1
  31. package/dist/cli/commands/audit.js +1 -1
  32. package/dist/{digest-LEMZS5ZK.js → digest-75FWSC74.js} +9 -6
  33. package/dist/{digest-LEMZS5ZK.js.map → digest-75FWSC74.js.map} +1 -1
  34. package/dist/favicon-SRHSJZ62.js +42 -0
  35. package/dist/favicon-SRHSJZ62.js.map +1 -0
  36. package/dist/freeze-Y5OJFOST.js +442 -0
  37. package/dist/freeze-Y5OJFOST.js.map +1 -0
  38. package/dist/header-image-H4N54DHN.js +95 -0
  39. package/dist/header-image-H4N54DHN.js.map +1 -0
  40. package/dist/index.d.ts +63 -1
  41. package/dist/index.js +64 -16
  42. package/dist/index.js.map +1 -1
  43. package/dist/{init-7PTP4O2K.js → init-2KOISBCZ.js} +3 -3
  44. package/dist/{launch-W33FWWOZ.js → launch-K4EHKTXC.js} +7 -5
  45. package/dist/{launch-W33FWWOZ.js.map → launch-K4EHKTXC.js.map} +1 -1
  46. package/dist/{orchestrate-NYRYEKQM.js → orchestrate-CPR6KVS2.js} +4 -3
  47. package/dist/{preflight-JCQFKB6M.js → preflight-QRRMPI7K.js} +5 -4
  48. package/dist/{preflight-JCQFKB6M.js.map → preflight-QRRMPI7K.js.map} +1 -1
  49. package/dist/{report-POHQEV6F.js → report-SLGBDJ6O.js} +8 -6
  50. package/dist/{report-POHQEV6F.js.map → report-SLGBDJ6O.js.map} +1 -1
  51. package/dist/reports/header-image/assets/plate.png +0 -0
  52. package/dist/run-migration-WP57U5XO.js +9 -0
  53. package/dist/run-migration-WP57U5XO.js.map +1 -0
  54. package/dist/{selftest-OO2WDSDM.js → selftest-RVASRO3Z.js} +8 -6
  55. package/dist/{selftest-OO2WDSDM.js.map → selftest-RVASRO3Z.js.map} +1 -1
  56. package/dist/webflow-624FEBFD.js +548 -0
  57. package/dist/webflow-624FEBFD.js.map +1 -0
  58. package/package.json +3 -3
  59. package/dist/blux-C24OVDDW.js.map +0 -1
  60. package/dist/chunk-6OYICQSJ.js.map +0 -1
  61. package/dist/chunk-JCITWZQR.js.map +0 -1
  62. package/dist/chunk-NUIBQPB3.js.map +0 -1
  63. package/dist/chunk-PERP7LSS.js.map +0 -1
  64. package/dist/run-migration-LJNNZWIV.js.map +0 -1
  65. /package/dist/{chunk-KKL3RYFQ.js.map → chunk-DYKG53IR.js.map} +0 -0
  66. /package/dist/{chunk-HBFMTM2E.js.map → chunk-JTRTG5CU.js.map} +0 -0
  67. /package/dist/{chunk-I55YWV6T.js.map → chunk-QWFHC3VH.js.map} +0 -0
  68. /package/dist/{chunk-KLVMJYZZ.js.map → chunk-SEQA7MC3.js.map} +0 -0
  69. /package/dist/{init-7PTP4O2K.js.map → init-2KOISBCZ.js.map} +0 -0
  70. /package/dist/{orchestrate-NYRYEKQM.js.map → orchestrate-CPR6KVS2.js.map} +0 -0
@@ -0,0 +1,442 @@
1
+ import {
2
+ FROZEN_PAGE_TYPE,
3
+ extractMapConfig,
4
+ frozenPageCustomType
5
+ } from "./chunk-DRDAEELJ.js";
6
+ import {
7
+ CDN_HOSTS
8
+ } from "./chunk-SL24DCGC.js";
9
+
10
+ // src/blux/freeze/index.ts
11
+ import { mkdir, writeFile, readFile as readFile2 } from "fs/promises";
12
+ import { join as join2 } from "path";
13
+
14
+ // src/blux/freeze/settle.ts
15
+ import { createServer } from "http";
16
+ import { readFile } from "fs/promises";
17
+ import { dirname, extname, join, normalize, sep } from "path";
18
+ import { chromium } from "@playwright/test";
19
+ var MIME = {
20
+ ".html": "text/html; charset=utf-8",
21
+ ".css": "text/css",
22
+ ".js": "text/javascript",
23
+ ".json": "application/json",
24
+ ".png": "image/png",
25
+ ".jpg": "image/jpeg",
26
+ ".jpeg": "image/jpeg",
27
+ ".gif": "image/gif",
28
+ ".svg": "image/svg+xml",
29
+ ".webp": "image/webp",
30
+ ".ico": "image/x-icon",
31
+ ".woff": "font/woff",
32
+ ".woff2": "font/woff2"
33
+ };
34
+ async function serveExport(dir) {
35
+ const root = normalize(dir);
36
+ const server = createServer((req, res) => {
37
+ void (async () => {
38
+ try {
39
+ const path = decodeURIComponent((req.url ?? "/").split(/[?#]/)[0]);
40
+ const rel = path === "/" ? "index.html" : path.slice(1);
41
+ const file = normalize(join(root, rel));
42
+ if (!file.startsWith(root + sep)) throw new Error("traversal");
43
+ const body = await readFile(file);
44
+ res.writeHead(200, {
45
+ "content-type": MIME[extname(file)] ?? "application/octet-stream"
46
+ });
47
+ res.end(body);
48
+ } catch {
49
+ res.writeHead(404);
50
+ res.end();
51
+ }
52
+ })();
53
+ });
54
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
55
+ const address = server.address();
56
+ if (address === null || typeof address === "string")
57
+ throw new Error("export server failed to bind");
58
+ return { url: `http://127.0.0.1:${address.port}/`, server };
59
+ }
60
+ async function auditAnchors(page) {
61
+ const ns = await page.evaluate(() => {
62
+ const found = /* @__PURE__ */ new Set();
63
+ for (const a of document.querySelectorAll('a[href^="/#"]')) {
64
+ const m = /^\/#(\d+)$/.exec(a.getAttribute("href") ?? "");
65
+ if (m?.[1]) found.add(m[1]);
66
+ }
67
+ return [...found];
68
+ });
69
+ if (ns.length === 0) return {};
70
+ const instrumented = await page.evaluate(() => {
71
+ const w = window;
72
+ const proto = w.DANDOM?.prototype;
73
+ if (!proto?.scrollPageToTarget) return false;
74
+ w.__rdAnchorCalls = [];
75
+ proto.scrollPageToTarget = function(t) {
76
+ w.__rdAnchorCalls?.push(t);
77
+ return this;
78
+ };
79
+ return true;
80
+ });
81
+ if (!instrumented) return {};
82
+ const targets = {};
83
+ for (const n of ns) {
84
+ const count = await page.evaluate(
85
+ (nn) => document.querySelectorAll(`a[href="/#${nn}"]`).length,
86
+ n
87
+ );
88
+ const seen = [];
89
+ for (let i = 0; i < count; i++) {
90
+ await page.evaluate(
91
+ ({ nn, idx }) => {
92
+ const w = window;
93
+ w.__rdAnchorCalls = [];
94
+ w.alreadyScrolling = false;
95
+ history.replaceState(null, "", "/");
96
+ document.querySelectorAll(`a[href="/#${nn}"]`)[idx]?.click();
97
+ },
98
+ { nn: n, idx: i }
99
+ );
100
+ await page.waitForTimeout(400);
101
+ const first = await page.evaluate(
102
+ () => window.__rdAnchorCalls?.[0] ?? null
103
+ );
104
+ if (first) seen.push(first);
105
+ }
106
+ const distinct = [...new Set(seen)];
107
+ if (distinct.length > 1) {
108
+ console.warn(
109
+ `[freeze] anchor /#${n}: links disagree on target (${distinct.join(", ")}) \u2014 baking "${seen[0]}" for all; verify that page's nav fidelity by hand`
110
+ );
111
+ }
112
+ if (seen[0]) targets[n] = seen[0];
113
+ }
114
+ return targets;
115
+ }
116
+ async function settleExport(indexHtmlPath, opts = {}) {
117
+ const width = opts.width ?? 1440;
118
+ const height = opts.height ?? 1e3;
119
+ const step = opts.step ?? 500;
120
+ const browser = await chromium.launch();
121
+ try {
122
+ const page = await browser.newPage({ viewport: { width, height } });
123
+ await page.goto(`file://${indexHtmlPath}`, {
124
+ waitUntil: "networkidle",
125
+ timeout: 6e4
126
+ });
127
+ const total = await page.evaluate(() => document.body.scrollHeight);
128
+ for (let y = 0; y < total + 1e3; y += step) {
129
+ await page.evaluate((yy) => window.scrollTo(0, yy), y);
130
+ await page.waitForTimeout(120);
131
+ }
132
+ await page.evaluate(() => window.scrollTo(0, 0));
133
+ await page.waitForTimeout(1200);
134
+ const html = await page.content();
135
+ const { url, server } = await serveExport(dirname(indexHtmlPath));
136
+ try {
137
+ const auditPage = await browser.newPage({
138
+ viewport: { width, height }
139
+ });
140
+ await auditPage.route(
141
+ "**/*",
142
+ (route) => void (["image", "media", "font"].includes(route.request().resourceType()) ? route.abort() : route.continue())
143
+ );
144
+ await auditPage.goto(url, { waitUntil: "load", timeout: 6e4 });
145
+ await auditPage.waitForTimeout(800);
146
+ const anchorTargets = await auditAnchors(auditPage);
147
+ await auditPage.close();
148
+ return { html, anchorTargets };
149
+ } finally {
150
+ await new Promise((resolve) => server.close(() => resolve()));
151
+ }
152
+ } finally {
153
+ await browser.close();
154
+ }
155
+ }
156
+
157
+ // src/blux/freeze/map-placeholder.ts
158
+ import { parse } from "node-html-parser";
159
+ var MID_RE = /[?&]mid=([\w-]+)/;
160
+ function mapPlaceholder(html, exportHtml) {
161
+ const root = parse(html);
162
+ const mid = exportHtml.match(MID_RE)?.[1];
163
+ for (const el of root.querySelectorAll("[id]")) {
164
+ const id = el.getAttribute("id") ?? "";
165
+ if (!/map/i.test(id)) continue;
166
+ if (!el.querySelector(".gm-style")) continue;
167
+ el.set_content("");
168
+ el.classList.add("blux-frozen-map");
169
+ if (mid) el.setAttribute("data-kml-mid", mid);
170
+ }
171
+ return root.toString();
172
+ }
173
+
174
+ // src/blux/freeze/slider-pin.ts
175
+ import { parse as parse2, HTMLElement } from "node-html-parser";
176
+ function pinSliders(html) {
177
+ const root = parse2(html);
178
+ for (const slider of root.querySelectorAll(".caslider")) {
179
+ const columns = parseInt(slider.getAttribute("data-columns") ?? "1", 10) || 1;
180
+ if (columns > 1) continue;
181
+ const slides = slider.childNodes.filter((n) => n instanceof HTMLElement);
182
+ slides.forEach((slide, i) => {
183
+ const style = slide.getAttribute("style") ?? "";
184
+ const pin = i === 0 ? "display:block;transform:translateX(0%);opacity:1" : "display:none";
185
+ slide.setAttribute("style", style ? `${style};${pin}` : pin);
186
+ });
187
+ }
188
+ return root.toString();
189
+ }
190
+
191
+ // src/blux/freeze/resolve-anchors.ts
192
+ var HASHLINK_RE = /href="\/#(\d+)"/g;
193
+ function resolveAnchors(html, targets = {}) {
194
+ return html.replace(
195
+ HASHLINK_RE,
196
+ (_full, n) => `href="#${targets[n] ?? `page-block-${n}`}"`
197
+ );
198
+ }
199
+
200
+ // src/blux/freeze/bake-images.ts
201
+ import { parse as parse3 } from "node-html-parser";
202
+
203
+ // src/blux/freeze/types.ts
204
+ var EXTRA_SLOT_PREFIX = "x.";
205
+ function tokenFor(kind, key) {
206
+ return `\u27E6${kind === "text" ? "t" : "i"}:${key}\u27E7`;
207
+ }
208
+ function slotKey(section, kind, n) {
209
+ return `${section}.${kind === "text" ? "t" : "i"}${n}`;
210
+ }
211
+
212
+ // src/blux/freeze/section.ts
213
+ function sectionIndexOf(root) {
214
+ const index = /* @__PURE__ */ new Map();
215
+ root.querySelectorAll("section").forEach((s, i) => index.set(s, i));
216
+ return index;
217
+ }
218
+ function sectionKeyOf(el, sectionIndex) {
219
+ let a = el;
220
+ while (a) {
221
+ if (a.tagName === "SECTION" && sectionIndex.has(a)) {
222
+ return `s${sectionIndex.get(a)}`;
223
+ }
224
+ a = a.parentNode;
225
+ }
226
+ return "h";
227
+ }
228
+
229
+ // src/blux/freeze/bake-images.ts
230
+ var isCdnUrl = (v) => CDN_HOSTS.some((h) => v.includes(h));
231
+ function stripUrlHintAttrs(root) {
232
+ for (const el of root.querySelectorAll("*")) {
233
+ for (const name of Object.keys(el.attributes)) {
234
+ if (name === "data-og-src" || name.startsWith("data-swaps")) {
235
+ el.removeAttribute(name);
236
+ }
237
+ }
238
+ }
239
+ }
240
+ function bakeImages(html) {
241
+ const root = parse3(html);
242
+ const sectionIndex = sectionIndexOf(root);
243
+ const counters = /* @__PURE__ */ new Map();
244
+ const slots = [];
245
+ const nextKey = (el) => {
246
+ const section = sectionKeyOf(el, sectionIndex);
247
+ const n = counters.get(section) ?? 0;
248
+ counters.set(section, n + 1);
249
+ return slotKey(section, "image", n);
250
+ };
251
+ for (const el of root.querySelectorAll("[data-media]")) {
252
+ const base = el.getAttribute("data-base");
253
+ if (!base) continue;
254
+ const media = el.getAttribute("data-media");
255
+ if (!media) continue;
256
+ const isBg = el.getAttribute("data-bgmedia") != null;
257
+ const size = el.getAttribute("data-size");
258
+ const sizeSeg = size ? `w:${size}/` : isBg ? "w:1600/" : "";
259
+ const url = `${base}${sizeSeg}${media}`;
260
+ const key = nextKey(el);
261
+ slots.push({ key, kind: "image", url, section: sectionKeyOf(el, sectionIndex) });
262
+ const decl = `background-image:url(${tokenFor("image", key)})`;
263
+ const style = el.getAttribute("style");
264
+ el.setAttribute("style", style ? `${style};${decl}` : decl);
265
+ el.removeAttribute("data-base");
266
+ }
267
+ for (const el of root.querySelectorAll("[src],[href],[poster]")) {
268
+ for (const attr of ["src", "href", "poster"]) {
269
+ const v = el.getAttribute(attr);
270
+ if (!v || !isCdnUrl(v)) continue;
271
+ const key = nextKey(el);
272
+ slots.push({ key, kind: "image", url: v, section: sectionKeyOf(el, sectionIndex) });
273
+ el.setAttribute(attr, tokenFor("image", key));
274
+ }
275
+ }
276
+ stripUrlHintAttrs(root);
277
+ return { html: root.toString(), slots };
278
+ }
279
+
280
+ // src/blux/freeze/tokenize-text.ts
281
+ import { parse as parse4, NodeType } from "node-html-parser";
282
+ var SKIP = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "NOSCRIPT", "HEAD", "TITLE", "SVG"]);
283
+ function tokenizeText(html) {
284
+ const root = parse4(html);
285
+ const sectionIndex = sectionIndexOf(root);
286
+ const counters = /* @__PURE__ */ new Map();
287
+ const slots = [];
288
+ const walk = (el) => {
289
+ for (const child of el.childNodes) {
290
+ if (child.nodeType === NodeType.TEXT_NODE) {
291
+ const raw = child.rawText;
292
+ if (raw && child.text.trim() !== "") {
293
+ const section = sectionKeyOf(el, sectionIndex);
294
+ const n = counters.get(section) ?? 0;
295
+ counters.set(section, n + 1);
296
+ const key = slotKey(section, "text", n);
297
+ slots.push({ key, kind: "text", text: raw, section });
298
+ child.rawText = tokenFor("text", key);
299
+ }
300
+ } else if (child.nodeType === NodeType.ELEMENT_NODE) {
301
+ const elChild = child;
302
+ if (!SKIP.has(elChild.tagName)) walk(elChild);
303
+ }
304
+ }
305
+ };
306
+ walk(root);
307
+ return { html: root.toString(), slots };
308
+ }
309
+
310
+ // src/blux/freeze/finalize.ts
311
+ import { parse as parse5 } from "node-html-parser";
312
+ var REVEAL_FORCE_CSS = ".block-effects{opacity:1!important;transform:none!important;animation:none!important;visibility:visible!important}";
313
+ function finalize(html) {
314
+ const root = parse5(html);
315
+ const title = root.querySelector("title")?.text?.trim() ?? "";
316
+ const metaOf = (prop) => root.querySelector(`meta[property="${prop}"]`)?.getAttribute("content") ?? void 0;
317
+ const metaTitle = metaOf("og:title") ?? (title || void 0);
318
+ const metaImageUrl = metaOf("og:image");
319
+ const fontLinks = root.querySelectorAll('link[rel="stylesheet"]').map((l) => l.getAttribute("href")).filter((h) => !!h);
320
+ const styles = [];
321
+ for (const s of root.querySelectorAll("style")) {
322
+ styles.push(s.innerHTML);
323
+ s.remove();
324
+ }
325
+ for (const sc of root.querySelectorAll("script")) sc.remove();
326
+ for (const holder of root.querySelectorAll(".load-media-holder, .load-media-element")) {
327
+ holder.remove();
328
+ }
329
+ const body = root.querySelector("body");
330
+ const inner = body ? body.innerHTML : root.toString();
331
+ const bodyClass = body?.getAttribute("class") ?? "";
332
+ const bodyStyle = body?.getAttribute("style") ?? "";
333
+ const cls = `frozen-root ${bodyClass}`.trim();
334
+ const styleAttr = bodyStyle ? ` style="${bodyStyle}"` : "";
335
+ const templateHtml = `<div class="${cls}"${styleAttr}>${inner}</div>`;
336
+ const styleCss = [...styles, REVEAL_FORCE_CSS].join("\n");
337
+ return { templateHtml, styleCss, title, metaTitle, metaImageUrl, fontLinks };
338
+ }
339
+
340
+ // src/blux/freeze/extra-slots.ts
341
+ function fail(path, why) {
342
+ throw new Error(`extra slots: ${path} ${why}`);
343
+ }
344
+ function asString(v, path) {
345
+ if (typeof v !== "string" || v.trim() === "") {
346
+ fail(path, `must be a non-empty string (got ${JSON.stringify(v)})`);
347
+ }
348
+ return v;
349
+ }
350
+ function validateExtraSlots(file, derived) {
351
+ if (file === void 0 || file === null) return [];
352
+ if (typeof file !== "object" || Array.isArray(file)) {
353
+ fail("root", "must be an object with a `slots` array");
354
+ }
355
+ const slots = file.slots;
356
+ if (!Array.isArray(slots)) fail("slots", "must be an array");
357
+ const derivedKeys = new Set(derived.map((s) => s.key));
358
+ const seen = /* @__PURE__ */ new Set();
359
+ const out = [];
360
+ slots.forEach((raw, i) => {
361
+ const at = `slots[${i}]`;
362
+ if (typeof raw !== "object" || raw === null) fail(at, "must be an object");
363
+ const s = raw;
364
+ const key = asString(s.key, `${at}.key`);
365
+ if (!key.startsWith(EXTRA_SLOT_PREFIX)) {
366
+ fail(
367
+ `${at}.key`,
368
+ `must start with "${EXTRA_SLOT_PREFIX}" so it cannot collide with a derived slot (got "${key}")`
369
+ );
370
+ }
371
+ if (derivedKeys.has(key)) fail(`${at}.key`, `collides with a derived slot ("${key}")`);
372
+ if (seen.has(key)) fail(`${at}.key`, `is declared twice ("${key}")`);
373
+ seen.add(key);
374
+ const kind = s.kind;
375
+ if (kind !== "text" && kind !== "image") {
376
+ fail(`${at}.kind`, `must be "text" or "image" (got ${JSON.stringify(kind)})`);
377
+ }
378
+ const section = typeof s.section === "string" && s.section.trim() ? s.section : "x";
379
+ if (kind === "text") {
380
+ out.push({ key, kind, text: asString(s.text, `${at}.text`), section });
381
+ } else {
382
+ out.push({ key, kind, url: asString(s.url, `${at}.url`), section });
383
+ }
384
+ });
385
+ return out;
386
+ }
387
+
388
+ // src/blux/freeze/index.ts
389
+ async function freezeSite(opts) {
390
+ const { html: settled, anchorTargets } = await settleExport(opts.indexHtmlPath);
391
+ const exportHtml = await readFile2(opts.indexHtmlPath, "utf-8");
392
+ const mapped = mapPlaceholder(settled, exportHtml);
393
+ const pinned = pinSliders(mapped);
394
+ const anchored = resolveAnchors(pinned, anchorTargets);
395
+ const baked = bakeImages(anchored);
396
+ const tokenized = tokenizeText(baked.html);
397
+ const fin = finalize(tokenized.html);
398
+ const rawMap = extractMapConfig(exportHtml);
399
+ const mapConfig = rawMap && fin.templateHtml.includes(`id="${rawMap.mountId}"`) ? rawMap : void 0;
400
+ const derived = [...baked.slots, ...tokenized.slots];
401
+ const extra = validateExtraSlots(opts.extraSlots, derived);
402
+ return {
403
+ manifest: {
404
+ site: opts.site,
405
+ uid: opts.uid ?? "home",
406
+ title: fin.title,
407
+ metaTitle: fin.metaTitle,
408
+ metaImageUrl: fin.metaImageUrl,
409
+ fontLinks: fin.fontLinks,
410
+ slots: [...derived, ...extra]
411
+ },
412
+ templateHtml: fin.templateHtml,
413
+ styleCss: fin.styleCss,
414
+ mapConfig
415
+ };
416
+ }
417
+ async function emitFrozen(outDir, result) {
418
+ const { site, uid } = result.manifest;
419
+ const frozenDir = join2(outDir, "frozen");
420
+ await mkdir(frozenDir, { recursive: true });
421
+ const template = join2(frozenDir, `${uid}.html`);
422
+ const style = join2(frozenDir, `${uid}.style.css`);
423
+ const fonts = join2(frozenDir, `${uid}.fonts.json`);
424
+ const manifest = join2(outDir, `${site}.slots.json`);
425
+ await writeFile(template, result.templateHtml, "utf-8");
426
+ await writeFile(style, result.styleCss, "utf-8");
427
+ await writeFile(fonts, JSON.stringify(result.manifest.fontLinks, null, 2), "utf-8");
428
+ await writeFile(manifest, JSON.stringify(result.manifest, null, 2), "utf-8");
429
+ if (!result.mapConfig) return { template, style, fonts, manifest };
430
+ const map = join2(frozenDir, `${uid}.map.json`);
431
+ await writeFile(map, JSON.stringify(result.mapConfig, null, 2), "utf-8");
432
+ return { template, style, fonts, manifest, map };
433
+ }
434
+ export {
435
+ EXTRA_SLOT_PREFIX,
436
+ FROZEN_PAGE_TYPE,
437
+ emitFrozen,
438
+ freezeSite,
439
+ frozenPageCustomType,
440
+ validateExtraSlots
441
+ };
442
+ //# sourceMappingURL=freeze-Y5OJFOST.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/blux/freeze/index.ts","../src/blux/freeze/settle.ts","../src/blux/freeze/map-placeholder.ts","../src/blux/freeze/slider-pin.ts","../src/blux/freeze/resolve-anchors.ts","../src/blux/freeze/bake-images.ts","../src/blux/freeze/types.ts","../src/blux/freeze/section.ts","../src/blux/freeze/tokenize-text.ts","../src/blux/freeze/finalize.ts","../src/blux/freeze/extra-slots.ts"],"sourcesContent":["import { mkdir, writeFile, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { settleExport } from \"./settle.js\";\nimport { mapPlaceholder } from \"./map-placeholder.js\";\nimport { pinSliders } from \"./slider-pin.js\";\nimport { resolveAnchors } from \"./resolve-anchors.js\";\nimport { bakeImages } from \"./bake-images.js\";\nimport { tokenizeText } from \"./tokenize-text.js\";\nimport { finalize } from \"./finalize.js\";\nimport { extractMapConfig } from \"../grid/extract-map.js\";\nimport { validateExtraSlots } from \"./extra-slots.js\";\nimport type { FrozenResult } from \"./types.js\";\n\nexport type { Slot, FrozenManifest, FrozenResult, ExtraSlotsFile } from \"./types.js\";\nexport { frozenPageCustomType, FROZEN_PAGE_TYPE } from \"./frozen-page-type.js\";\nexport { validateExtraSlots } from \"./extra-slots.js\";\nexport { EXTRA_SLOT_PREFIX } from \"./types.js\";\n\nexport interface FreezeOptions {\n /** Path to the export's index.html. */\n indexHtmlPath: string;\n /** Site slug — names the emitted template/style/manifest files. */\n site: string;\n /** Prismic uid for the page doc (default \"home\"). */\n uid?: string;\n /** A site's own extra-slots declaration (parsed `ExtraSlotsFile`), for\n * editable content the template carries no token for. See types.ts. */\n extraSlots?: unknown;\n}\n\n/**\n * Freeze one Blux page into a byte-faithful template + editable slot manifest.\n * Order matters: bake images (their urls become slots) BEFORE tokenizing text,\n * so image tokens live in style attributes and never collide with text tokens.\n */\nexport async function freezeSite(opts: FreezeOptions): Promise<FrozenResult> {\n const { html: settled, anchorTargets } = await settleExport(opts.indexHtmlPath);\n // Settled-DOM repairs run before image/text tokenizing: swap the dead\n // Google-Map DOM for a placeholder (using the raw export for the KML mid),\n // pin each hero slider to slide 1, and bake nav hashlink targets (settle's\n // click audit measured them against the export's own runtime).\n const exportHtml = await readFile(opts.indexHtmlPath, \"utf-8\");\n const mapped = mapPlaceholder(settled, exportHtml);\n const pinned = pinSliders(mapped);\n const anchored = resolveAnchors(pinned, anchorTargets);\n const baked = bakeImages(anchored);\n const tokenized = tokenizeText(baked.html);\n const fin = finalize(tokenized.html);\n\n // The full map widget config (layers/toggles/styles) lives in the export's\n // initMap script — carried alongside the template so the render can hydrate\n // the real map. Only meaningful when its mount survived into the template.\n const rawMap = extractMapConfig(exportHtml);\n const mapConfig =\n rawMap && fin.templateHtml.includes(`id=\"${rawMap.mountId}\"`) ? rawMap : undefined;\n\n // Derived slots first, then the site's own. Validated against the derived\n // keys so a declaration can never shadow real page content.\n const derived = [...baked.slots, ...tokenized.slots];\n const extra = validateExtraSlots(opts.extraSlots, derived);\n\n return {\n manifest: {\n site: opts.site,\n uid: opts.uid ?? \"home\",\n title: fin.title,\n metaTitle: fin.metaTitle,\n metaImageUrl: fin.metaImageUrl,\n fontLinks: fin.fontLinks,\n slots: [...derived, ...extra],\n },\n templateHtml: fin.templateHtml,\n styleCss: fin.styleCss,\n mapConfig,\n };\n}\n\n/**\n * Write the freeze artifacts under `outDir`. Returns their paths.\n *\n * `frozen/` is site-repo-ready: uid-keyed `<uid>.html` / `<uid>.style.css` /\n * `<uid>.fonts.json` (+ `<uid>.map.json` when the export has a map widget),\n * copied verbatim into the site repo's `src/lib/blux-frozen/frozen/` — exactly\n * the names the starter's artifact globs load. The slots manifest stays\n * site-keyed at the out-dir root, where `blux migrate-frozen` finds it.\n */\nexport async function emitFrozen(\n outDir: string,\n result: FrozenResult,\n): Promise<{\n template: string;\n style: string;\n fonts: string;\n manifest: string;\n map?: string;\n}> {\n const { site, uid } = result.manifest;\n const frozenDir = join(outDir, \"frozen\");\n await mkdir(frozenDir, { recursive: true });\n const template = join(frozenDir, `${uid}.html`);\n const style = join(frozenDir, `${uid}.style.css`);\n const fonts = join(frozenDir, `${uid}.fonts.json`);\n const manifest = join(outDir, `${site}.slots.json`);\n await writeFile(template, result.templateHtml, \"utf-8\");\n await writeFile(style, result.styleCss, \"utf-8\");\n await writeFile(fonts, JSON.stringify(result.manifest.fontLinks, null, 2), \"utf-8\");\n await writeFile(manifest, JSON.stringify(result.manifest, null, 2), \"utf-8\");\n if (!result.mapConfig) return { template, style, fonts, manifest };\n const map = join(frozenDir, `${uid}.map.json`);\n await writeFile(map, JSON.stringify(result.mapConfig, null, 2), \"utf-8\");\n return { template, style, fonts, manifest, map };\n}\n","/// <reference lib=\"dom\" />\n// The evaluate() callbacks below run in the browser page context, so they use\n// DOM globals (document/window). The CLI's base tsconfig is Node-only (lib\n// ES2022); this directive adds DOM types for this file's type-checking.\nimport { createServer, type Server } from \"node:http\";\nimport { readFile } from \"node:fs/promises\";\nimport { dirname, extname, join, normalize, sep } from \"node:path\";\nimport { chromium, type Page } from \"@playwright/test\";\n\n// The Blux export's index.html renders its final layout only after its runtime\n// JS applies height/position adjustments. Freezing the RAW html renders the\n// wrong height (the-pointe: 16873px); freezing the JS-SETTLED DOM renders the\n// live height (15333px). So the freeze hydrates the page once in headless\n// chromium, scrolls top→bottom to trigger the layout pass, and snapshots the\n// settled DOM. (Proven in the design POC scratchpad/freeze-final.mjs.)\n\nexport interface SettleOptions {\n /** Layout viewport — Blux desktop layout is authored at 1440. */\n width?: number;\n height?: number;\n /** Scroll step in px (smaller = more reliable lazy triggers, slower). */\n step?: number;\n}\n\nexport interface SettledExport {\n /** The serialized settled DOM (full document HTML). */\n html: string;\n /**\n * Nav-anchor answer key, measured from the export's OWN runtime: hash index\n * → the id its JS actually scrolls to. Blux core maps `/#N` to\n * `page-block-N`, but site-specific custom scripts override that (the-pointe\n * embeds one sending any \"Contact Us\" link to `footer0`) — behavior no\n * static rule can recover, so it is recorded by clicking each hashlink with\n * `scrollPageToTarget` instrumented. Empty when the runtime is absent.\n */\n anchorTargets: Record<string, string>;\n}\n\n// Blux runtime globals reached from inside evaluate() callbacks. Types are\n// erased before serialization, so declaring this at module level is safe.\ntype BluxWindow = Window & {\n DANDOM?: { prototype: { scrollPageToTarget?: (t: string) => unknown } };\n __rdAnchorCalls?: string[];\n alreadyScrolling?: boolean;\n};\n\nconst MIME: Record<string, string> = {\n \".html\": \"text/html; charset=utf-8\",\n \".css\": \"text/css\",\n \".js\": \"text/javascript\",\n \".json\": \"application/json\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".svg\": \"image/svg+xml\",\n \".webp\": \"image/webp\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n};\n\n/**\n * One-shot local static server over the export dir, for the anchor audit ONLY.\n * The audit must run on an http origin: there `/#N` is a same-document hash\n * change (exactly the live site's semantics), while on `file://` it resolves\n * to the filesystem ROOT — a cross-document navigation that Blux's handler\n * chain triggers even past preventDefault + timer stubs, tearing the page down\n * mid-audit. Serving beats intercepting.\n */\nasync function serveExport(dir: string): Promise<{ url: string; server: Server }> {\n const root = normalize(dir);\n const server = createServer((req, res) => {\n void (async () => {\n try {\n const path = decodeURIComponent((req.url ?? \"/\").split(/[?#]/)[0]!);\n const rel = path === \"/\" ? \"index.html\" : path.slice(1);\n // Separator-anchored: a bare prefix test would also pass sibling\n // dirs that merely share the prefix (/exports/site-secret vs\n // /exports/site).\n const file = normalize(join(root, rel));\n if (!file.startsWith(root + sep)) throw new Error(\"traversal\");\n const body = await readFile(file);\n res.writeHead(200, {\n \"content-type\": MIME[extname(file)] ?? \"application/octet-stream\",\n });\n res.end(body);\n } catch {\n res.writeHead(404);\n res.end();\n }\n })();\n });\n await new Promise<void>((resolve) => server.listen(0, \"127.0.0.1\", resolve));\n const address = server.address();\n if (address === null || typeof address === \"string\")\n throw new Error(\"export server failed to bind\");\n return { url: `http://127.0.0.1:${address.port}/`, server };\n}\n\n/**\n * Click every distinct `/#N` hashlink on the SERVED page and record the first\n * target the site's runtime passes to `scrollPageToTarget` — first, because on\n * the live site later calls in a click's handler chain are gated by\n * `alreadyScrolling`, so the first call is the one users see. The scroll\n * itself is suppressed (the wrapper records and returns), so clicks leave no\n * scroll state behind; the settle snapshot is taken from a separate file://\n * page that the audit never touches.\n */\nasync function auditAnchors(page: Page): Promise<Record<string, string>> {\n const ns = await page.evaluate(() => {\n const found = new Set<string>();\n for (const a of document.querySelectorAll('a[href^=\"/#\"]')) {\n const m = /^\\/#(\\d+)$/.exec(a.getAttribute(\"href\") ?? \"\");\n if (m?.[1]) found.add(m[1]);\n }\n return [...found];\n });\n if (ns.length === 0) return {};\n\n const instrumented = await page.evaluate(() => {\n const w = window as unknown as BluxWindow;\n const proto = w.DANDOM?.prototype;\n if (!proto?.scrollPageToTarget) return false;\n w.__rdAnchorCalls = [];\n proto.scrollPageToTarget = function (t: string) {\n w.__rdAnchorCalls?.push(t);\n return this; // record only — never actually scroll\n };\n return true;\n });\n if (!instrumented) return {};\n\n const targets: Record<string, string> = {};\n for (const n of ns) {\n // Audit EVERY link sharing this index, not just the first: custom scripts\n // intercept by link semantics (the-pointe keys on \"Contact Us\" text), so\n // two /#N links can legitimately scroll to different targets. The bake is\n // per-index, so disagreement can't be represented — surface it loudly and\n // bake the first link's (document-order) behavior.\n const count = await page.evaluate(\n (nn) => document.querySelectorAll(`a[href=\"/#${nn}\"]`).length,\n n,\n );\n const seen: string[] = [];\n for (let i = 0; i < count; i++) {\n await page.evaluate(\n ({ nn, idx }) => {\n const w = window as unknown as BluxWindow;\n w.__rdAnchorCalls = [];\n w.alreadyScrolling = false;\n history.replaceState(null, \"\", \"/\");\n document.querySelectorAll<HTMLElement>(`a[href=\"/#${nn}\"]`)[idx]?.click();\n },\n { nn: n, idx: i },\n );\n // checkScrollHash defers its core scroll 100ms past the hashchange (and\n // re-navigates at +50ms); 400ms covers the whole handler chain.\n await page.waitForTimeout(400);\n const first = await page.evaluate(\n () => (window as unknown as BluxWindow).__rdAnchorCalls?.[0] ?? null,\n );\n if (first) seen.push(first);\n }\n const distinct = [...new Set(seen)];\n if (distinct.length > 1) {\n console.warn(\n `[freeze] anchor /#${n}: links disagree on target (${distinct.join(\", \")}) — ` +\n `baking \"${seen[0]}\" for all; verify that page's nav fidelity by hand`,\n );\n }\n if (seen[0]) targets[n] = seen[0];\n }\n return targets;\n}\n\n/**\n * Render a local `index.html` in headless chromium, let its JS settle the\n * layout, and return the serialized settled DOM plus the measured nav-anchor\n * answer key.\n */\nexport async function settleExport(\n indexHtmlPath: string,\n opts: SettleOptions = {},\n): Promise<SettledExport> {\n const width = opts.width ?? 1440;\n const height = opts.height ?? 1000;\n const step = opts.step ?? 500;\n\n const browser = await chromium.launch();\n try {\n const page = await browser.newPage({ viewport: { width, height } });\n await page.goto(`file://${indexHtmlPath}`, {\n waitUntil: \"networkidle\",\n timeout: 60000,\n });\n // Trigger the JS layout pass across the whole page.\n const total = await page.evaluate(() => document.body.scrollHeight);\n for (let y = 0; y < total + 1000; y += step) {\n await page.evaluate((yy) => window.scrollTo(0, yy), y);\n await page.waitForTimeout(120);\n }\n await page.evaluate(() => window.scrollTo(0, 0));\n await page.waitForTimeout(1200);\n const html = await page.content();\n\n // Anchor audit on a separate, HTTP-served copy (see serveExport). Media\n // is aborted for speed — the audit needs handlers, not layout.\n const { url, server } = await serveExport(dirname(indexHtmlPath));\n try {\n const auditPage = await browser.newPage({\n viewport: { width, height },\n });\n await auditPage.route(\n \"**/*\",\n (route) =>\n void ([\"image\", \"media\", \"font\"].includes(route.request().resourceType())\n ? route.abort()\n : route.continue()),\n );\n await auditPage.goto(url, { waitUntil: \"load\", timeout: 60000 });\n await auditPage.waitForTimeout(800);\n const anchorTargets = await auditAnchors(auditPage);\n await auditPage.close();\n return { html, anchorTargets };\n } finally {\n await new Promise<void>((resolve) => server.close(() => resolve()));\n }\n } finally {\n await browser.close();\n }\n}\n","import { parse } from \"node-html-parser\";\n\n// The frozen export's Google-Map band is a CUSTOM Google My Maps: a KmlLayer\n// keyed by a My Maps id (`…/kml?…&mid=<ID>`) rendered at runtime. With the Blux\n// JS stripped, the settled `gm-style` DOM Google left behind is dead (broken\n// tiles + CSP console noise). Replace it with a clean, height-preserving\n// placeholder that carries the `mid` so a future (production) hydrator can bring\n// the real map back once a domain-scoped Maps key exists.\n\n// The My Maps id, pulled from the export's KML url (deterministic, not from the\n// settled DOM which no longer holds it).\nconst MID_RE = /[?&]mid=([\\w-]+)/;\n\n/**\n * Convert each rendered Google-Map container into a `.blux-frozen-map`\n * placeholder. A container is an element whose `id` names a map (`/map/i`) AND\n * that actually rendered (has a `.gm-style` descendant); its dead children are\n * removed and the KML `mid` (if found in `exportHtml`) is attached. Its existing\n * id + inline height are kept so the band's box is unchanged. `exportHtml` is the\n * raw Blux export string.\n */\nexport function mapPlaceholder(html: string, exportHtml: string): string {\n const root = parse(html);\n const mid = exportHtml.match(MID_RE)?.[1];\n\n for (const el of root.querySelectorAll(\"[id]\")) {\n const id = el.getAttribute(\"id\") ?? \"\";\n if (!/map/i.test(id)) continue;\n if (!el.querySelector(\".gm-style\")) continue; // only actually-rendered maps\n el.set_content(\"\"); // drop the dead gm-style DOM\n el.classList.add(\"blux-frozen-map\");\n if (mid) el.setAttribute(\"data-kml-mid\", mid);\n }\n\n return root.toString();\n}\n","import { parse, HTMLElement } from \"node-html-parser\";\n\n// Blux single-column hero sliders (`.caslider` with `data-columns` 1 or absent)\n// animate through their slides at runtime. The settle snapshot freezes whichever\n// slide happened to be active, which is not reproducible across re-freezes. Pin\n// such a slider to its first slide: force slide 1 visible + in position, hide the\n// rest with `display:none`. Appended declarations win (inline-style last-wins),\n// so the first slide's own styles are preserved and height is unchanged (slide 1\n// keeps the band's natural height).\n//\n// A MULTI-column slider (`data-columns` >= 2) shows several slides at once — the\n// settled snapshot already renders those columns faithfully — so pinning would\n// hide the simultaneously-visible columns and regress the band. Leave those\n// untouched; the pin is only for the single-column hero case.\n\n/**\n * Pin each single-column `.caslider` to its first slide, deterministically.\n * Multi-column sliders (`data-columns` >= 2) are left as settled.\n */\nexport function pinSliders(html: string): string {\n const root = parse(html);\n\n for (const slider of root.querySelectorAll(\".caslider\")) {\n const columns = parseInt(slider.getAttribute(\"data-columns\") ?? \"1\", 10) || 1;\n if (columns > 1) continue; // multi-column carousel: keep the settled columns\n const slides = slider.childNodes.filter((n): n is HTMLElement => n instanceof HTMLElement);\n slides.forEach((slide, i) => {\n const style = slide.getAttribute(\"style\") ?? \"\";\n const pin = i === 0 ? \"display:block;transform:translateX(0%);opacity:1\" : \"display:none\";\n slide.setAttribute(\"style\", style ? `${style};${pin}` : pin);\n });\n }\n\n return root.toString();\n}\n","// Blux nav anchors are JS-driven: core runtime maps `href=\"/#N\"` to the band\n// id `page-block-N` — but site-specific custom scripts embedded in the export\n// can send a link anywhere (the-pointe's sends \"Contact Us\" to `footer0`), so\n// the authoritative mapping is the answer key MEASURED from the export's own\n// runtime by settle's click audit. With the runtime stripped those hrefs are\n// dead; bake the resolved target into the frozen markup. Unmeasured indexes\n// fall back to Blux core's `page-block-N` (when that band is missing, Blux\n// core no-ops — exactly what a dead in-page anchor does, so the fallback is\n// faithful either way). Digit-only fragments only — real named anchors\n// (`/#about`) are untouched. The starter's render-time enhance layer keeps its\n// own `/#N` rewrite as a repair path for pre-bake artifacts; baked hrefs no\n// longer match it.\n\nconst HASHLINK_RE = /href=\"\\/#(\\d+)\"/g;\n\nexport function resolveAnchors(html: string, targets: Record<string, string> = {}): string {\n return html.replace(\n HASHLINK_RE,\n (_full, n: string) => `href=\"#${targets[n] ?? `page-block-${n}`}\"`,\n );\n}\n","import { parse, HTMLElement } from \"node-html-parser\";\nimport { slotKey, tokenFor, type Slot } from \"./types.js\";\nimport { sectionIndexOf, sectionKeyOf } from \"./section.js\";\nimport { CDN_HOSTS } from \"../assets.js\";\n\nconst isCdnUrl = (v: string): boolean => CDN_HOSTS.some((h) => v.includes(h));\n\n// Blux media elements carry the image as data-attributes, not a live src: the\n// runtime reads `data-base` (CDN folder) + `data-size` (width) + `data-media`\n// (filename) and paints a background on a lazy `.load-media-element` span\n// (loading/unloading per viewport). We assemble the url deterministically —\n// `{data-base}w:{data-size}/{data-media}` (verified 200) — and bake it as an\n// inline background-image token so no runtime is needed. `data-bgmedia` is a\n// full-bleed layer whose filename wins over `data-media`. A second pass catches\n// any other CDN url still living in src/href/poster (e.g. a <video>), so the\n// frozen template leaks no CDN host at all.\n\n// Runtime-only url hints the (removed) Blux JS used; not rendered — drop them so\n// they don't leak a CDN host.\nfunction stripUrlHintAttrs(root: HTMLElement): void {\n for (const el of root.querySelectorAll(\"*\")) {\n for (const name of Object.keys(el.attributes)) {\n if (name === \"data-og-src\" || name.startsWith(\"data-swaps\")) {\n el.removeAttribute(name);\n }\n }\n }\n}\n\n/**\n * Tokenize every media url in the document as a `⟦i:KEY⟧` image slot. Pass 1:\n * data-attribute backgrounds → inline `background-image:url(token)`. Pass 2: any\n * remaining CDN url in `src`/`href`/`poster` → bare `token`. Keys are\n * `s{section}.i{n}` in document order.\n */\nexport function bakeImages(html: string): { html: string; slots: Slot[] } {\n const root = parse(html);\n const sectionIndex = sectionIndexOf(root);\n const counters = new Map<string, number>();\n const slots: Slot[] = [];\n\n const nextKey = (el: HTMLElement): string => {\n const section = sectionKeyOf(el, sectionIndex);\n const n = counters.get(section) ?? 0;\n counters.set(section, n + 1);\n return slotKey(section, \"image\", n);\n };\n\n // Pass 1: data-* backgrounds. The filename is ALWAYS `data-media`;\n // `data-bgmedia` is a boolean flag (\"1\") marking a full-bleed layer — it is\n // NOT a filename. When an element is unsized we default full-bleed layers to a\n // wide variant and inline images to the raw file.\n for (const el of root.querySelectorAll(\"[data-media]\")) {\n const base = el.getAttribute(\"data-base\");\n if (!base) continue;\n const media = el.getAttribute(\"data-media\");\n if (!media) continue;\n const isBg = el.getAttribute(\"data-bgmedia\") != null;\n const size = el.getAttribute(\"data-size\");\n const sizeSeg = size ? `w:${size}/` : isBg ? \"w:1600/\" : \"\";\n const url = `${base}${sizeSeg}${media}`;\n\n const key = nextKey(el);\n slots.push({ key, kind: \"image\", url, section: sectionKeyOf(el, sectionIndex) });\n const decl = `background-image:url(${tokenFor(\"image\", key)})`;\n const style = el.getAttribute(\"style\");\n el.setAttribute(\"style\", style ? `${style};${decl}` : decl);\n el.removeAttribute(\"data-base\");\n }\n\n // Pass 2: any remaining CDN url in src/href/poster (video, source, links…).\n for (const el of root.querySelectorAll(\"[src],[href],[poster]\")) {\n for (const attr of [\"src\", \"href\", \"poster\"] as const) {\n const v = el.getAttribute(attr);\n if (!v || !isCdnUrl(v)) continue;\n const key = nextKey(el);\n slots.push({ key, kind: \"image\", url: v, section: sectionKeyOf(el, sectionIndex) });\n el.setAttribute(attr, tokenFor(\"image\", key));\n }\n }\n\n stripUrlHintAttrs(root);\n return { html: root.toString(), slots };\n}\n","// Frozen-page freeze: shared types + token contract. A frozen site keeps the\n// Blux export's own markup as the layout (byte-faithful) and exposes only the\n// editable leaves — text runs and image urls — as Prismic slots. The template\n// carries a placeholder token per slot; the render substitutes current Prismic\n// values back in. See docs/superpowers/specs/2026-07-24-blux-frozen-page-design.md.\n\nimport type { MapConfig } from \"../grid/extract-map.js\";\n\nexport type SlotKind = \"text\" | \"image\";\n\nexport interface Slot {\n /** Stable, document-order key `s{section}.{t|i}{n}` (see `slotKey`). */\n key: string;\n kind: SlotKind;\n /** Present when kind === \"text\": the original text-node value. */\n text?: string;\n /** Present when kind === \"image\": the assembled (cloudfront) image url. */\n url?: string;\n /** Grouping label for editor sanity — the owning section key. */\n section: string;\n}\n\n/**\n * Slots a SITE declares, on top of the ones the freeze derives from the export.\n *\n * Every derived slot exists because the template carries a token for it. Some\n * editable content has no token and never can: the render composes it itself,\n * from markup the export never had. Two real cases on the-pointe — a `<video>`\n * poster (the export ships none, so there is no attribute to tokenize) and a\n * data panel the export baked as a flattened PNG (rebuilt as real text, so the\n * text exists only in the render).\n *\n * Without this, that content can only live in committed files and a deploy is\n * required to change a leasing number. Declaring it here puts it in Prismic\n * alongside everything else, and — because `blux migrate-frozen` PUTs the whole\n * document — keeps it there across re-migrations, which a row added by hand in\n * the Prismic UI would not survive.\n *\n * Kept generic on purpose: the tool serves several frozen sites, so it knows\n * only that extra slots exist, never what any one site puts in them.\n */\nexport interface ExtraSlotsFile {\n /** Free-form note for whoever opens the file; ignored by the freeze. */\n comment?: string;\n slots: Slot[];\n}\n\n/** Prefix reserved for site-declared slots, so a synthetic key can never\n * collide with a derived `s{n}.t{n}` / `h.i{n}` one. */\nexport const EXTRA_SLOT_PREFIX = \"x.\";\n\nexport interface FrozenManifest {\n site: string;\n uid: string;\n title: string;\n metaTitle?: string | undefined;\n metaImageUrl?: string | undefined;\n /** External stylesheet hrefs from the export <head> (e.g. Google Fonts) that\n * the render must re-inject — font metrics are load-bearing for layout. */\n fontLinks: string[];\n slots: Slot[];\n}\n\n/** The emitted freeze artifact: the manifest plus the repo files' bodies. */\nexport interface FrozenResult {\n manifest: FrozenManifest;\n /** Tokenized `<body>` innerHTML — the repo template. */\n templateHtml: string;\n /** The extracted `<style>` block text — the repo style file. */\n styleCss: string;\n /** Present when the export carries a Blux map widget whose mount survives\n * in the template — emitted as `frozen/<uid>.map.json`, hydrated by the\n * starter's frozen-map layer. */\n mapConfig?: MapConfig | undefined;\n}\n\n/** The placeholder a slot's value is substituted for at render time.\n * Text: replaces the whole text node. Image: sits inside `url(...)`. */\nexport function tokenFor(kind: SlotKind, key: string): string {\n return `⟦${kind === \"text\" ? \"t\" : \"i\"}:${key}⟧`;\n}\n\n/** Build a slot key. `section` is the nearest-ancestor <section> index (or\n * \"h\" for chrome above the first section); `n` is a per-kind counter. */\nexport function slotKey(section: string, kind: SlotKind, n: number): string {\n return `${section}.${kind === \"text\" ? \"t\" : \"i\"}${n}`;\n}\n\n/** Matches any slot token; group 1 = \"t\"|\"i\", group 2 = key. Global — callers\n * that reuse it across `.test()`/`.replace()` should construct a fresh copy or\n * reset `lastIndex`. Use `TOKEN_RE()` to get a fresh instance. */\nexport const TOKEN_RE = (): RegExp => /⟦([ti]):([^⟧]+)⟧/g;\n","import { HTMLElement } from \"node-html-parser\";\n\n/** Map each <section> element to its document-order index. */\nexport function sectionIndexOf(root: HTMLElement): Map<HTMLElement, number> {\n const index = new Map<HTMLElement, number>();\n root.querySelectorAll(\"section\").forEach((s, i) => index.set(s, i));\n return index;\n}\n\n/** Nearest-ancestor <section> key (`s{index}`), or `h` for chrome (nav/footer)\n * that lives above/outside any <section>. */\nexport function sectionKeyOf(el: HTMLElement, sectionIndex: Map<HTMLElement, number>): string {\n let a: HTMLElement | null | undefined = el;\n while (a) {\n if (a.tagName === \"SECTION\" && sectionIndex.has(a)) {\n return `s${sectionIndex.get(a)}`;\n }\n a = a.parentNode as HTMLElement | null | undefined;\n }\n return \"h\";\n}\n","import { parse, HTMLElement, NodeType } from \"node-html-parser\";\nimport { slotKey, tokenFor, type Slot } from \"./types.js\";\nimport { sectionIndexOf, sectionKeyOf } from \"./section.js\";\n\n// Elements whose text is not editable page copy.\nconst SKIP = new Set([\"SCRIPT\", \"STYLE\", \"NOSCRIPT\", \"HEAD\", \"TITLE\", \"SVG\"]);\n\n/**\n * Replace every text node that carries CONTENT with a `⟦t:KEY⟧` token and\n * collect the originals as text slots. Keys are `s{section}.t{n}` in document\n * order so the template + manifest stay stable across re-freezes (golden\n * canary). The raw source text is preserved verbatim (entities and surrounding\n * whitespace) so a frozen render is byte-faithful before any Prismic edit.\n *\n * \"Carries content\" is decided on the DECODED text, not the raw source. Page\n * builders emit whitespace-only leaves as layout — a list item or table cell\n * holding `&nbsp;` purely to occupy a line — and those must stay literal in the\n * template rather than becoming CMS fields, for two reasons:\n *\n * 1. They are not editable copy. Nobody wants a Prismic field whose only\n * correct value is \"one blank line\".\n * 2. Prismic Rich Text CANNOT store a whitespace-only value. It round-trips to\n * \"\", the row collapses to its padding, and the page silently loses a line\n * of vertical rhythm — a defect that only shows up after the migration, on\n * the live site, in a place nobody thought to re-measure.\n *\n * `rawText.trim()` cannot see this: for a `&nbsp;` leaf it trims to the literal\n * string \"&nbsp;\", which is not empty, so the leaf looked like content. Testing\n * the decoded `.text` catches every spelling — `&nbsp;`, `&#160;`, `&#xa0;`,\n * `&emsp;`, `&thinsp;` — because JS `String.trim()` strips the whole Unicode\n * whitespace class. A real character such as `&amp;` still decodes to content\n * and is tokenized as before.\n */\nexport function tokenizeText(html: string): { html: string; slots: Slot[] } {\n const root = parse(html);\n const sectionIndex = sectionIndexOf(root);\n\n const counters = new Map<string, number>();\n const slots: Slot[] = [];\n\n const walk = (el: HTMLElement): void => {\n for (const child of el.childNodes) {\n if (child.nodeType === NodeType.TEXT_NODE) {\n const raw = child.rawText;\n // Decoded, so entity-encoded whitespace is treated as the whitespace it\n // is. A skipped leaf does not advance the section counter — same as a\n // plain-whitespace leaf, which this check has always skipped.\n if (raw && child.text.trim() !== \"\") {\n const section = sectionKeyOf(el, sectionIndex);\n const n = counters.get(section) ?? 0;\n counters.set(section, n + 1);\n const key = slotKey(section, \"text\", n);\n slots.push({ key, kind: \"text\", text: raw, section });\n child.rawText = tokenFor(\"text\", key);\n }\n } else if (child.nodeType === NodeType.ELEMENT_NODE) {\n const elChild = child as HTMLElement;\n if (!SKIP.has(elChild.tagName)) walk(elChild);\n }\n }\n };\n walk(root);\n\n return { html: root.toString(), slots };\n}\n","import { parse } from \"node-html-parser\";\n\n// Reveal effects hide content until the Blux runtime adds `block-effects-applied`\n// on scroll. With the runtime stripped, force the end-state so everything is\n// visible without JS. (`.block-effects{opacity:0}` is the initial rule.)\nexport const REVEAL_FORCE_CSS =\n \".block-effects{opacity:1!important;transform:none!important;animation:none!important;visibility:visible!important}\";\n\nexport interface Finalized {\n /** `<body>` inner HTML wrapped so the body's own class/style survive. */\n templateHtml: string;\n /** All page `<style>` blocks + the reveal-force override. */\n styleCss: string;\n title: string;\n metaTitle?: string | undefined;\n metaImageUrl?: string | undefined;\n /** External stylesheet hrefs (Google Fonts) to re-inject at render. */\n fontLinks: string[];\n}\n\n/**\n * Turn a tokenized settled document into the two repo artifacts: strip all\n * `<script>`, extract every `<style>` block (+ reveal-force) into a stylesheet,\n * and wrap the body's inner HTML in a `.frozen-root` div that carries the\n * body's own class/style (which `body.innerHTML` would otherwise drop).\n */\nexport function finalize(html: string): Finalized {\n const root = parse(html);\n const title = root.querySelector(\"title\")?.text?.trim() ?? \"\";\n const metaOf = (prop: string): string | undefined =>\n root.querySelector(`meta[property=\"${prop}\"]`)?.getAttribute(\"content\") ?? undefined;\n const metaTitle = metaOf(\"og:title\") ?? (title || undefined);\n const metaImageUrl = metaOf(\"og:image\");\n\n const fontLinks = root\n .querySelectorAll('link[rel=\"stylesheet\"]')\n .map((l) => l.getAttribute(\"href\"))\n .filter((h): h is string => !!h);\n\n const styles: string[] = [];\n for (const s of root.querySelectorAll(\"style\")) {\n styles.push(s.innerHTML);\n s.remove();\n }\n for (const sc of root.querySelectorAll(\"script\")) sc.remove();\n // Remove the Blux runtime's lazy media spans: they carry raw cloudfront urls\n // for whichever images were on-screen at snapshot (the rest unloaded), and the\n // deterministic baked token on the parent media div is what renders instead.\n for (const holder of root.querySelectorAll(\".load-media-holder, .load-media-element\")) {\n holder.remove();\n }\n\n const body = root.querySelector(\"body\");\n const inner = body ? body.innerHTML : root.toString();\n const bodyClass = body?.getAttribute(\"class\") ?? \"\";\n const bodyStyle = body?.getAttribute(\"style\") ?? \"\";\n const cls = `frozen-root ${bodyClass}`.trim();\n const styleAttr = bodyStyle ? ` style=\"${bodyStyle}\"` : \"\";\n const templateHtml = `<div class=\"${cls}\"${styleAttr}>${inner}</div>`;\n\n const styleCss = [...styles, REVEAL_FORCE_CSS].join(\"\\n\");\n return { templateHtml, styleCss, title, metaTitle, metaImageUrl, fontLinks };\n}\n","// Site-declared slots, merged into the freeze manifest alongside the derived\n// ones. See `ExtraSlotsFile` in types.ts for why they exist.\n//\n// This module is deliberately incurious about what a site declares: it checks\n// the shape, the reserved prefix and key uniqueness, and nothing else. The\n// freeze tool serves several frozen sites and must not learn any one site's\n// content model.\n\nimport { EXTRA_SLOT_PREFIX, type Slot } from \"./types.js\";\n\n/** Thrown with the offending path so a bad declaration names itself. */\nfunction fail(path: string, why: string): never {\n throw new Error(`extra slots: ${path} ${why}`);\n}\n\nfunction asString(v: unknown, path: string): string {\n if (typeof v !== \"string\" || v.trim() === \"\") {\n fail(path, `must be a non-empty string (got ${JSON.stringify(v)})`);\n }\n return v as string;\n}\n\n/**\n * Validate a site's extra-slots declaration against the slots the freeze\n * derived, returning the extras to append.\n *\n * A declaration that is wrong must stop the freeze rather than ship: these\n * slots become live CMS fields, and a key that collides with a derived one\n * would let a site-declared value silently overwrite real page content when\n * the render builds its value map.\n */\nexport function validateExtraSlots(file: unknown, derived: Slot[]): Slot[] {\n if (file === undefined || file === null) return [];\n if (typeof file !== \"object\" || Array.isArray(file)) {\n fail(\"root\", \"must be an object with a `slots` array\");\n }\n // Read as `unknown`, not as `ExtraSlotsFile` — the whole point here is that\n // the input has not been checked yet, and typing it as the target would make\n // every access below assert what this function is supposed to prove.\n const slots: unknown = (file as { slots?: unknown }).slots;\n if (!Array.isArray(slots)) fail(\"slots\", \"must be an array\");\n\n const derivedKeys = new Set(derived.map((s) => s.key));\n const seen = new Set<string>();\n const out: Slot[] = [];\n\n slots.forEach((raw, i) => {\n const at = `slots[${i}]`;\n if (typeof raw !== \"object\" || raw === null) fail(at, \"must be an object\");\n const s = raw as Record<string, unknown>;\n\n const key = asString(s.key, `${at}.key`);\n if (!key.startsWith(EXTRA_SLOT_PREFIX)) {\n fail(\n `${at}.key`,\n `must start with \"${EXTRA_SLOT_PREFIX}\" so it cannot collide with a derived slot (got \"${key}\")`,\n );\n }\n if (derivedKeys.has(key)) fail(`${at}.key`, `collides with a derived slot (\"${key}\")`);\n if (seen.has(key)) fail(`${at}.key`, `is declared twice (\"${key}\")`);\n seen.add(key);\n\n const kind = s.kind;\n if (kind !== \"text\" && kind !== \"image\") {\n fail(`${at}.kind`, `must be \"text\" or \"image\" (got ${JSON.stringify(kind)})`);\n }\n\n const section = typeof s.section === \"string\" && s.section.trim() ? s.section : \"x\";\n if (kind === \"text\") {\n out.push({ key, kind, text: asString(s.text, `${at}.text`), section });\n } else {\n out.push({ key, kind, url: asString(s.url, `${at}.url`), section });\n }\n });\n\n return out;\n}\n"],"mappings":"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","names":["readFile","join","parse","parse","parse","parse","parse","parse","readFile","join"]}
@@ -0,0 +1,95 @@
1
+ import {
2
+ generateHeaderImage
3
+ } from "./chunk-37S6L6G4.js";
4
+ import {
5
+ uploadAttachment
6
+ } from "./chunk-MPBXOJ3N.js";
7
+ import {
8
+ ACTIVE_STATUSES,
9
+ listWebsites,
10
+ siteSlug
11
+ } from "./chunk-GXVX4HUH.js";
12
+ import {
13
+ openBase,
14
+ readAirtableConfig
15
+ } from "./chunk-HN6I2XWI.js";
16
+ import "./chunk-6VAL7XJT.js";
17
+
18
+ // src/cli/commands/header-image.ts
19
+ import { mkdir, writeFile } from "fs/promises";
20
+ import { resolve } from "path";
21
+ function resolveTargets(rows, opts) {
22
+ if (opts.site) {
23
+ const want = siteSlug(opts.site);
24
+ return rows.filter((r) => siteSlug(r.name) === want);
25
+ }
26
+ if (!opts.all) return [];
27
+ return rows.filter((r) => {
28
+ if (!r.url) return false;
29
+ if (r.status === null || !ACTIVE_STATUSES.has(r.status)) return false;
30
+ return opts.force ? true : !r.headerImage;
31
+ });
32
+ }
33
+ function parseSettleMs(value) {
34
+ if (value === void 0) return void 0;
35
+ const n = Number(value);
36
+ if (!Number.isFinite(n) || n < 0) {
37
+ throw Object.assign(
38
+ new Error(`--settle-ms must be a non-negative number of milliseconds, got "${value}"`),
39
+ { exitCode: 2 }
40
+ );
41
+ }
42
+ return n;
43
+ }
44
+ async function generateForTargets(targets, opts, generate = generateHeaderImage) {
45
+ const outDir = resolve(opts.outDir ?? "reports");
46
+ const settleMs = parseSettleMs(opts.settleMs);
47
+ if (!opts.writeAirtable) await mkdir(outDir, { recursive: true });
48
+ const lines = [];
49
+ let failed = 0;
50
+ for (const row of targets) {
51
+ try {
52
+ const gen = await generate({
53
+ url: row.url,
54
+ slug: siteSlug(row.name),
55
+ // `exactOptionalPropertyTypes` rejects an explicit `settleMs: undefined`
56
+ // against `settleMs?: number`, so omit the key entirely when unset.
57
+ ...settleMs === void 0 ? {} : { settleMs }
58
+ });
59
+ if (opts.writeAirtable) {
60
+ await uploadAttachment(row.id, "Header image", gen.bytes, gen.filename, gen.contentType);
61
+ lines.push(
62
+ `\u2714 ${row.name} \u2014 uploaded ${gen.filename} (${(gen.bytes.byteLength / 1024 / 1024).toFixed(2)} MB)`
63
+ );
64
+ } else {
65
+ const path = resolve(outDir, gen.filename);
66
+ await writeFile(path, gen.bytes);
67
+ lines.push(`\u2714 ${row.name} \u2014 wrote ${path} (review, then re-run with --write-airtable)`);
68
+ }
69
+ } catch (err) {
70
+ failed++;
71
+ lines.push(`\u2716 ${row.name} \u2014 ${err instanceof Error ? err.message : String(err)}`);
72
+ }
73
+ }
74
+ lines.push(`${targets.length - failed}/${targets.length} generated.`);
75
+ return { output: lines.join("\n"), code: failed > 0 ? 1 : 0 };
76
+ }
77
+ async function runHeaderImageCommand(site, opts) {
78
+ const base = openBase(readAirtableConfig());
79
+ const rows = await listWebsites(base);
80
+ const targets = resolveTargets(rows, { site, all: opts.all, force: opts.force });
81
+ if (targets.length === 0) {
82
+ return {
83
+ output: site ? `No site matched "${site}".` : "No sites need a header image.",
84
+ code: 1
85
+ };
86
+ }
87
+ return generateForTargets(targets, opts);
88
+ }
89
+ export {
90
+ generateForTargets,
91
+ parseSettleMs,
92
+ resolveTargets,
93
+ runHeaderImageCommand
94
+ };
95
+ //# sourceMappingURL=header-image-H4N54DHN.js.map