pattern-dump 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pd.js CHANGED
@@ -2,9 +2,11 @@
2
2
  import {
3
3
  ConfigError,
4
4
  sameHostWorkbenchPath,
5
+ storefrontBasePath,
5
6
  suggest,
6
- validateConfig
7
- } from "./chunks/chunk-5WFBNPXB.js";
7
+ validateConfig,
8
+ withOwnManifest
9
+ } from "./chunks/chunk-DIDJPCLS.js";
8
10
 
9
11
  // packages/contracts/src/dev-protocol.ts
10
12
  var AUDIT_SCOPE = {
@@ -899,17 +901,78 @@ function checkSchemaHelperNames(schema, helpers) {
899
901
  }
900
902
  var isHelperNameProblem = (message) => message.includes(`" ${HELPER_NAME}, so {{`);
901
903
 
904
+ // packages/renderer/src/helper-shadow.ts
905
+ import Handlebars3 from "handlebars";
906
+ function keysOf(data) {
907
+ const found = /* @__PURE__ */ new Map();
908
+ const walk = (v, path) => {
909
+ if (Array.isArray(v)) {
910
+ for (const item of v) walk(item, `${path}[]`);
911
+ return;
912
+ }
913
+ if (typeof v !== "object" || v === null) return;
914
+ for (const [k, child] of Object.entries(v)) {
915
+ const here = path ? `${path}.${k}` : k;
916
+ if (!found.has(k)) found.set(k, here);
917
+ walk(child, here);
918
+ }
919
+ };
920
+ walk(data, "");
921
+ return found;
922
+ }
923
+ function helperShadows(template, data, helpers, skip = /* @__PURE__ */ new Set()) {
924
+ let ast;
925
+ try {
926
+ ast = Handlebars3.parse(template);
927
+ } catch {
928
+ return [];
929
+ }
930
+ const helperNames = new Set(helpers);
931
+ const keys = keysOf(data);
932
+ const shadows = [];
933
+ const seen = /* @__PURE__ */ new Set();
934
+ const visit2 = (node) => {
935
+ if (Array.isArray(node)) {
936
+ node.forEach(visit2);
937
+ return;
938
+ }
939
+ if (typeof node !== "object" || node === null) return;
940
+ const n = node;
941
+ if (n.type === "MustacheStatement") {
942
+ const path = n["path"];
943
+ const params = n["params"];
944
+ const hash = n["hash"];
945
+ const name = path?.parts?.[0];
946
+ if (path?.type === "PathExpression" && !path.data && path.parts?.length === 1 && path.original === name && !params?.length && !hash?.pairs?.length && name && helperNames.has(name) && keys.has(name) && !skip.has(name)) {
947
+ const loc = n["loc"];
948
+ const key = `${name}@${loc?.start?.line}:${loc?.start?.column}`;
949
+ if (!seen.has(key)) {
950
+ seen.add(key);
951
+ shadows.push({ name, at: keys.get(name), line: loc?.start?.line ?? 1, column: (loc?.start?.column ?? 0) + 1 });
952
+ }
953
+ }
954
+ }
955
+ for (const [k, v] of Object.entries(n)) if (k !== "loc" && typeof v === "object") visit2(v);
956
+ };
957
+ visit2(ast);
958
+ return shadows;
959
+ }
960
+
902
961
  // packages/renderer/src/index.ts
903
962
  import { HtmlValidate } from "html-validate";
904
- import Handlebars3 from "handlebars";
963
+ import Handlebars4 from "handlebars";
905
964
  import { readFile } from "node:fs/promises";
906
965
  import { basename as basename2, isAbsolute, relative as relative2, sep } from "node:path";
966
+ var COMPILE = { preventIndent: true };
907
967
  var htmlValidator = new HtmlValidate({
908
968
  rules: {
909
969
  "element-permitted-content": "error",
910
970
  "element-permitted-parent": "error"
911
971
  }
912
972
  });
973
+ function renderPlainTemplate(source2, data) {
974
+ return Handlebars4.create().compile(source2, COMPILE)(data);
975
+ }
913
976
  function deepMerge(target, ...sources) {
914
977
  for (const source2 of sources) {
915
978
  if (!source2) continue;
@@ -956,7 +1019,7 @@ async function readDataFile(path, display) {
956
1019
  }
957
1020
  }
958
1021
  function imageTag(source2, o) {
959
- const esc = Handlebars3.escapeExpression;
1022
+ const esc = Handlebars4.escapeExpression;
960
1023
  const rest = `alt="${esc(o.alt)}"${o.title !== void 0 ? ` title="${esc(o.title)}"` : ""} loading="${o.eager ? "eager" : "lazy"}" decoding="async"`;
961
1024
  if (source2.kind === "single") return `<img src="${esc(source2.url)}" width="${source2.width}" height="${source2.height}" ${rest}>`;
962
1025
  const widths = o.widths.filter((w) => w <= source2.width);
@@ -1102,7 +1165,7 @@ function makeDisplayPath(roots, displayPath) {
1102
1165
  }
1103
1166
  function dryRunHelperRegistrar(register) {
1104
1167
  try {
1105
- register(Handlebars3.create());
1168
+ register(Handlebars4.create());
1106
1169
  return null;
1107
1170
  } catch (e) {
1108
1171
  return e instanceof Error ? e.message : String(e);
@@ -1153,7 +1216,7 @@ var createRenderer = (options) => {
1153
1216
  variantData,
1154
1217
  input.data
1155
1218
  );
1156
- const env = Handlebars3.create();
1219
+ const env = Handlebars4.create();
1157
1220
  const componentSlug = safePart(componentId.replace(/[\\/]/g, "-"));
1158
1221
  let currentBase = `pd-${componentSlug}`;
1159
1222
  const idCache = /* @__PURE__ */ new Map();
@@ -1197,7 +1260,7 @@ var createRenderer = (options) => {
1197
1260
  const baseJsonPath = variantTilde > 0 ? discoveryMap.get(compId.slice(0, variantTilde))?.jsonPath : void 0;
1198
1261
  const baseData = baseJsonPath ? await readDataFile(baseJsonPath, display) : {};
1199
1262
  const childDefaults = deepMerge(structuredClone(baseData.data ?? {}), childData.data);
1200
- const childTemplate = env.compile(await slotted(partialSrc, compId));
1263
+ const childTemplate = env.compile(await slotted(partialSrc, compId), COMPILE);
1201
1264
  const capturedDefaults = childDefaults;
1202
1265
  const partialSlug = safePart(compId.replace(/[\\/]/g, "-"));
1203
1266
  env.registerPartial(
@@ -1240,7 +1303,7 @@ var createRenderer = (options) => {
1240
1303
  }
1241
1304
  let html;
1242
1305
  try {
1243
- html = env.compile(await slotted(templateSrc, componentId))(data, {
1306
+ html = env.compile(await slotted(templateSrc, componentId), COMPILE)(data, {
1244
1307
  partials: { ...env.partials, "@partial-block": () => "" },
1245
1308
  // The render's image settings, for {{pd-image}} here and in every partial (data frames pass on).
1246
1309
  data: { pdImages: input.images }
@@ -1321,6 +1384,16 @@ var createRenderer = (options) => {
1321
1384
  }
1322
1385
  } catch {
1323
1386
  }
1387
+ const schemaNamed = new Set(diagnostics.filter((d) => d.kind === "SCHEMA_INVALID" && d.path).map((d) => /\/properties\/([^/]+)$/.exec(d.path)?.[1]).filter((n) => !!n));
1388
+ for (const s of helperShadows(templateSrc, data, Object.keys(env.helpers), schemaNamed)) {
1389
+ diagnostics.push({
1390
+ kind: "HELPER_SHADOW",
1391
+ severity: "warning",
1392
+ source: "render",
1393
+ message: `{{${s.name}}} calls the helper "${s.name}", so the data's "${s.at}" is never read here: write {{this.${s.name}}} to read the field, or give the field another name`,
1394
+ location: { file: display(entry.hbsPath), line: s.line, column: s.column }
1395
+ });
1396
+ }
1324
1397
  const meta = diagnostics.length > 0 ? { componentId, variant, diagnostics } : void 0;
1325
1398
  return { html, document: buildDocument(html, options), ...meta ? { meta } : {} };
1326
1399
  };
@@ -1807,7 +1880,7 @@ function isOwnOrigin(origin, hosts, port2) {
1807
1880
  // packages/dev-server/src/index.ts
1808
1881
  import { createServer } from "node:http";
1809
1882
  import { stat as stat6 } from "node:fs/promises";
1810
- import { join as join28, sep as sep11 } from "node:path";
1883
+ import { join as join29, sep as sep11 } from "node:path";
1811
1884
  import { fileURLToPath as fileURLToPath6 } from "node:url";
1812
1885
 
1813
1886
  // packages/dev-server/src/themes.ts
@@ -2313,9 +2386,12 @@ function resolveServerConfig(config, cwd, opts = {}, packages = []) {
2313
2386
  workbenchUrl: config.storefront.workbenchUrl,
2314
2387
  image: config.storefront.image,
2315
2388
  url: config.storefront.url,
2389
+ basePath: storefrontBasePath(config.storefront),
2316
2390
  lang: config.storefront.lang ?? "en",
2317
2391
  index: config.storefront.index !== false,
2318
- images: { ...images, ...config.storefront.images }
2392
+ images: { ...images, ...config.storefront.images },
2393
+ head: config.storefront.head !== void 0 ? resolve4(cwd, config.storefront.head) : void 0,
2394
+ headName: config.storefront.head
2319
2395
  } : null,
2320
2396
  images,
2321
2397
  overviewsEnabled: config.overviews !== false,
@@ -2516,6 +2592,7 @@ async function buildAllStyleData(roots, componentIds, entryOverride) {
2516
2592
  const componentCssBuckets = /* @__PURE__ */ new Map();
2517
2593
  const baseCssByRoot = [];
2518
2594
  const outsideRules = /* @__PURE__ */ new Map();
2595
+ const baseRulesByEntry = /* @__PURE__ */ new Map();
2519
2596
  for (const id of componentIds) styleUsesModules.set(id, []);
2520
2597
  const rootList = typeof roots === "string" ? [{ dir: roots, ...entryOverride ? { entries: entryOverride } : {} }] : roots;
2521
2598
  for (const styleRoot of rootList) {
@@ -2583,6 +2660,7 @@ async function buildAllStyleData(roots, componentIds, entryOverride) {
2583
2660
  if (!rawMap) continue;
2584
2661
  const consumer = new SourceMapConsumer(rawMap);
2585
2662
  const root = postcss3.parse(compiled.css);
2663
+ const entryBase = /* @__PURE__ */ new Set();
2586
2664
  root.each((node) => {
2587
2665
  const originPath = originPathFor2(node);
2588
2666
  if (originPath === null) return;
@@ -2590,6 +2668,7 @@ async function buildAllStyleData(roots, componentIds, entryOverride) {
2590
2668
  const owner = inBase(originPath) ? "__base__" : resolveScssToComponent(originPath, stylesDir, componentIds) ?? "__base__";
2591
2669
  if (owner === "__base__") {
2592
2670
  addLastOccurrence(baseCssBucket, nodeText);
2671
+ addLastOccurrence(entryBase, nodeText);
2593
2672
  if (reportOutside && !inBase(originPath)) {
2594
2673
  const seen = outsideRules.get(originPath) ?? /* @__PURE__ */ new Set();
2595
2674
  seen.add(nodeText);
@@ -2601,6 +2680,7 @@ async function buildAllStyleData(roots, componentIds, entryOverride) {
2601
2680
  componentCssBuckets.set(owner, bucket);
2602
2681
  }
2603
2682
  });
2683
+ baseRulesByEntry.set(resolve5(entryFile), joinCssChunks(entryBase));
2604
2684
  const { classMap: perFileClasses, baseModules: perFileBaseModules, ranks: perFileRanks } = extractClassesFromCompiled(consumer, root, entryFile, stylesDir, componentIds);
2605
2685
  for (const [name, owner] of perFileClasses) {
2606
2686
  const rank = perFileRanks.get(name) ?? 0;
@@ -2631,7 +2711,8 @@ async function buildAllStyleData(roots, componentIds, entryOverride) {
2631
2711
  componentCssMap,
2632
2712
  // Packages' base rules first (later roots), the project's last — its own base wins the cascade.
2633
2713
  baseCss: joinCssChunks([...baseCssByRoot].reverse().flat()),
2634
- baseOutsideList: new Map([...outsideRules].map(([path, rules]) => [path, rules.size]))
2714
+ baseOutsideList: new Map([...outsideRules].map(([path, rules]) => [path, rules.size])),
2715
+ baseRulesByEntry
2635
2716
  };
2636
2717
  }
2637
2718
 
@@ -2644,7 +2725,7 @@ async function surfaceCollisions(collisions, cwd, hotreloader) {
2644
2725
  console.warn(`[dev-server] HANDLE_COLLISION: "${col.handle}" \u2014 incumbent: ${col.incumbentPath} \u2014 quarantined: ${col.quarantinedPath}`);
2645
2726
  }
2646
2727
  if (collisions.length > 0) console.warn(`[dev-server] collisions listed in ${join9(pdScratchDir(cwd), "CONFLICTS.generated.md")}`);
2647
- const md = collisions.length === 0 ? "# Conflicts\n\nNo handle collisions detected.\n" : [
2728
+ const md2 = collisions.length === 0 ? "# Conflicts\n\nNo handle collisions detected.\n" : [
2648
2729
  "# Conflicts",
2649
2730
  "",
2650
2731
  `${collisions.length} handle collision${collisions.length > 1 ? "s" : ""} detected. Quarantined components are excluded from the tree until the collision is resolved.`,
@@ -2659,7 +2740,7 @@ async function surfaceCollisions(collisions, cwd, hotreloader) {
2659
2740
  try {
2660
2741
  const dir = pdScratchDir(cwd);
2661
2742
  await mkdir2(dir, { recursive: true });
2662
- await writeFile2(join9(dir, "CONFLICTS.generated.md"), md, "utf-8");
2743
+ await writeFile2(join9(dir, "CONFLICTS.generated.md"), md2, "utf-8");
2663
2744
  } catch (e) {
2664
2745
  console.warn("[dev-server] Could not write CONFLICTS.generated.md to scratch dir:", e);
2665
2746
  }
@@ -2677,7 +2758,7 @@ async function surfaceFlattening(discoveryMap, registry, cwd) {
2677
2758
  }
2678
2759
  const hasEdge = (a, b) => (registry.uses.get(a)?.includes(b) ?? false) || (registry.uses.get(b)?.includes(a) ?? false);
2679
2760
  const clusters = detectFlattening(inputs, { hasEdge });
2680
- const md = renderFlatteningReport(clusters);
2761
+ const md2 = renderFlatteningReport(clusters);
2681
2762
  if (clusters.length) {
2682
2763
  const members = clusters.reduce((n, c3) => n + c3.members.length, 0);
2683
2764
  console.warn(`[dev-server] FLATTENED: ${clusters.length} cluster(s) across ${members} components \u2014 see ${join9(pdScratchDir(cwd), "FLATTENED.generated.md")}`);
@@ -2685,7 +2766,7 @@ async function surfaceFlattening(discoveryMap, registry, cwd) {
2685
2766
  try {
2686
2767
  const dir = pdScratchDir(cwd);
2687
2768
  await mkdir2(dir, { recursive: true });
2688
- await writeFile2(join9(dir, "FLATTENED.generated.md"), md, "utf-8");
2769
+ await writeFile2(join9(dir, "FLATTENED.generated.md"), md2, "utf-8");
2689
2770
  } catch (e) {
2690
2771
  console.warn("[dev-server] Could not write FLATTENED.generated.md to scratch dir:", e);
2691
2772
  }
@@ -2747,8 +2828,8 @@ async function assetFiles(roots, names) {
2747
2828
  if (!e.isFile()) continue;
2748
2829
  const path = join10(e.parentPath, e.name);
2749
2830
  const rel = relative5(root, path).split(sep2).join("/");
2750
- if (roots.length > 1) files.push({ url: `${PD_ROUTE_PREFIX.assets}${names[i]}/${rel}`, path });
2751
- if (i === 0) files.push({ url: `${PD_ROUTE_PREFIX.assets}${rel}`, path });
2831
+ files.push({ url: `${PD_ROUTE_PREFIX.assets}${names[i]}/${rel}`, path });
2832
+ if (i === 0 && !names.includes(rel.split("/")[0])) files.push({ url: `${PD_ROUTE_PREFIX.assets}${rel}`, path });
2752
2833
  }
2753
2834
  }
2754
2835
  return files;
@@ -3086,6 +3167,7 @@ var page_rule_tiers_generated_default = {
3086
3167
  var require4 = createRequire3(import.meta.url);
3087
3168
  var axeSource;
3088
3169
  var source = () => axeSource ??= readFileSync2(require4.resolve("axe-core/axe.min.js"), "utf8");
3170
+ var axeScript = () => source();
3089
3171
  var PAGE_TAGS = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa", "best-practice"];
3090
3172
  async function runAxeOnPage(html, rules) {
3091
3173
  const dom = new JSDOM2(html, { url: "http://site.invalid/", runScripts: "outside-only", pretendToBeVisual: true });
@@ -3116,6 +3198,7 @@ async function runPageA11y(html) {
3116
3198
  nodes: v.nodes.map((n) => ({ target: n.target.map(String).join(" "), html: n.html }))
3117
3199
  }));
3118
3200
  }
3201
+ var pageRuleTiers = page_rule_tiers_generated_default;
3119
3202
 
3120
3203
  // packages/dev-server/src/diagnostics.ts
3121
3204
  async function checkComponent(componentId, variant, render, quarantineEntry) {
@@ -3137,7 +3220,7 @@ async function checkComponent(componentId, variant, render, quarantineEntry) {
3137
3220
  const a11yDiags = await runA11yProducer(previewHtml);
3138
3221
  return { previewHtml, diagnostics: [...renderDiags, ...a11yDiags] };
3139
3222
  } catch (e) {
3140
- const location = e instanceof Error && "location" in e ? e.location : void 0;
3223
+ const location2 = e instanceof Error && "location" in e ? e.location : void 0;
3141
3224
  return {
3142
3225
  previewHtml: "",
3143
3226
  diagnostics: [{
@@ -3145,7 +3228,7 @@ async function checkComponent(componentId, variant, render, quarantineEntry) {
3145
3228
  severity: "error",
3146
3229
  source: "render",
3147
3230
  message: e instanceof Error ? e.message : String(e),
3148
- ...location && Object.keys(location).length ? { location } : {}
3231
+ ...location2 && Object.keys(location2).length ? { location: location2 } : {}
3149
3232
  }]
3150
3233
  };
3151
3234
  }
@@ -3232,7 +3315,8 @@ async function componentContentHash(id, discoveryMap, globalDataDigest2, checkCo
3232
3315
  const entry = discoveryMap.get(id);
3233
3316
  const hbs = entry?.hbsPath ? await readFile8(entry.hbsPath, "utf-8").catch(() => "") : "";
3234
3317
  const json2 = entry?.jsonPath ? await readFile8(entry.jsonPath, "utf-8").catch(() => "") : "";
3235
- return createHash4("sha1").update(id).update(" ").update(hbs).update(" ").update(json2).update(" ").update(globalDataDigest2).update(" ").update(checkCode).digest("hex");
3318
+ const schema = entry?.schemaPath ? await readFile8(entry.schemaPath, "utf-8").catch(() => "") : "";
3319
+ return createHash4("sha1").update(id).update(" ").update(hbs).update(" ").update(json2).update(" ").update(schema).update(" ").update(globalDataDigest2).update(" ").update(checkCode).digest("hex");
3236
3320
  }
3237
3321
  function globalDataDigest(globalData) {
3238
3322
  return createHash4("sha1").update(JSON.stringify(globalData ?? {})).digest("hex");
@@ -3446,8 +3530,8 @@ async function runDiagnosticsAudit(config, cwd) {
3446
3530
  }
3447
3531
 
3448
3532
  // packages/dev-server/src/static-build.ts
3449
- import { cp, mkdir as mkdir5, readdir as readdir7, rm as rm3, stat as stat4, writeFile as writeFile5 } from "node:fs/promises";
3450
- import { basename as basename4, join as join17, relative as relative9, resolve as resolve7, sep as sep6 } from "node:path";
3533
+ import { cp, mkdir as mkdir5, readdir as readdir8, rm as rm3, stat as stat5, writeFile as writeFile5 } from "node:fs/promises";
3534
+ import { basename as basename4, join as join19, relative as relative10, resolve as resolve7, sep as sep7 } from "node:path";
3451
3535
 
3452
3536
  // packages/dev-server/src/http-utils.ts
3453
3537
  import { readFile as readFile9 } from "node:fs/promises";
@@ -3568,12 +3652,12 @@ function imageCacheDir(cwd) {
3568
3652
  }
3569
3653
  var caches = /* @__PURE__ */ new WeakMap();
3570
3654
  var cacheFor = (cfg) => {
3571
- let cache = caches.get(cfg);
3572
- if (!cache) {
3573
- cache = createImageCache(imageCacheDir(cfg.cwd));
3574
- caches.set(cfg, cache);
3655
+ let cache2 = caches.get(cfg);
3656
+ if (!cache2) {
3657
+ cache2 = createImageCache(imageCacheDir(cfg.cwd));
3658
+ caches.set(cfg, cache2);
3575
3659
  }
3576
- return cache;
3660
+ return cache2;
3577
3661
  };
3578
3662
  async function variantSource(name, hash, width, encoding, cfg) {
3579
3663
  const base2 = PD_ROUTE_PREFIX.assets + name.split("/").map(encodeURIComponent).join("/");
@@ -3611,11 +3695,11 @@ async function serveImageVariant(pathname, host, res, cfg) {
3611
3695
  return;
3612
3696
  }
3613
3697
  if (variant.width > (await imageSize(sharp, source2)).width) return notFound();
3614
- const cache = cacheFor(cfg);
3615
- let bytes = await cache.read(variant.hash);
3698
+ const cache2 = cacheFor(cfg);
3699
+ let bytes = await cache2.read(variant.hash);
3616
3700
  if (!bytes) {
3617
3701
  bytes = await encodeWebp(sharp, source2, variant.width, encoding);
3618
- await cache.write(variant.hash, bytes);
3702
+ await cache2.write(variant.hash, bytes);
3619
3703
  }
3620
3704
  res.writeHead(200, { "Content-Type": "image/webp", "Cache-Control": "public, max-age=31536000, immutable" });
3621
3705
  res.end(bytes);
@@ -3681,172 +3765,618 @@ function createSnapshotWriter({ outDir, basePath, origin, report, fetch: get = (
3681
3765
  return { capture, store, failures };
3682
3766
  }
3683
3767
 
3684
- // packages/dev-server/src/storefront/crawl.ts
3685
- import { request } from "node:http";
3686
- function hostTransport(port2, host) {
3687
- return (liveUrl) => new Promise((resolve15, reject) => {
3688
- const req = request({ host: "127.0.0.1", port: port2, path: liveUrl, headers: { host: `${host}:${port2}` } }, (res) => {
3689
- const chunks = [];
3690
- res.on("data", (c3) => chunks.push(c3));
3691
- res.on("end", () => {
3692
- const headers = new Headers();
3693
- for (const [k, v] of Object.entries(res.headers)) if (typeof v === "string") headers.set(k, v);
3694
- resolve15(new Response(Buffer.concat(chunks), { status: res.statusCode ?? 0, headers }));
3695
- });
3696
- });
3697
- req.on("error", reject);
3698
- req.end();
3699
- });
3768
+ // packages/dev-server/src/storefront/pages.ts
3769
+ import { readdir as readdir7, readFile as readFile11, stat as stat4 } from "node:fs/promises";
3770
+ import { join as join17, relative as relative9, sep as sep6 } from "node:path";
3771
+
3772
+ // packages/dev-server/src/frontmatter.ts
3773
+ var BOM = "\uFEFF";
3774
+ var unquote = (s) => {
3775
+ const t = s.trim();
3776
+ return t.length >= 2 && (t[0] === '"' || t[0] === "'") && t.at(-1) === t[0] ? t.slice(1, -1) : t;
3777
+ };
3778
+ var unterminatedQuote = (s) => {
3779
+ const t = s.trim();
3780
+ return (t[0] === '"' || t[0] === "'") && (t.length < 2 || t.at(-1) !== t[0]);
3781
+ };
3782
+ function block(md2) {
3783
+ const text = md2.startsWith(BOM) ? md2.slice(1) : md2;
3784
+ const lines = text.split(/\r?\n/);
3785
+ if (lines[0]?.trim() !== "---") return { text, lines, end: null };
3786
+ return { text, lines, end: lines.findIndex((l, i) => i > 0 && l.trim() === "---") };
3700
3787
  }
3701
- var unescapeHtml = (s) => s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, `"`).replace(/&amp;/g, "&");
3702
- var markupRefs = (html) => [
3703
- ...[...html.matchAll(/(?:href|src)="(\/(?:_pd|assets|preview)\/[^"]+)"/g)].map((m) => unescapeHtml(m[1])),
3704
- // Each candidate of a srcset: its URL, before the width or density that describes it.
3705
- ...[...html.matchAll(/srcset="([^"]+)"/g)].flatMap((m) => unescapeHtml(m[1]).split(",").map((c3) => c3.trim().split(/\s+/)[0] ?? "")).filter((u) => /^\/(?:_pd|assets)\//.test(u)),
3706
- ...[...html.matchAll(/<meta property="og:image" content="https?:\/\/[^/"]+(\/assets\/[^"]+)">/g)].map((m) => unescapeHtml(m[1]))
3707
- ];
3708
- var cssRefs = (css) => [...css.matchAll(/url\(\s*(['"]?)(\/(?:_pd|assets)\/[^'")\s]+)\1\s*\)/g)].map((m) => m[2]);
3709
- async function crawlStorefront(get, files = []) {
3710
- const problems = [];
3711
- const siteProblems = /* @__PURE__ */ new Set();
3712
- const list = await get(PD_ROUTES.pages);
3713
- if (list.status !== 200) {
3714
- const body = await list.text();
3715
- let error = body;
3716
- try {
3717
- error = JSON.parse(body).error ?? body;
3718
- } catch {
3719
- }
3720
- return { urls: [], problems: error.split("\n").filter(Boolean), documents: /* @__PURE__ */ new Map() };
3788
+ function scan(md2) {
3789
+ const errors = [];
3790
+ const err = (line, message) => {
3791
+ errors.push({ line, message });
3792
+ };
3793
+ const { text, lines, end: blockEnd } = block(md2);
3794
+ if (blockEnd === null) {
3795
+ if (/^---\r(?!\n)/.test(text)) err(1, "lines end in a bare CR \u2014 use LF or CRLF line endings");
3796
+ return { values: {}, errors, lines: {} };
3721
3797
  }
3722
- const { pages } = await list.json();
3723
- const urls = [];
3724
- const documents = /* @__PURE__ */ new Map();
3725
- const seen = /* @__PURE__ */ new Set();
3726
- const queue = [];
3727
- const add = (url) => {
3728
- if (!seen.has(url)) {
3729
- seen.add(url);
3730
- queue.push(url);
3798
+ const end = blockEnd;
3799
+ if (end < 0) {
3800
+ err(1, "the frontmatter block opened here is never closed \u2014 end it with a line holding only ---");
3801
+ return { values: {}, errors, lines: {} };
3802
+ }
3803
+ const out = {};
3804
+ const keyLines = {};
3805
+ let listKey = null;
3806
+ for (let i = 1; i < end; i++) {
3807
+ const line = lines[i];
3808
+ const n = i + 1;
3809
+ const bareCr = line.includes("\r");
3810
+ if (bareCr) err(n, "line ends in a bare CR \u2014 use LF or CRLF line endings");
3811
+ const lineErr = (message) => {
3812
+ if (!bareCr) err(n, message);
3813
+ };
3814
+ const item = line.match(/^\s*-\s+(.*)$/);
3815
+ if (item && listKey) {
3816
+ if (unterminatedQuote(item[1])) lineErr(`list item ${item[1].trim()} has an opening quote with no closing one`);
3817
+ out[listKey].push(unquote(item[1]));
3818
+ continue;
3731
3819
  }
3732
- };
3733
- for (const p of pages) add(p.route);
3734
- for (const p of pages) for (const old of p.redirectFrom ?? []) add(old);
3735
- for (const f of files) add(f);
3736
- while (queue.length) {
3737
- const url = queue.shift();
3738
- const res = await get(url);
3739
- const type = res.headers.get("content-type") ?? "";
3740
- if (res.status !== (url === PD_ROUTES.notFound ? 404 : 200)) {
3741
- const body = await res.text();
3742
- const items = [...body.matchAll(/<li( data-pd-site)?>([\s\S]*?)<\/li>/g)].map((m) => ({ site: !!m[1], text: unescapeHtml(m[2]) }));
3743
- for (const i of items) {
3744
- if (!i.site) problems.push(`${url}: ${i.text}`);
3745
- else if (!siteProblems.has(i.text)) {
3746
- siteProblems.add(i.text);
3747
- problems.push(i.text);
3748
- }
3820
+ const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
3821
+ if (!kv) {
3822
+ if (line.trim() !== "") {
3823
+ if (item) lineErr("list item with no key above it \u2014 a list follows a line `key:`");
3824
+ else if (/^\s+[A-Za-z_][\w-]*:/.test(line)) lineErr("nested value \u2014 only top-level keys are supported, written at the start of the line");
3825
+ else lineErr(`cannot read this line: ${line.trim()}`);
3749
3826
  }
3750
- if (!items.length) problems.push(`${url}: ${res.status} ${body.split("\n")[0]?.slice(0, 160) ?? ""}`.trimEnd());
3827
+ listKey = null;
3751
3828
  continue;
3752
3829
  }
3753
- urls.push(url);
3754
- if (/text\/html/.test(type)) {
3755
- const html = await res.text();
3756
- documents.set(url, html);
3757
- for (const ref of markupRefs(html)) add(ref);
3758
- } else if (/text\/css/.test(type)) for (const ref of cssRefs(await res.text())) add(ref);
3759
- }
3760
- return { urls, problems, documents };
3761
- }
3762
-
3763
- // packages/dev-server/src/storefront/html-tags.ts
3764
- var MARKUP = /<!--[\s\S]*?-->|<([a-zA-Z][\w:-]*)((?:\s+[^\s"'>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*)\s*\/?>/g;
3765
- var ATTRIBUTE = /(\s+)([^\s"'>/=]+)(?:(\s*=\s*)("[^"]*"|'[^']*'|[^\s"'=<>`]+))?/g;
3766
- var RAW_TEXT = /* @__PURE__ */ new Set(["script", "style", "textarea", "title"]);
3767
- function startTags(html) {
3768
- const tags = [];
3769
- const markup = new RegExp(MARKUP.source, "g");
3770
- const lower = html.toLowerCase();
3771
- for (let m = markup.exec(html); m; m = markup.exec(html)) {
3772
- const name = m[1];
3773
- if (name === void 0) continue;
3774
- const tag = { name, start: m.index, end: markup.lastIndex, attrs: m[2] ?? "" };
3775
- const key = name.toLowerCase();
3776
- if (RAW_TEXT.has(key)) {
3777
- const close = lower.indexOf(`</${key}`, tag.end);
3778
- tag.content = { start: tag.end, end: close < 0 ? html.length : close };
3779
- markup.lastIndex = tag.content.end;
3830
+ const [, key, raw] = kv;
3831
+ if (key in out) lineErr(`duplicate key "${key}"`);
3832
+ keyLines[key] = n;
3833
+ const value = raw.trim();
3834
+ if (value === "") {
3835
+ out[key] = [];
3836
+ listKey = key;
3837
+ } else if (value.startsWith("[") && value.endsWith("]")) {
3838
+ const items = value.slice(1, -1).split(",");
3839
+ for (const it of items) if (unterminatedQuote(it)) lineErr(`list item ${it.trim()} has an opening quote with no closing one`);
3840
+ out[key] = items.map(unquote).filter(Boolean);
3841
+ listKey = null;
3842
+ } else {
3843
+ if (value.startsWith("[")) lineErr(`"${key}": the list opened with [ is never closed with ]`);
3844
+ else if (unterminatedQuote(value)) lineErr(`"${key}": the value has an opening quote with no closing one`);
3845
+ out[key] = unquote(value);
3846
+ listKey = null;
3780
3847
  }
3781
- tags.push(tag);
3782
3848
  }
3783
- return tags;
3849
+ return { values: out, errors, lines: keyLines };
3784
3850
  }
3785
- function decodeAttribute(written) {
3786
- const q = written[0] === '"' || written[0] === "'" ? written.slice(1, -1) : written;
3787
- return q.replace(/&(#x[0-9a-f]+|#[0-9]+|amp|lt|gt|quot|apos|#39);/gi, (_, e) => {
3788
- const k = e.toLowerCase();
3789
- if (k.startsWith("#x")) return String.fromCodePoint(parseInt(k.slice(2), 16));
3790
- if (k.startsWith("#")) return String.fromCodePoint(parseInt(k.slice(1), 10));
3791
- return { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" }[k] ?? `&${e};`;
3792
- });
3851
+ function readFrontmatter(md2) {
3852
+ return scan(md2).values;
3793
3853
  }
3794
- function attributesOf(tag) {
3795
- return [...tag.attrs.matchAll(ATTRIBUTE)].map((m) => ({ name: m[2].toLowerCase(), value: m[4] === void 0 ? void 0 : decodeAttribute(m[4]) }));
3854
+ function readFrontmatterStrict(md2) {
3855
+ return scan(md2);
3796
3856
  }
3797
-
3798
- // packages/dev-server/src/storefront/links.ts
3799
- var SKIPPED = /^(?:mailto|tel|data|javascript):/i;
3800
- var OTHER_HOST = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
3801
- function idsOf(html) {
3802
- const ids = /* @__PURE__ */ new Set();
3803
- for (const tag of startTags(html)) {
3804
- for (const a of attributesOf(tag)) {
3805
- if (a.value === void 0) continue;
3806
- if (a.name === "id" || a.name === "name" && tag.name.toLowerCase() === "a") ids.add(a.value);
3807
- }
3808
- }
3809
- return ids;
3857
+ function frontmatterBodyOffset(md2) {
3858
+ const { end } = block(md2);
3859
+ return end === null || end < 0 ? 0 : end + 1;
3810
3860
  }
3811
- function referencesOf(html) {
3812
- const refs = [];
3813
- for (const tag of startTags(html)) {
3814
- for (const a of attributesOf(tag)) {
3815
- if (a.value === void 0) continue;
3816
- if (a.name === "href" || a.name === "src") refs.push([tag.name.toLowerCase(), a.name, a.value.trim()]);
3817
- else if (a.name === "srcset") {
3818
- for (const c3 of a.value.split(",")) {
3819
- const url = c3.trim().split(/\s+/)[0];
3820
- if (url) refs.push([tag.name.toLowerCase(), a.name, url]);
3821
- }
3822
- }
3823
- }
3824
- }
3825
- return refs;
3861
+ function frontmatterBody(md2) {
3862
+ const { text, lines, end } = block(md2);
3863
+ if (end === null) return text;
3864
+ if (end < 0) return "";
3865
+ return lines.slice(end + 1).join("\n");
3826
3866
  }
3827
- function linkedWorkbenchRoutes(documents, pages, workbenchUrl) {
3828
- const routes = /* @__PURE__ */ new Set();
3829
- for (const page of pages) {
3830
- for (const [, , ref] of referencesOf(documents.get(page) ?? "")) {
3831
- let rest;
3832
- if (OTHER_HOST.test(workbenchUrl)) {
3833
- if (ref.startsWith(workbenchUrl)) rest = ref.slice(workbenchUrl.length);
3834
- } else if (!OTHER_HOST.test(ref)) {
3835
- const path = new URL(ref, `http://site.invalid${page}`).pathname;
3836
- if (path.startsWith(workbenchUrl)) rest = path.slice(workbenchUrl.length);
3837
- }
3838
- if (rest !== void 0) routes.add(decodeURIComponent(rest.split(/[?#]/)[0].replace(/\/$/, "")));
3839
- }
3840
- }
3841
- return routes;
3867
+
3868
+ // packages/dev-server/src/tree-order.ts
3869
+ function parseOrder(value) {
3870
+ if (value === void 0) return void 0;
3871
+ if (Array.isArray(value)) return { list: value };
3872
+ if (/^-?\d+(\.\d+)?$/.test(value)) return { weight: Number(value) };
3873
+ const list = value.split(",").map((s) => s.trim()).filter(Boolean);
3874
+ return list.length ? { list } : void 0;
3842
3875
  }
3843
- function checkLinks(site) {
3844
- const problems = [];
3845
- const external = /* @__PURE__ */ new Map();
3846
- const idCache = /* @__PURE__ */ new Map();
3847
- const ids = (url) => {
3848
- const html = site.documents.get(url);
3849
- if (html === void 0) return void 0;
3876
+ var collator = new Intl.Collator("en", { numeric: true });
3877
+ var REST = "...";
3878
+ function orderSiblings(items, list) {
3879
+ const position = /* @__PURE__ */ new Map();
3880
+ list?.forEach((name, i) => {
3881
+ if (!position.has(name)) position.set(name, i);
3882
+ });
3883
+ const restPosition = position.get(REST) ?? Infinity;
3884
+ const positionOf2 = (item) => list ? position.get(item.segment) ?? restPosition : 0;
3885
+ return [...items].sort((a, b) => {
3886
+ const ra = a.weight ?? 0;
3887
+ const rb = b.weight ?? 0;
3888
+ if (ra !== rb) return ra - rb;
3889
+ const pa = positionOf2(a);
3890
+ const pb = positionOf2(b);
3891
+ if (pa !== pb) return pa - pb;
3892
+ return collator.compare(a.segment, b.segment) || (a.segment < b.segment ? -1 : a.segment > b.segment ? 1 : 0);
3893
+ });
3894
+ }
3895
+
3896
+ // packages/dev-server/src/storefront/redirects.ts
3897
+ function redirectsOf(f) {
3898
+ const v = f.frontmatter.values["redirectFrom"];
3899
+ return Array.isArray(v) ? v : typeof v === "string" ? [v] : [];
3900
+ }
3901
+ var RESERVED = [PD_ROUTE_PREFIX.preview, "/_pd/", PD_ROUTE_PREFIX.assets, "/api/", PD_ROUTE_PREFIX.source];
3902
+ function redirectProblems(files, workbenchPath) {
3903
+ const problems = [];
3904
+ const claimed = /* @__PURE__ */ new Map();
3905
+ for (const f of files.values()) {
3906
+ const at = `${f.path}${f.frontmatter.lines["redirectFrom"] !== void 0 ? `:${f.frontmatter.lines["redirectFrom"]}` : ""}`;
3907
+ for (const old of redirectsOf(f)) {
3908
+ if (typeof old !== "string" || !old.startsWith("/") || !old.endsWith("/")) continue;
3909
+ const page = files.get(old);
3910
+ if (page) {
3911
+ problems.push(`${at}: redirectFrom "${old}" is the page ${page.path}: a route can't both be a page and forward to one`);
3912
+ continue;
3913
+ }
3914
+ const reserved = RESERVED.find((r) => old.startsWith(r));
3915
+ if (reserved) {
3916
+ problems.push(`${at}: redirectFrom "${old}" is under ${reserved}, which Pattern Dump serves itself`);
3917
+ continue;
3918
+ }
3919
+ if (workbenchPath !== void 0 && old.startsWith(workbenchPath)) {
3920
+ problems.push(`${at}: redirectFrom "${old}" is ${workbenchPlace(old, workbenchPath)}`);
3921
+ continue;
3922
+ }
3923
+ const other = claimed.get(old);
3924
+ if (other && other !== f) {
3925
+ problems.push(`${at}: redirectFrom "${old}" is also claimed by ${other.path}: one old route forwards to one page`);
3926
+ continue;
3927
+ }
3928
+ claimed.set(old, f);
3929
+ }
3930
+ }
3931
+ return problems;
3932
+ }
3933
+ function workbenchPlace(route, workbenchPath) {
3934
+ return `${route === workbenchPath ? "at" : "under"} ${workbenchPath}, where the workbench is published on this host (storefront.workbenchUrl), so the workbench would overwrite it there`;
3935
+ }
3936
+ function redirectStub(p) {
3937
+ const target = escapeAttr(p.route);
3938
+ const canonical2 = p.siteUrl !== void 0 ? `
3939
+ <link rel="canonical" href="${escapeAttr(new URL(p.route.slice(1), p.siteUrl).href)}">` : "";
3940
+ return `<!DOCTYPE html>
3941
+ <html lang="${escapeAttr(p.lang)}">
3942
+ <head>
3943
+ <meta charset="UTF-8">
3944
+ <title>Moved to ${escapeHtml(p.title)}</title>${canonical2}
3945
+ <meta name="robots" content="noindex">
3946
+ <meta http-equiv="refresh" content="0; url=${target}">
3947
+ </head>
3948
+ <body>
3949
+ <p>This page has moved to <a href="${target}">${escapeHtml(p.title)}</a>.</p>
3950
+ </body>
3951
+ </html>
3952
+ `;
3953
+ }
3954
+
3955
+ // packages/dev-server/src/storefront/listing-keys.ts
3956
+ var DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
3957
+ var TAG = /^[a-z0-9-]+$/;
3958
+ var show = (v) => Array.isArray(v) ? `[${v.join(", ")}]` : `"${v}"`;
3959
+ function readDate(v) {
3960
+ const m = typeof v === "string" ? DATE.exec(v) : null;
3961
+ if (m) {
3962
+ const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
3963
+ const t = new Date(Date.UTC(y, mo - 1, d));
3964
+ if (t.getUTCFullYear() === y && t.getUTCMonth() === mo - 1 && t.getUTCDate() === d) return { ok: true, value: v };
3965
+ }
3966
+ return { ok: false, message: `date ${show(v)} is not a date: write it YYYY-MM-DD, such as "2026-11-14"` };
3967
+ }
3968
+ function readTags(v) {
3969
+ const list = Array.isArray(v) ? v : [v];
3970
+ const bad = list.filter((t) => !TAG.test(t));
3971
+ if (bad.length) {
3972
+ return { ok: false, message: `tags: ${bad.map((t) => `"${t}"`).join(", ")} ${bad.length === 1 ? "is" : "are"} not a tag: a tag is lowercase letters, digits and hyphens, such as "design-systems"` };
3973
+ }
3974
+ const twice = [...new Set(list.filter((t, i) => list.indexOf(t) !== i))];
3975
+ if (twice.length) return { ok: false, message: `tags: ${twice.map((t) => `"${t}"`).join(", ")} ${twice.length === 1 ? "is" : "are"} listed twice` };
3976
+ return { ok: true, value: list };
3977
+ }
3978
+ function readDraft(v) {
3979
+ return v === "true" || v === "false" ? { ok: true, value: v === "true" } : { ok: false, message: "draft must be true or false" };
3980
+ }
3981
+ function readSort(v, path) {
3982
+ if (path !== "index.md" && !path.endsWith("/index.md")) {
3983
+ return { ok: false, message: `sort orders a folder's pages, so it belongs on the folder's index.md, not on a page` };
3984
+ }
3985
+ return v === "date" || v === "order" ? { ok: true, value: v } : { ok: false, message: `sort must be "date" (newest first) or "order" (the folder's order, the default)` };
3986
+ }
3987
+
3988
+ // packages/dev-server/src/storefront/teaser.ts
3989
+ import { createHash as createHash5 } from "node:crypto";
3990
+ import MarkdownIt from "markdown-it";
3991
+ var md = new MarkdownIt({ html: true, linkify: false });
3992
+ function plainText(children) {
3993
+ let depth = 0;
3994
+ let out = "";
3995
+ for (const t of children) {
3996
+ if (t.type === "html_inline") {
3997
+ if (/^<pd-[a-z0-9-]+(?:\s[^>]*)?(?<!\/)>$/i.test(t.content.trim())) depth++;
3998
+ else if (/^<\/pd-/i.test(t.content.trim()) && depth > 0) depth--;
3999
+ continue;
4000
+ }
4001
+ if (depth > 0) continue;
4002
+ if (t.type === "text" || t.type === "code_inline") out += t.content;
4003
+ else if (t.type === "softbreak" || t.type === "hardbreak") out += " ";
4004
+ }
4005
+ return out.replace(/\s+/g, " ").trim();
4006
+ }
4007
+ function firstParagraph(body) {
4008
+ const tokens = md.parse(body, {});
4009
+ for (let i = 0; i < tokens.length; i++) {
4010
+ if (tokens[i].type !== "paragraph_open") continue;
4011
+ const text = plainText(tokens[i + 1]?.children ?? []);
4012
+ if (text !== "") return text;
4013
+ }
4014
+ return null;
4015
+ }
4016
+ var cache = /* @__PURE__ */ new Map();
4017
+ var LIMIT = 1e4;
4018
+ var teaserStats = { parses: 0 };
4019
+ function teaserOf(body) {
4020
+ const key = createHash5("sha256").update(body).digest("hex");
4021
+ if (cache.has(key)) return cache.get(key);
4022
+ teaserStats.parses++;
4023
+ const value = firstParagraph(body);
4024
+ if (cache.size >= LIMIT) cache.delete(cache.keys().next().value);
4025
+ cache.set(key, value);
4026
+ return value;
4027
+ }
4028
+
4029
+ // packages/dev-server/src/storefront/pages.ts
4030
+ var DEFAULT_LAYOUT = "page";
4031
+ var NOT_FOUND_FILE = "404.md";
4032
+ function routeFor(path) {
4033
+ const parts = path.replace(/\.md$/, "").split("/");
4034
+ if (parts.at(-1) === "index") parts.pop();
4035
+ return parts.length ? `/${parts.join("/")}/` : "/";
4036
+ }
4037
+ var NAME = /^[a-z0-9-]+$/;
4038
+ async function markdownFiles(dir, root, problems) {
4039
+ const out = [];
4040
+ const entries = (await readdir7(dir, { withFileTypes: true }).catch(() => [])).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
4041
+ for (const e of entries) {
4042
+ if (e.name.startsWith(".")) continue;
4043
+ const p = join17(dir, e.name);
4044
+ const rel = relative9(root, p).split(sep6).join("/");
4045
+ if (e.isDirectory()) {
4046
+ if (!NAME.test(e.name)) {
4047
+ problems.push(`${rel}/: a folder name is part of the URL, so it is lowercase letters, digits and hyphens only`);
4048
+ continue;
4049
+ }
4050
+ out.push(...await markdownFiles(p, root, problems));
4051
+ } else if (!e.name.endsWith(".md")) {
4052
+ problems.push(`${rel} is not a page: only .md files belong in the pages folder (images and other files go in the project's assets)`);
4053
+ } else if (!NAME.test(e.name.slice(0, -".md".length))) {
4054
+ problems.push(`${rel}: a page's file name is part of the URL, so it is lowercase letters, digits and hyphens only, then .md`);
4055
+ } else {
4056
+ out.push(rel);
4057
+ }
4058
+ }
4059
+ return out;
4060
+ }
4061
+ var scalar = (v) => typeof v === "string" ? v : void 0;
4062
+ async function readPages(pagesDir, opts = {}) {
4063
+ const problems = [];
4064
+ const files = /* @__PURE__ */ new Map();
4065
+ let notFound;
4066
+ if (!await stat4(pagesDir).then((s) => s.isDirectory(), () => false)) {
4067
+ return { pages: [], nav: [], files, entries: /* @__PURE__ */ new Map(), folders: /* @__PURE__ */ new Map(), drafts: [], problems: [`the pages folder ${pagesDir} does not exist`] };
4068
+ }
4069
+ const bodies = /* @__PURE__ */ new Map();
4070
+ for (const path of (await markdownFiles(pagesDir, pagesDir, problems)).sort()) {
4071
+ const route = routeFor(path);
4072
+ const other = files.get(route);
4073
+ if (other) {
4074
+ problems.push(`${other.path} and ${path} are both the page at ${route}`);
4075
+ continue;
4076
+ }
4077
+ const file = join17(pagesDir, ...path.split("/"));
4078
+ const text = await readFile11(file, "utf8").catch((e) => {
4079
+ if (e.code === "ENOENT") return null;
4080
+ throw e;
4081
+ });
4082
+ if (text === null) continue;
4083
+ if (path === NOT_FOUND_FILE) {
4084
+ notFound = { route: PD_ROUTES.notFound, file, path, frontmatter: readFrontmatterStrict(text) };
4085
+ continue;
4086
+ }
4087
+ files.set(route, { route, file, path, frontmatter: readFrontmatterStrict(text) });
4088
+ bodies.set(route, text);
4089
+ }
4090
+ const isDraft = (f) => {
4091
+ const v = f?.frontmatter.values["draft"];
4092
+ return v !== void 0 && (() => {
4093
+ const r = readDraft(v);
4094
+ return r.ok && r.value;
4095
+ })();
4096
+ };
4097
+ const drafts = [];
4098
+ for (const [route, f] of [...files]) {
4099
+ const segments = route.split("/").filter(Boolean);
4100
+ const folderDraft = segments.slice(0, -1).some((_, i) => isDraft(files.get(`/${segments.slice(0, i + 1).join("/")}/`)));
4101
+ if (!isDraft(f) && !(opts.drafts === "leave-out" && folderDraft)) continue;
4102
+ drafts.push(f.path);
4103
+ if (opts.drafts === "leave-out") {
4104
+ files.delete(route);
4105
+ bodies.delete(route);
4106
+ }
4107
+ }
4108
+ drafts.sort();
4109
+ const pageOf = (f) => {
4110
+ const redirectFrom = redirectsOf(f);
4111
+ return {
4112
+ route: f.route,
4113
+ title: scalar(f.frontmatter.values["title"]) ?? f.route,
4114
+ layout: scalar(f.frontmatter.values["layout"]) ?? DEFAULT_LAYOUT,
4115
+ ...redirectFrom.length ? { redirectFrom } : {}
4116
+ };
4117
+ };
4118
+ const entryOf = (f) => {
4119
+ const description = scalar(f.frontmatter.values["description"]);
4120
+ return { route: f.route, title: pageOf(f).title, ...description !== void 0 ? { description } : {} };
4121
+ };
4122
+ const weightOf = (f) => {
4123
+ const o = parseOrder(f?.frontmatter.values["order"]);
4124
+ return o && "weight" in o ? o.weight : void 0;
4125
+ };
4126
+ const listOf = (f) => {
4127
+ const o = parseOrder(f?.frontmatter.values["order"]);
4128
+ return o && "list" in o ? o.list : void 0;
4129
+ };
4130
+ const folders = /* @__PURE__ */ new Set();
4131
+ for (const f of files.values()) {
4132
+ const parts = f.path.split("/").slice(0, -1);
4133
+ for (let i = 1; i <= parts.length; i++) folders.add(`/${parts.slice(0, i).join("/")}/`);
4134
+ }
4135
+ if (!files.has("/")) problems.push("there is no home page: the pages folder needs an index.md at its top, the page at /");
4136
+ for (const folder of [...folders].sort()) {
4137
+ if (!files.has(folder)) problems.push(`the folder ${folder.slice(1)} holds pages but has no index.md \u2014 it needs one, as the folder's own page`);
4138
+ }
4139
+ const childrenOf = (folder) => {
4140
+ const depth = folder === "/" ? 0 : folder.split("/").length - 2;
4141
+ const out = /* @__PURE__ */ new Map();
4142
+ for (const route of [...files.keys(), ...folders]) {
4143
+ if (route === folder || !route.startsWith(folder)) continue;
4144
+ const segs = route.split("/").filter(Boolean);
4145
+ if (segs.length !== depth + 1) continue;
4146
+ out.set(route, segs[depth]);
4147
+ }
4148
+ return [...out].map(([route, segment]) => ({ route, segment }));
4149
+ };
4150
+ const ordered = (folder, extra = []) => {
4151
+ const items = childrenOf(folder).map((c3) => {
4152
+ const w = weightOf(files.get(c3.route));
4153
+ return { route: c3.route, segment: c3.segment, ...w !== void 0 ? { weight: w } : {} };
4154
+ });
4155
+ return orderSiblings([...extra, ...items], listOf(files.get(folder)));
4156
+ };
4157
+ const home = files.get("/");
4158
+ const homeWeight = weightOf(home);
4159
+ const top = ordered("/", home ? [{ route: "/", segment: "index", ...homeWeight !== void 0 ? { weight: homeWeight } : {} }] : []);
4160
+ const nav = [];
4161
+ for (const item of top) {
4162
+ const page = files.get(item.route);
4163
+ if (!page) continue;
4164
+ const children = folders.has(item.route) && item.route !== "/" ? ordered(item.route).flatMap((c3) => {
4165
+ const p = files.get(c3.route);
4166
+ return p ? [entryOf(p)] : [];
4167
+ }) : [];
4168
+ nav.push({ ...entryOf(page), ...children.length ? { children } : {} });
4169
+ }
4170
+ const inNav = nav.flatMap((n) => [n.route, ...(n.children ?? []).map((c3) => c3.route)]);
4171
+ const rest = [...files.keys()].filter((r) => !inNav.includes(r)).sort();
4172
+ const routes = [...inNav, ...rest];
4173
+ const pages = routes.map((r) => pageOf(files.get(r)));
4174
+ const { workbenchPath } = opts;
4175
+ if (workbenchPath !== void 0) {
4176
+ for (const f of files.values()) {
4177
+ if (f.route.startsWith(workbenchPath)) problems.push(`${f.path}: the page ${f.route} is ${workbenchPlace(f.route, workbenchPath)}`);
4178
+ }
4179
+ }
4180
+ problems.push(...redirectProblems(files, workbenchPath));
4181
+ const ok = (v, read) => {
4182
+ if (v === void 0) return void 0;
4183
+ const r = read(v);
4184
+ return r.ok ? r.value : void 0;
4185
+ };
4186
+ const entries = /* @__PURE__ */ new Map();
4187
+ for (const route of routes) {
4188
+ const f = files.get(route);
4189
+ const v = f.frontmatter.values;
4190
+ const description = scalar(v["description"]);
4191
+ const image = scalar(v["image"]);
4192
+ entries.set(route, {
4193
+ description: description ?? teaserOf(frontmatterBody(bodies.get(route))),
4194
+ date: ok(v["date"], readDate) ?? null,
4195
+ image: image !== void 0 && /^\/assets\/[^?#]+[^/?#]$/.test(image) ? image : null,
4196
+ tags: ok(v["tags"], readTags) ?? [],
4197
+ draft: ok(v["draft"], readDraft) ?? false
4198
+ });
4199
+ }
4200
+ const listings = /* @__PURE__ */ new Map();
4201
+ for (const folder of ["/", ...[...folders].sort()]) {
4202
+ const index = files.get(folder);
4203
+ if (!index) continue;
4204
+ listings.set(folder, {
4205
+ sort: ok(index.frontmatter.values["sort"], (v) => readSort(v, index.path)) ?? "order",
4206
+ children: ordered(folder).map((c3) => c3.route).filter((r) => files.has(r))
4207
+ });
4208
+ }
4209
+ return { pages, nav, files: new Map(routes.map((r) => [r, files.get(r)])), entries, folders: listings, drafts, problems, ...notFound ? { notFound } : {} };
4210
+ }
4211
+
4212
+ // packages/dev-server/src/storefront/crawl.ts
4213
+ import { request } from "node:http";
4214
+ function hostTransport(port2, host, basePath = "/") {
4215
+ return (liveUrl) => new Promise((resolve15, reject) => {
4216
+ const path = basePath === "/" || !liveUrl.startsWith("/") ? liveUrl : `${basePath}${liveUrl.slice(1)}`;
4217
+ const req = request({ host: "127.0.0.1", port: port2, path, headers: { host: `${host}:${port2}` } }, (res) => {
4218
+ const chunks = [];
4219
+ res.on("data", (c3) => chunks.push(c3));
4220
+ res.on("end", () => {
4221
+ const headers = new Headers();
4222
+ for (const [k, v] of Object.entries(res.headers)) if (typeof v === "string") headers.set(k, v);
4223
+ resolve15(new Response(Buffer.concat(chunks), { status: res.statusCode ?? 0, headers }));
4224
+ });
4225
+ });
4226
+ req.on("error", reject);
4227
+ req.end();
4228
+ });
4229
+ }
4230
+ var unescapeHtml = (s) => s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, `"`).replace(/&amp;/g, "&");
4231
+ var toSite = (ref, base2) => base2 === "/" ? ref : ref.startsWith(base2) ? `/${ref.slice(base2.length)}` : null;
4232
+ var markupRefs = (html, base2) => [
4233
+ ...[...html.matchAll(/(?:href|src)="(\/[^"]+)"/g)].map((m) => unescapeHtml(m[1])),
4234
+ // Each candidate of a srcset: its URL, before the width or density that describes it.
4235
+ ...[...html.matchAll(/srcset="([^"]+)"/g)].flatMap((m) => unescapeHtml(m[1]).split(",").map((c3) => c3.trim().split(/\s+/)[0] ?? "")),
4236
+ ...[...html.matchAll(/<meta property="og:image" content="https?:\/\/[^/"]+(\/[^"]+)">/g)].map((m) => unescapeHtml(m[1]))
4237
+ ].map((u) => toSite(u, base2)).filter((u) => u !== null && /^\/(?:_pd|assets|preview)\//.test(u));
4238
+ var cssRefs = (css, base2) => [...css.matchAll(/url\(\s*(['"]?)(\/[^'")\s]+)\1\s*\)/g)].map((m) => toSite(m[2], base2)).filter((u) => u !== null && /^\/(?:_pd|assets)\//.test(u));
4239
+ async function crawlStorefront(get, files = [], basePath = "/") {
4240
+ const problems = [];
4241
+ const siteProblems = /* @__PURE__ */ new Set();
4242
+ const list = await get(PD_ROUTES.pages);
4243
+ if (list.status !== 200) {
4244
+ const body = await list.text();
4245
+ let error = body;
4246
+ try {
4247
+ error = JSON.parse(body).error ?? body;
4248
+ } catch {
4249
+ }
4250
+ return { urls: [], problems: error.split("\n").filter(Boolean), documents: /* @__PURE__ */ new Map() };
4251
+ }
4252
+ const { pages } = await list.json();
4253
+ const urls = [];
4254
+ const documents = /* @__PURE__ */ new Map();
4255
+ const seen = /* @__PURE__ */ new Set();
4256
+ const queue = [];
4257
+ const add = (url) => {
4258
+ if (!seen.has(url)) {
4259
+ seen.add(url);
4260
+ queue.push(url);
4261
+ }
4262
+ };
4263
+ for (const p of pages) add(p.route);
4264
+ for (const p of pages) for (const old of p.redirectFrom ?? []) add(old);
4265
+ for (const f of files) add(f);
4266
+ while (queue.length) {
4267
+ const url = queue.shift();
4268
+ const res = await get(url);
4269
+ const type = res.headers.get("content-type") ?? "";
4270
+ if (res.status !== (url === PD_ROUTES.notFound ? 404 : 200)) {
4271
+ const body = await res.text();
4272
+ const items = [...body.matchAll(/<li( data-pd-site)?>([\s\S]*?)<\/li>/g)].map((m) => ({ site: !!m[1], text: unescapeHtml(m[2]) }));
4273
+ for (const i of items) {
4274
+ if (!i.site) problems.push(`${url}: ${i.text}`);
4275
+ else if (!siteProblems.has(i.text)) {
4276
+ siteProblems.add(i.text);
4277
+ problems.push(i.text);
4278
+ }
4279
+ }
4280
+ if (!items.length) problems.push(`${url}: ${res.status} ${body.split("\n")[0]?.slice(0, 160) ?? ""}`.trimEnd());
4281
+ continue;
4282
+ }
4283
+ urls.push(url);
4284
+ if (/text\/html/.test(type)) {
4285
+ const html = await res.text();
4286
+ documents.set(url, html);
4287
+ for (const ref of markupRefs(html, basePath)) add(ref);
4288
+ } else if (/text\/css/.test(type)) for (const ref of cssRefs(await res.text(), basePath)) add(ref);
4289
+ }
4290
+ return { urls, problems, documents };
4291
+ }
4292
+
4293
+ // packages/dev-server/src/storefront/html-tags.ts
4294
+ var MARKUP = /<!--[\s\S]*?-->|<([a-zA-Z][\w:-]*)((?:\s+[^\s"'>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*)\s*\/?>/g;
4295
+ var ATTRIBUTE = /(\s+)([^\s"'>/=]+)(?:(\s*=\s*)("[^"]*"|'[^']*'|[^\s"'=<>`]+))?/g;
4296
+ var RAW_TEXT = /* @__PURE__ */ new Set(["script", "style", "textarea", "title"]);
4297
+ function startTags(html) {
4298
+ const tags = [];
4299
+ const markup = new RegExp(MARKUP.source, "g");
4300
+ const lower = html.toLowerCase();
4301
+ for (let m = markup.exec(html); m; m = markup.exec(html)) {
4302
+ const name = m[1];
4303
+ if (name === void 0) continue;
4304
+ const tag = { name, start: m.index, end: markup.lastIndex, attrs: m[2] ?? "" };
4305
+ const key = name.toLowerCase();
4306
+ if (RAW_TEXT.has(key)) {
4307
+ const close = lower.indexOf(`</${key}`, tag.end);
4308
+ tag.content = { start: tag.end, end: close < 0 ? html.length : close };
4309
+ markup.lastIndex = tag.content.end;
4310
+ }
4311
+ tags.push(tag);
4312
+ }
4313
+ return tags;
4314
+ }
4315
+ function decodeAttribute(written) {
4316
+ const q = written[0] === '"' || written[0] === "'" ? written.slice(1, -1) : written;
4317
+ return q.replace(/&(#x[0-9a-f]+|#[0-9]+|amp|lt|gt|quot|apos|#39);/gi, (_, e) => {
4318
+ const k = e.toLowerCase();
4319
+ if (k.startsWith("#x")) return String.fromCodePoint(parseInt(k.slice(2), 16));
4320
+ if (k.startsWith("#")) return String.fromCodePoint(parseInt(k.slice(1), 10));
4321
+ return { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" }[k] ?? `&${e};`;
4322
+ });
4323
+ }
4324
+ function attributesOf(tag) {
4325
+ return [...tag.attrs.matchAll(ATTRIBUTE)].map((m) => ({ name: m[2].toLowerCase(), value: m[4] === void 0 ? void 0 : decodeAttribute(m[4]) }));
4326
+ }
4327
+
4328
+ // packages/dev-server/src/storefront/links.ts
4329
+ var SKIPPED = /^(?:mailto|tel|data|javascript):/i;
4330
+ var OTHER_HOST = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
4331
+ function idsOf(html) {
4332
+ const ids = /* @__PURE__ */ new Set();
4333
+ for (const tag of startTags(html)) {
4334
+ for (const a of attributesOf(tag)) {
4335
+ if (a.value === void 0) continue;
4336
+ if (a.name === "id" || a.name === "name" && tag.name.toLowerCase() === "a") ids.add(a.value);
4337
+ }
4338
+ }
4339
+ return ids;
4340
+ }
4341
+ function referencesOf(html) {
4342
+ const refs = [];
4343
+ for (const tag of startTags(html)) {
4344
+ for (const a of attributesOf(tag)) {
4345
+ if (a.value === void 0) continue;
4346
+ if (a.name === "href" || a.name === "src") refs.push([tag.name.toLowerCase(), a.name, a.value.trim()]);
4347
+ else if (a.name === "srcset") {
4348
+ for (const c3 of a.value.split(",")) {
4349
+ const url = c3.trim().split(/\s+/)[0];
4350
+ if (url) refs.push([tag.name.toLowerCase(), a.name, url]);
4351
+ }
4352
+ }
4353
+ }
4354
+ }
4355
+ return refs;
4356
+ }
4357
+ function linkedWorkbenchRoutes(documents, pages, workbenchUrl) {
4358
+ const routes = /* @__PURE__ */ new Set();
4359
+ for (const page of pages) {
4360
+ for (const [, , ref] of referencesOf(documents.get(page) ?? "")) {
4361
+ let rest;
4362
+ if (OTHER_HOST.test(workbenchUrl)) {
4363
+ if (ref.startsWith(workbenchUrl)) rest = ref.slice(workbenchUrl.length);
4364
+ } else if (!OTHER_HOST.test(ref)) {
4365
+ const path = new URL(ref, `http://site.invalid${page}`).pathname;
4366
+ if (path.startsWith(workbenchUrl)) rest = path.slice(workbenchUrl.length);
4367
+ }
4368
+ if (rest !== void 0) routes.add(decodeURIComponent(rest.split(/[?#]/)[0].replace(/\/$/, "")));
4369
+ }
4370
+ }
4371
+ return routes;
4372
+ }
4373
+ function checkLinks(site) {
4374
+ const problems = [];
4375
+ const external = /* @__PURE__ */ new Map();
4376
+ const idCache = /* @__PURE__ */ new Map();
4377
+ const ids = (url) => {
4378
+ const html = site.documents.get(url);
4379
+ if (html === void 0) return void 0;
3850
4380
  let found = idCache.get(url);
3851
4381
  if (!found) idCache.set(url, found = idsOf(html));
3852
4382
  return found;
@@ -3867,6 +4397,8 @@ function checkLinks(site) {
3867
4397
  if (hash && !(wb.variants.get(route) ?? []).includes(decodeURIComponent(hash))) return `${route} has no variant "${hash}" in the workbench`;
3868
4398
  return void 0;
3869
4399
  };
4400
+ const base2 = site.basePath ?? "/";
4401
+ const hint = site.pathHint ?? base2;
3870
4402
  let references = 0;
3871
4403
  for (const page of site.pages) {
3872
4404
  const html = site.documents.get(page);
@@ -3882,7 +4414,7 @@ function checkLinks(site) {
3882
4414
  continue;
3883
4415
  }
3884
4416
  let href = ref;
3885
- if (site.siteUrl && ref.startsWith(site.siteUrl)) href = `/${ref.slice(site.siteUrl.length)}`;
4417
+ if (site.siteUrl && ref.startsWith(site.siteUrl)) href = `${base2}${ref.slice(site.siteUrl.length)}`;
3886
4418
  else if (OTHER_HOST.test(ref)) {
3887
4419
  if (site.pagesOnly) continue;
3888
4420
  const list = external.get(ref) ?? [];
@@ -3890,18 +4422,24 @@ function checkLinks(site) {
3890
4422
  external.set(ref, list);
3891
4423
  continue;
3892
4424
  }
3893
- const url = new URL(href, `http://site.invalid${page}`);
3894
- const path = decodeURIComponent(url.pathname);
4425
+ const url = new URL(href, `http://site.invalid${base2}${page.slice(1)}`);
4426
+ const served = decodeURIComponent(url.pathname);
3895
4427
  const hash = url.hash.slice(1);
3896
- if (wb && wb.url.startsWith("/") && path.startsWith(wb.url)) {
3897
- const why = workbenchProblem(path.slice(wb.url.length), hash);
4428
+ if (wb && wb.url.startsWith("/") && served.startsWith(wb.url)) {
4429
+ const why = workbenchProblem(served.slice(wb.url.length), hash);
3898
4430
  if (why) say(why);
3899
4431
  continue;
3900
4432
  }
4433
+ if (!served.startsWith(base2)) {
4434
+ say(`${served} is outside ${base2}, the path the site is published under (storefront.url)`);
4435
+ continue;
4436
+ }
4437
+ const path = `/${served.slice(base2.length)}`;
3901
4438
  if (site.pagesOnly && /^\/(?:_pd|assets|preview)\//.test(path)) continue;
3902
4439
  const target = builtAs(path);
3903
4440
  if (target === void 0) {
3904
- say(`no page or file is built at ${path}`);
4441
+ const doubled = hint !== "/" && path.startsWith(hint) && builtAs(`/${path.slice(hint.length)}`) !== void 0;
4442
+ say(doubled ? `no page or file is built at ${path}: the link is written with the site's path ${hint} already; write it from the site's root, such as /${path.slice(hint.length)}, and the path is put in front of it` : `no page or file is built at ${path}`);
3905
4443
  continue;
3906
4444
  }
3907
4445
  if (!hash) continue;
@@ -3928,6 +4466,135 @@ function checkPageBasics(html) {
3928
4466
  return problems;
3929
4467
  }
3930
4468
 
4469
+ // packages/dev-server/src/storefront/browser-checks.ts
4470
+ import { createRequire as createRequire4 } from "node:module";
4471
+ import { join as join18 } from "node:path";
4472
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
4473
+ var BrowserUnavailableError = class extends Error {
4474
+ };
4475
+ var INSTALL = "npm install --save-dev playwright, then npx playwright install chromium";
4476
+ async function loadProjectPlaywright(root) {
4477
+ const require5 = createRequire4(join18(root, "package.json"));
4478
+ for (const name of ["playwright", "@playwright/test"]) {
4479
+ let resolved;
4480
+ try {
4481
+ resolved = require5.resolve(name);
4482
+ } catch {
4483
+ continue;
4484
+ }
4485
+ const mod = await import(pathToFileURL2(resolved).href);
4486
+ const chromium = mod.chromium ?? mod.default?.chromium;
4487
+ if (chromium) return { chromium };
4488
+ }
4489
+ throw new BrowserUnavailableError(`pd build --browser needs Playwright in this project: ${INSTALL}`);
4490
+ }
4491
+ var VIEWPORTS = [
4492
+ { name: "1000\xD7720", viewport: { width: 1e3, height: 720 }, dpr: 1 },
4493
+ { name: "390\xD7844", viewport: { width: 390, height: 844 }, dpr: 2 }
4494
+ ];
4495
+ async function runBrowserChecks(o) {
4496
+ const base2 = o.basePath ?? "/";
4497
+ let browser;
4498
+ try {
4499
+ browser = await o.playwright.chromium.launch({ headless: true });
4500
+ } catch (e) {
4501
+ throw new BrowserUnavailableError(`pd build --browser couldn't start Chromium (${(e instanceof Error ? e.message : String(e)).split("\n")[0]}): ${INSTALL}`);
4502
+ }
4503
+ const origin = "http://site.invalid";
4504
+ const axe2 = axeScript();
4505
+ const found = /* @__PURE__ */ new Map();
4506
+ const note = (kind, key, text, at) => {
4507
+ const f = found.get(key) ?? { kind, text, at: [] };
4508
+ f.at.push(at);
4509
+ found.set(key, f);
4510
+ };
4511
+ let current = "";
4512
+ try {
4513
+ for (const vp of VIEWPORTS) {
4514
+ const context = await browser.newContext({ viewport: vp.viewport, deviceScaleFactor: vp.dpr });
4515
+ await context.route("**/*", async (route) => {
4516
+ const url = new URL(route.request().url());
4517
+ if (url.origin !== origin) {
4518
+ await route.abort();
4519
+ return;
4520
+ }
4521
+ if (!url.pathname.startsWith(base2)) {
4522
+ if (url.pathname !== "/favicon.ico") {
4523
+ const text = `${current}: a request to ${url.pathname}${url.search} is outside ${base2}, the path the site is published under (storefront.url): a script that builds this URL needs the site's path, which <meta name="pd-base-path"> names`;
4524
+ note("error", `outside ${text}`, text, vp.name);
4525
+ }
4526
+ await route.fulfill({ status: 404, body: Buffer.from("") });
4527
+ return;
4528
+ }
4529
+ const res = await o.get(`/${url.pathname.slice(base2.length)}${url.search}`);
4530
+ const headers = {};
4531
+ const type = res.headers.get("content-type");
4532
+ if (type) headers["content-type"] = type;
4533
+ await route.fulfill({ status: res.status, headers, body: Buffer.from(await res.arrayBuffer()) });
4534
+ });
4535
+ const page = await context.newPage();
4536
+ for (const path of o.pages) {
4537
+ current = path;
4538
+ await page.goto(`${origin}${base2}${path.slice(1)}`, { waitUntil: "load" });
4539
+ await page.addScriptTag({ content: axe2 });
4540
+ const violations = await page.evaluate(async (rules) => {
4541
+ const r = await window.axe.run(document, { runOnly: { type: "rule", values: rules }, resultTypes: ["violations"], iframes: false });
4542
+ return r.violations;
4543
+ }, pageRuleTiers.browser);
4544
+ for (const v of violations) {
4545
+ const where = v.nodes.map((n) => n.target.map(String).join(" ")).join(", ");
4546
+ const text = `${path}: ${v.id} (${v.impact ?? "minor"}): ${v.help}, on ${where} (${v.helpUrl})`;
4547
+ note(severityOf(v.impact, v.tags) === "error" ? "error" : "a11y", `a11y ${text}`, text, vp.name);
4548
+ }
4549
+ const images = await page.evaluate(async () => {
4550
+ const seen = [];
4551
+ for (const img of Array.from(document.querySelectorAll("img[srcset]"))) {
4552
+ img.scrollIntoView({ block: "center", inline: "center" });
4553
+ if (!img.complete || img.naturalWidth === 0) {
4554
+ await new Promise((resolve15) => {
4555
+ const done = () => resolve15();
4556
+ img.addEventListener("load", done, { once: true });
4557
+ img.addEventListener("error", done, { once: true });
4558
+ setTimeout(done, 5e3);
4559
+ });
4560
+ }
4561
+ const candidates = img.srcset.split(",").map((c3) => c3.trim().split(/\s+/)).map(([u, d]) => ({ url: new URL(u, location.href).href, w: parseInt(d ?? "", 10) }));
4562
+ const chosen = candidates.find((c3) => c3.url === img.currentSrc);
4563
+ seen.push({
4564
+ src: img.getAttribute("src") ?? "",
4565
+ sizes: img.sizes,
4566
+ box: img.getBoundingClientRect().width,
4567
+ chosen: chosen && !Number.isNaN(chosen.w) ? chosen.w : null,
4568
+ candidates: candidates.map((c3) => c3.w).filter((w) => !Number.isNaN(w))
4569
+ });
4570
+ }
4571
+ return seen;
4572
+ }, void 0);
4573
+ for (const img of images) {
4574
+ if (img.chosen === null || img.box === 0) continue;
4575
+ const need = Math.round(img.box * vp.dpr);
4576
+ const covering = img.candidates.filter((w) => w >= need).sort((a, b) => a - b)[0];
4577
+ const written = img.src.replace(/\?v=[0-9a-f]+$/, "");
4578
+ const src = base2 !== "/" && written.startsWith(base2) ? `/${written.slice(base2.length)}` : written;
4579
+ const asset = /^\/_pd\/images\/(.+)\.[0-9a-f]{10}\.\d+\.webp$/.exec(src)?.[1];
4580
+ const what = asset ? `the image ${asset}` : `<img src="${src}">`;
4581
+ const box = `is laid out ${Math.round(img.box)} px wide (${need} px at ${vp.dpr}x)`;
4582
+ if (covering !== void 0 && img.chosen > covering) {
4583
+ note("image", `image ${path} ${src} large`, `${path}: ${what} ${box} but takes the ${img.chosen}w variant where ${covering}w would do: its sizes, "${img.sizes}", says it is wider than it is`, vp.name);
4584
+ } else if (img.chosen < need && img.candidates.some((w) => w > img.chosen)) {
4585
+ note("image", `image ${path} ${src} small`, `${path}: ${what} ${box} but takes the ${img.chosen}w variant, which is too small: its sizes, "${img.sizes}", says it is narrower than it is`, vp.name);
4586
+ }
4587
+ }
4588
+ }
4589
+ await context.close();
4590
+ }
4591
+ } finally {
4592
+ await browser.close();
4593
+ }
4594
+ const lines = (kind) => [...found.values()].filter((f) => f.kind === kind).map((f) => `${f.text} (at ${f.at.join(" and ")})`);
4595
+ return { errors: lines("error"), a11yWarnings: lines("a11y"), images: lines("image") };
4596
+ }
4597
+
3931
4598
  // packages/dev-server/src/storefront/site-files.ts
3932
4599
  var escapeXml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
3933
4600
  function sitemapXml(entries) {
@@ -3954,6 +4621,68 @@ Sitemap: ${p.siteUrl}sitemap.xml
3954
4621
  ` : ""}`;
3955
4622
  }
3956
4623
 
4624
+ // packages/dev-server/src/storefront/base-path.ts
4625
+ var URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src", "poster", "action", "formaction", "xlink:href"]);
4626
+ var SRCSET_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset"]);
4627
+ function withBase(ref, p) {
4628
+ if (p.base === "/" || !ref.startsWith("/") || ref.startsWith("//") || ref.startsWith("/\\")) return ref;
4629
+ if (p.workbenchPath !== void 0 && p.workbenchPath !== "/" && ref.startsWith(p.workbenchPath)) return ref;
4630
+ return `${p.base}${ref.slice(1)}`;
4631
+ }
4632
+ function withBaseSrcset(value, p) {
4633
+ return value.split(",").map((entry) => {
4634
+ const m = /^(\s*)(\S+)(.*)$/s.exec(entry);
4635
+ return m ? `${m[1]}${withBase(m[2], p)}${m[3]}` : entry;
4636
+ }).join(",");
4637
+ }
4638
+ function withBaseCss(css, p) {
4639
+ if (p.base === "/") return css;
4640
+ return css.replace(/url\(\s*(['"]?)([^'")\s]+)\1\s*\)/g, (whole, q, ref) => {
4641
+ const out = withBase(ref, p);
4642
+ return out === ref ? whole : `url(${q}${out}${q})`;
4643
+ }).replace(/(@import\s+)(["'])([^"']+)\2/g, (_whole, at, q, ref) => `${at}${q}${withBase(ref, p)}${q}`);
4644
+ }
4645
+ function withBaseAttrs(name, attrs, p) {
4646
+ const tag = name.toLowerCase();
4647
+ const refresh = tag === "meta" && /\shttp-equiv\s*=\s*["']?refresh/i.test(attrs);
4648
+ return attrs.replace(ATTRIBUTE, (whole, lead, attr2, eq, quoted) => {
4649
+ if (quoted === void 0 || eq === void 0) return whole;
4650
+ const q = quoted[0] === '"' || quoted[0] === "'" ? quoted[0] : "";
4651
+ const value = q ? quoted.slice(1, -1) : quoted;
4652
+ const key = attr2.toLowerCase();
4653
+ let out = value;
4654
+ if (URL_ATTRIBUTES.has(key) || key === "data" && tag === "object") out = withBase(value, p);
4655
+ else if (SRCSET_ATTRIBUTES.has(key)) out = withBaseSrcset(value, p);
4656
+ else if (key === "style") out = withBaseCss(value, p);
4657
+ else if (key === "content" && refresh) {
4658
+ out = value.replace(/^(\s*[\d.]*\s*[;,]\s*(?:url\s*=\s*)?)(['"]?)(\S[^'"]*)/i, (_m, head, uq, ref) => `${head}${uq}${withBase(ref, p)}`);
4659
+ }
4660
+ return out === value ? whole : `${lead}${attr2}${eq}${q}${out}${q}`;
4661
+ });
4662
+ }
4663
+ function withBaseHtml(html, p) {
4664
+ if (p.base === "/") return html;
4665
+ const parts = [];
4666
+ let last = 0;
4667
+ for (const tag of startTags(html)) {
4668
+ parts.push(html.slice(last, tag.start));
4669
+ const attrsAt = tag.start + 1 + tag.name.length;
4670
+ parts.push(`<${tag.name}${withBaseAttrs(tag.name, tag.attrs, p)}${html.slice(attrsAt + tag.attrs.length, tag.end)}`);
4671
+ last = tag.end;
4672
+ if (!tag.content) continue;
4673
+ const content = html.slice(tag.content.start, tag.content.end);
4674
+ parts.push(tag.name.toLowerCase() === "style" ? withBaseCss(content, p) : content);
4675
+ last = tag.content.end;
4676
+ }
4677
+ parts.push(html.slice(last));
4678
+ return parts.join("");
4679
+ }
4680
+ function withNoindex(html) {
4681
+ if (/<meta\s+name=["']?robots["']?/i.test(html)) return html;
4682
+ return html.replace(/<head(\s[^>]*)?>/i, (head) => `${head}
4683
+ <meta name="robots" content="noindex">`);
4684
+ }
4685
+
3957
4686
  // packages/dev-server/src/static-build.ts
3958
4687
  var notABarrier = (path) => basename4(path) !== ".__testOnly_barrier";
3959
4688
  async function buildStaticSite(config, root, opts) {
@@ -3966,8 +4695,8 @@ async function buildStaticSite(config, root, opts) {
3966
4695
  const outDir = resolve7(opts.outDir);
3967
4696
  const cfg = resolveServerConfig(config, root, {}, loadPatternPackages(config.packages, root));
3968
4697
  const inside2 = (child, parent) => {
3969
- const rel = relative9(parent, child);
3970
- return rel === "" || !rel.startsWith("..") && !rel.startsWith(sep6) && !/^[a-zA-Z]:/.test(rel);
4698
+ const rel = relative10(parent, child);
4699
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith(sep7) && !/^[a-zA-Z]:/.test(rel);
3971
4700
  };
3972
4701
  const project = resolve7(root);
3973
4702
  if (inside2(project, outDir)) throw new Error(`pd build: --out ${outDir} would contain the project itself \u2014 choose a folder inside or beside it, like ./dist`);
@@ -3996,6 +4725,7 @@ async function buildStaticSite(config, root, opts) {
3996
4725
  throw new Error(`pd build: storefront.workbenchUrl is ${storefront.workbenchUrl}, so the workbench must be built for ${sameHost} (build.basePath or --base); it is being built for ${basePath}`);
3997
4726
  }
3998
4727
  }
4728
+ const playwright = opts.browser && storefront ? await (opts.loadPlaywright ?? loadProjectPlaywright)(project) : void 0;
3999
4729
  const fixed = opts.now;
4000
4730
  const now = fixed ? () => new Date(fixed.getTime()) : () => /* @__PURE__ */ new Date();
4001
4731
  const storefrontNotes = /* @__PURE__ */ new Set();
@@ -4009,9 +4739,14 @@ async function buildStaticSite(config, root, opts) {
4009
4739
  }
4010
4740
  });
4011
4741
  log("\u2026 server ready");
4012
- const sfGet = storefront ? hostTransport(server.port, storefront.host) : null;
4013
- const sfFiles = storefront ? [PD_ROUTES.robots, PD_ROUTES.notFound, ...storefront.url !== void 0 && storefront.index ? [PD_ROUTES.sitemap] : []] : [];
4014
- const sfCrawl = sfGet ? await crawlStorefront(sfGet, sfFiles) : null;
4742
+ const sfBase = storefront?.basePath ?? "/";
4743
+ const sfGet = storefront ? hostTransport(server.port, storefront.host, sfBase) : null;
4744
+ const sfFiles = storefront ? [
4745
+ ...sfBase === "/" ? [PD_ROUTES.robots] : [],
4746
+ PD_ROUTES.notFound,
4747
+ ...storefront.url !== void 0 && storefront.index ? [PD_ROUTES.sitemap] : []
4748
+ ] : [];
4749
+ const sfCrawl = sfGet ? await crawlStorefront(sfGet, sfFiles, sfBase) : null;
4015
4750
  if (sfCrawl && sfCrawl.problems.length) {
4016
4751
  await server.close();
4017
4752
  throw new Error(`pd build: the storefront has problems, so nothing was written:
@@ -4021,6 +4756,7 @@ ${sfCrawl.problems.map((p) => ` - ${p}`).join("\n")}`);
4021
4756
  const origin = `http://127.0.0.1:${server.port}`;
4022
4757
  let linkSummary;
4023
4758
  let a11ySummary;
4759
+ let imageSummary;
4024
4760
  const externalNotes = [];
4025
4761
  if (storefront && sfGet && sfCrawl) {
4026
4762
  const { pages: list } = await (await sfGet(PD_ROUTES.pages)).json();
@@ -4048,6 +4784,7 @@ ${sfCrawl.problems.map((p) => ` - ${p}`).join("\n")}`);
4048
4784
  documents: sfCrawl.documents,
4049
4785
  pages,
4050
4786
  ...storefront.url !== void 0 ? { siteUrl: storefront.url.endsWith("/") ? storefront.url : `${storefront.url}/` } : {},
4787
+ basePath: sfBase,
4051
4788
  workbench: { url: wbUrl, routes, variants }
4052
4789
  });
4053
4790
  if (links.problems.length) {
@@ -4081,15 +4818,34 @@ ${links.problems.map((p) => ` - ${p}`).join("\n")}`);
4081
4818
  throw new Error(`pd build: the storefront has accessibility problems, so nothing was written:
4082
4819
  ${errors.map((e) => ` - ${e}`).join("\n")}`);
4083
4820
  }
4821
+ let imageWarnings;
4822
+ const browserWarnings = [];
4823
+ if (playwright) {
4824
+ const checked = await runBrowserChecks({ pages, get: sfGet, playwright, basePath: sfBase }).catch(async (e) => {
4825
+ await server.close();
4826
+ throw e;
4827
+ });
4828
+ if (checked.errors.length) {
4829
+ await server.close();
4830
+ throw new Error(`pd build: the storefront has problems in a browser, so nothing was written:
4831
+ ${checked.errors.map((e) => ` - ${e}`).join("\n")}`);
4832
+ }
4833
+ browserWarnings.push(...checked.a11yWarnings);
4834
+ imageWarnings = checked.images;
4835
+ log(`\u2026 checked in Chromium at ${VIEWPORTS.map((v) => `${v.name} @${v.dpr}x`).join(" and ")}`);
4836
+ }
4837
+ imageSummary = imageWarnings ? { warnings: imageWarnings } : void 0;
4084
4838
  a11ySummary = {
4085
4839
  pages: pages.length,
4086
- warnings: [...warned.values()].map((w) => `${w.finding.ruleId} (${w.finding.impact}): ${w.finding.help}, ${w.elements} element${w.elements === 1 ? "" : "s"} on ${w.pages.join(", ")} (${w.finding.helpUrl})`)
4840
+ ...playwright ? { browser: true } : {},
4841
+ warnings: [...[...warned.values()].map((w) => `${w.finding.ruleId} (${w.finding.impact}): ${w.finding.help}, ${w.elements} element${w.elements === 1 ? "" : "s"} on ${w.pages.join(", ")} (${w.finding.helpUrl})`), ...browserWarnings]
4087
4842
  };
4088
4843
  log(`\u2026 accessibility checked on ${pages.length} pages`);
4089
4844
  }
4090
4845
  await rm3(outDir, { recursive: true, force: true });
4091
4846
  await mkdir5(outDir, { recursive: true });
4092
4847
  const report = { outDir, basePath, files: 0, bytes: 0, components: 0, warnings: [], notes: [], ms: 0 };
4848
+ if (opts.browser && !storefront) report.notes.push("--browser checks a storefront's pages in Chromium, and this project has no storefront: nothing was checked");
4093
4849
  const { capture, store } = createSnapshotWriter({ outDir, basePath, origin, report });
4094
4850
  const referenced = (html) => [...html.matchAll(/(?:href|src)="(\/_pd\/(?:components|themes|scripts)\/[^"]+)"/g)].map((m) => m[1].replace(/&amp;/g, "&"));
4095
4851
  try {
@@ -4128,9 +4884,9 @@ ${errors.map((e) => ` - ${e}`).join("\n")}`);
4128
4884
  for (const variant of [void 0, ...base2.variants.map((v) => v.name)]) {
4129
4885
  const detailText = variant ? await capture(componentUrl(id, variant)) : baseText;
4130
4886
  if (detailText) {
4131
- const { document: document2 } = JSON.parse(detailText);
4132
- await store(previewUrl(id, variant), document2);
4133
- for (const ref of referenced(document2)) await capture(ref);
4887
+ const { document: document3 } = JSON.parse(detailText);
4888
+ await store(previewUrl(id, variant), basePath === "/" ? document3 : withNoindex(document3));
4889
+ for (const ref of referenced(document3)) await capture(ref);
4134
4890
  }
4135
4891
  await capture(renderedSourceUrl(id, variant), { optional: true });
4136
4892
  await capture(dataSourceUrl(id, variant), { optional: true });
@@ -4152,13 +4908,13 @@ ${errors.map((e) => ` - ${e}`).join("\n")}`);
4152
4908
  for (const [i, rootDir] of cfg.assetRoots.entries()) {
4153
4909
  const by = shadowed.get(rootDir);
4154
4910
  if (by !== void 0) {
4155
- report.warnings.push(`assets: ${relative9(root, rootDir)} is hidden by ${relative9(root, by) || "."}, which has the same folder name; left out`);
4911
+ report.warnings.push(`assets: ${relative10(root, rootDir)} is hidden by ${relative10(root, by) || "."}, which has the same folder name; left out`);
4156
4912
  continue;
4157
4913
  }
4158
- const exists = await stat4(rootDir).then((s) => s.isDirectory(), () => false);
4914
+ const exists = await stat5(rootDir).then((s) => s.isDirectory(), () => false);
4159
4915
  if (!exists) continue;
4160
- await cp(rootDir, join17(outDir, "assets", cfg.assetRootNames[i]), { recursive: true, filter: notABarrier });
4161
- if (cfg.assetRoots.length === 1) await cp(rootDir, join17(outDir, "assets"), { recursive: true, force: false, filter: notABarrier });
4916
+ await cp(rootDir, join19(outDir, "assets", cfg.assetRootNames[i]), { recursive: true, filter: notABarrier });
4917
+ if (cfg.assetRoots.length === 1) await cp(rootDir, join19(outDir, "assets"), { recursive: true, force: false, filter: notABarrier });
4162
4918
  }
4163
4919
  const runtime = {
4164
4920
  mode: "static",
@@ -4174,12 +4930,12 @@ ${errors.map((e) => ` - ${e}`).join("\n")}`);
4174
4930
  report.warnings.push(`shell page for route ${route} not written: ${staticPathFor(route)} already holds the server's response to ${holder}`);
4175
4931
  }
4176
4932
  }
4177
- await writeFile5(join17(outDir, ".nojekyll"), "");
4933
+ await writeFile5(join19(outDir, ".nojekyll"), "");
4178
4934
  report.files++;
4179
- await writeFile5(join17(outDir, "robots.txt"), workbenchRobotsTxt(basePath));
4935
+ await writeFile5(join19(outDir, "robots.txt"), workbenchRobotsTxt(basePath));
4180
4936
  report.files++;
4181
- if (basePath !== "/" && sameHostWorkbenchPath(storefront ?? void 0) !== basePath) {
4182
- report.notes.push(`robots.txt is written to the workbench's root, but crawlers read it only at a host's root: with the base path ${basePath}, add its rule (Disallow: ${basePath}preview/) to the robots.txt the host serves at /`);
4937
+ if (basePath !== "/" && (sameHostWorkbenchPath(storefront ?? void 0) !== basePath || sfBase !== "/")) {
4938
+ report.notes.push(`robots.txt is written to the workbench's root, but crawlers read it only at a host's root: with the base path ${basePath}, each preview carries a robots noindex tag instead; where you can, also add its rule (Disallow: ${basePath}preview/) to the robots.txt the host serves at /`);
4183
4939
  }
4184
4940
  if (storefront && sfGet && sfCrawl) {
4185
4941
  const sfOut = storefront.outDir;
@@ -4192,22 +4948,34 @@ ${errors.map((e) => ` - ${e}`).join("\n")}`);
4192
4948
  throw new Error(`pd build: the storefront changed while it was being written:
4193
4949
  ${sf.failures.map((f) => ` - ${f}`).join("\n")}`);
4194
4950
  }
4195
- await writeFile5(join17(sfOut, ".nojekyll"), "");
4951
+ await writeFile5(join19(sfOut, ".nojekyll"), "");
4196
4952
  counts.files++;
4197
4953
  const stubs = new Set((await (await sfGet(PD_ROUTES.pages)).json()).pages.flatMap((p) => p.redirectFrom ?? []));
4198
4954
  const pages = sfCrawl.urls.filter((u) => !/^\/(?:_pd|assets|preview)\//.test(u) && !sfFiles.includes(u) && !stubs.has(u)).length;
4199
- report.storefront = { outDir: sfOut, pages, files: counts.files, bytes: counts.bytes };
4955
+ report.storefront = { outDir: sfOut, basePath: sfBase, pages, files: counts.files, bytes: counts.bytes };
4956
+ if (sfBase !== "/") {
4957
+ const wbPath = sameHostWorkbenchPath(storefront);
4958
+ const rules = storefront.index ? [
4959
+ `Disallow: ${sfBase}preview/`,
4960
+ ...wbPath !== void 0 ? [`Disallow: ${wbPath}preview/`] : [],
4961
+ ...storefront.url !== void 0 ? [`Sitemap: ${storefront.url}sitemap.xml`] : []
4962
+ ] : [`Disallow: ${sfBase}`];
4963
+ report.notes.push(`robots.txt is not written: the site is published under ${sfBase}, and crawlers read robots.txt only at a host's root. Where you can, add to the host's own robots.txt: ${rules.join("; ")}`);
4964
+ }
4200
4965
  if (storefront.url === void 0) {
4201
4966
  report.notes.push(`storefront.url is not set, so pages have no canonical URL, og:url, og:image or JSON-LD, and there is no sitemap: set storefront.url to the site's address, such as "https://example.com/"`);
4202
4967
  }
4203
4968
  report.notes.push(...storefrontNotes, ...externalNotes);
4204
4969
  if (linkSummary) report.links = linkSummary;
4205
4970
  if (a11ySummary) report.accessibility = a11ySummary;
4971
+ if (imageSummary) report.images = imageSummary;
4972
+ const drafts = (await readPages(storefront.pagesDir, { drafts: "leave-out" })).drafts;
4973
+ if (drafts.length) report.drafts = drafts;
4206
4974
  log(`\u2026 storefront: ${pages} pages, ${counts.files} files`);
4207
4975
  }
4208
4976
  const used = /* @__PURE__ */ new Set();
4209
4977
  for (const root2 of [outDir, ...storefront ? [storefront.outDir] : []]) {
4210
- const files = await readdir7(join17(root2, "_pd", "images"), { recursive: true }).catch(() => []);
4978
+ const files = await readdir8(join19(root2, "_pd", "images"), { recursive: true }).catch(() => []);
4211
4979
  for (const f of files) {
4212
4980
  const m = /\.([0-9a-f]{10})\.[1-9][0-9]*\.webp$/.exec(String(f));
4213
4981
  if (m) used.add(m[1]);
@@ -4222,19 +4990,19 @@ ${sf.failures.map((f) => ` - ${f}`).join("\n")}`);
4222
4990
  }
4223
4991
 
4224
4992
  // packages/dev-server/src/requires.ts
4225
- import { readFile as readFile11 } from "node:fs/promises";
4993
+ import { readFile as readFile12 } from "node:fs/promises";
4226
4994
  import { existsSync as existsSync4 } from "node:fs";
4227
- import { basename as basename5, join as join18, relative as relative10 } from "node:path";
4995
+ import { basename as basename5, join as join20, relative as relative11 } from "node:path";
4228
4996
  function normalizeScriptId(raw) {
4229
4997
  let s = raw.trim().replace(/^\[(.+)\]$/, "$1").trim();
4230
4998
  s = basename5(s);
4231
4999
  s = s.replace(/\.(ts|js)$/, "");
4232
5000
  return s;
4233
5001
  }
4234
- function parseFrontmatterList(md, key) {
5002
+ function parseFrontmatterList(md2, key) {
4235
5003
  const values = [];
4236
5004
  const re = new RegExp(`^${key}:\\s*(.+)$`, "mg");
4237
- for (const [, val] of md.matchAll(re)) {
5005
+ for (const [, val] of md2.matchAll(re)) {
4238
5006
  values.push(val.trim());
4239
5007
  }
4240
5008
  return values;
@@ -4247,9 +5015,9 @@ async function aggregateRequires(componentId, patternsDir, flatNameToPath, disco
4247
5015
  let ownAssets = [];
4248
5016
  if (entry?.mdPath) {
4249
5017
  try {
4250
- const md = await readFile11(entry.mdPath, "utf-8");
4251
- ownScripts = parseFrontmatterList(md, "requireScript").map(normalizeScriptId);
4252
- ownAssets = parseFrontmatterList(md, "requireAssets").map((v) => v.trim().replace(/^\[(.+)\]$/, "$1").trim());
5018
+ const md2 = await readFile12(entry.mdPath, "utf-8");
5019
+ ownScripts = parseFrontmatterList(md2, "requireScript").map(normalizeScriptId);
5020
+ ownAssets = parseFrontmatterList(md2, "requireAssets").map((v) => v.trim().replace(/^\[(.+)\]$/, "$1").trim());
4253
5021
  } catch {
4254
5022
  }
4255
5023
  }
@@ -4258,7 +5026,7 @@ async function aggregateRequires(componentId, patternsDir, flatNameToPath, disco
4258
5026
  const hbsPath = entry?.hbsPath ?? "";
4259
5027
  let hbs = "";
4260
5028
  try {
4261
- hbs = hbsPath ? await readFile11(hbsPath, "utf-8") : "";
5029
+ hbs = hbsPath ? await readFile12(hbsPath, "utf-8") : "";
4262
5030
  } catch {
4263
5031
  }
4264
5032
  for (const flatName of parsePartialsFromHbs(hbs)) {
@@ -4279,7 +5047,7 @@ async function loadSvgAssets(ids, assetsDir) {
4279
5047
  for (const id of ids) {
4280
5048
  for (const candidate of [`${id}.svg`, `images/${id}.svg`]) {
4281
5049
  try {
4282
- const raw = await readFile11(join18(assetsDir, candidate), "utf-8");
5050
+ const raw = await readFile12(join20(assetsDir, candidate), "utf-8");
4283
5051
  contents.push(hideSvg(raw));
4284
5052
  break;
4285
5053
  } catch {
@@ -4292,21 +5060,21 @@ function hideSvg(svg) {
4292
5060
  return svg.replace(/<svg(\s|>)/, '<svg$1style="display:none;position:absolute" aria-hidden="true" ');
4293
5061
  }
4294
5062
  async function reportUnresolvableScripts(discoveryMap, scriptsDirs, globalScripts, cwd) {
4295
- const resolves = (id) => scriptsDirs.some((d) => existsSync4(join18(d, `${id}.ts`)));
4296
- const where = scriptsDirs.length ? scriptsDirs.map((d) => relative10(cwd, d) || d).join(" or ") : "no scriptsDir configured";
5063
+ const resolves = (id) => scriptsDirs.some((d) => existsSync4(join20(d, `${id}.ts`)));
5064
+ const where = scriptsDirs.length ? scriptsDirs.map((d) => relative11(cwd, d) || d).join(" or ") : "no scriptsDir configured";
4297
5065
  for (const id of globalScripts) {
4298
5066
  if (id.includes("://") || resolves(normalizeScriptId(id))) continue;
4299
5067
  console.warn(`[dev-server] globalScripts "${id}": no ${normalizeScriptId(id)}.ts in ${where}`);
4300
5068
  }
4301
5069
  for (const [componentId, entry] of discoveryMap) {
4302
5070
  if (!entry.mdPath) continue;
4303
- let md;
5071
+ let md2;
4304
5072
  try {
4305
- md = await readFile11(entry.mdPath, "utf-8");
5073
+ md2 = await readFile12(entry.mdPath, "utf-8");
4306
5074
  } catch {
4307
5075
  continue;
4308
5076
  }
4309
- for (const raw of parseFrontmatterList(md, "requireScript")) {
5077
+ for (const raw of parseFrontmatterList(md2, "requireScript")) {
4310
5078
  const id = normalizeScriptId(raw);
4311
5079
  if (resolves(id)) continue;
4312
5080
  console.warn(`[dev-server] requireScript "${raw}" on ${componentId}: no ${id}.ts in ${where}`);
@@ -4423,6 +5191,7 @@ function createServerState() {
4423
5191
  baseProvenance: null,
4424
5192
  perComponentCss: /* @__PURE__ */ new Map(),
4425
5193
  baseCss: "",
5194
+ entryBaseRules: /* @__PURE__ */ new Map(),
4426
5195
  baseCssVersion: 0,
4427
5196
  discovery: createLazy("discovery", { slow: { afterMs: DISCOVERY_SLOW_MS, what: "the component index" } }),
4428
5197
  audit: createLazy("audit"),
@@ -4453,6 +5222,7 @@ function applyStyleData(state, data) {
4453
5222
  state.baseProvenance = data.baseProvenance;
4454
5223
  state.perComponentCss = data.componentCssMap;
4455
5224
  state.baseCss = data.baseCss;
5225
+ state.entryBaseRules = new Map(data.baseRulesByEntry);
4456
5226
  return { changedComponents: changedComponents.sort(), baseChanged };
4457
5227
  }
4458
5228
  function scriptFor(state, scriptId) {
@@ -4498,24 +5268,24 @@ function cachedDiscovery(state) {
4498
5268
  }
4499
5269
 
4500
5270
  // packages/dev-server/src/routes.ts
4501
- import { basename as basename9, extname as extname2, join as join24, relative as relative12 } from "node:path";
5271
+ import { basename as basename9, extname as extname2, join as join25, relative as relative12 } from "node:path";
4502
5272
  import { readFile as readFile24 } from "node:fs/promises";
4503
5273
 
4504
5274
  // packages/dev-server/src/component-tree.ts
4505
- import { readFile as readFile14 } from "node:fs/promises";
4506
- import { join as join21 } from "node:path";
5275
+ import { readFile as readFile15 } from "node:fs/promises";
5276
+ import { join as join23 } from "node:path";
4507
5277
 
4508
5278
  // packages/dev-server/src/layout-mode.ts
4509
- import { readFile as readFile13 } from "node:fs/promises";
4510
- import { join as join20 } from "node:path";
5279
+ import { readFile as readFile14 } from "node:fs/promises";
5280
+ import { join as join22 } from "node:path";
4511
5281
 
4512
5282
  // packages/dev-server/src/pattern-dirs.ts
4513
- import { readFile as readFile12 } from "node:fs/promises";
4514
- import { join as join19 } from "node:path";
5283
+ import { readFile as readFile13 } from "node:fs/promises";
5284
+ import { join as join21 } from "node:path";
4515
5285
  async function readFolderFile(dirs, rel) {
4516
5286
  for (const dir of typeof dirs === "string" ? [dirs] : dirs) {
4517
5287
  try {
4518
- return await readFile12(join19(dir, rel), "utf-8");
5288
+ return await readFile13(join21(dir, rel), "utf-8");
4519
5289
  } catch {
4520
5290
  }
4521
5291
  }
@@ -4528,7 +5298,7 @@ async function resolveLayoutMode(componentId, patternsDir, discoveryMap) {
4528
5298
  const parts = componentId.split("/");
4529
5299
  const category = parts[0];
4530
5300
  let layout = FULL_CATEGORIES.has(stripOrderPrefixes(category)) ? "full" : "centered";
4531
- const categoryIndex = await readFolderFile(patternsDir, join20(category, "index.md"));
5301
+ const categoryIndex = await readFolderFile(patternsDir, join22(category, "index.md"));
4532
5302
  if (categoryIndex !== null) {
4533
5303
  const m = categoryIndex.match(/^preview:\s*(centered|full)$/m);
4534
5304
  if (m) layout = m[1];
@@ -4536,8 +5306,8 @@ async function resolveLayoutMode(componentId, patternsDir, discoveryMap) {
4536
5306
  const mdPath = discoveryMap?.get(componentId)?.mdPath;
4537
5307
  if (mdPath) {
4538
5308
  try {
4539
- const md = await readFile13(mdPath, "utf-8");
4540
- const m = md.match(/^preview:\s*(centered|full)$/m);
5309
+ const md2 = await readFile14(mdPath, "utf-8");
5310
+ const m = md2.match(/^preview:\s*(centered|full)$/m);
4541
5311
  if (m) layout = m[1];
4542
5312
  } catch {
4543
5313
  }
@@ -4545,130 +5315,6 @@ async function resolveLayoutMode(componentId, patternsDir, discoveryMap) {
4545
5315
  return layout;
4546
5316
  }
4547
5317
 
4548
- // packages/dev-server/src/frontmatter.ts
4549
- var BOM = "\uFEFF";
4550
- var unquote = (s) => {
4551
- const t = s.trim();
4552
- return t.length >= 2 && (t[0] === '"' || t[0] === "'") && t.at(-1) === t[0] ? t.slice(1, -1) : t;
4553
- };
4554
- var unterminatedQuote = (s) => {
4555
- const t = s.trim();
4556
- return (t[0] === '"' || t[0] === "'") && (t.length < 2 || t.at(-1) !== t[0]);
4557
- };
4558
- function block(md) {
4559
- const text = md.startsWith(BOM) ? md.slice(1) : md;
4560
- const lines = text.split(/\r?\n/);
4561
- if (lines[0]?.trim() !== "---") return { text, lines, end: null };
4562
- return { text, lines, end: lines.findIndex((l, i) => i > 0 && l.trim() === "---") };
4563
- }
4564
- function scan(md) {
4565
- const errors = [];
4566
- const err = (line, message) => {
4567
- errors.push({ line, message });
4568
- };
4569
- const { text, lines, end: blockEnd } = block(md);
4570
- if (blockEnd === null) {
4571
- if (/^---\r(?!\n)/.test(text)) err(1, "lines end in a bare CR \u2014 use LF or CRLF line endings");
4572
- return { values: {}, errors, lines: {} };
4573
- }
4574
- const end = blockEnd;
4575
- if (end < 0) {
4576
- err(1, "the frontmatter block opened here is never closed \u2014 end it with a line holding only ---");
4577
- return { values: {}, errors, lines: {} };
4578
- }
4579
- const out = {};
4580
- const keyLines = {};
4581
- let listKey = null;
4582
- for (let i = 1; i < end; i++) {
4583
- const line = lines[i];
4584
- const n = i + 1;
4585
- const bareCr = line.includes("\r");
4586
- if (bareCr) err(n, "line ends in a bare CR \u2014 use LF or CRLF line endings");
4587
- const lineErr = (message) => {
4588
- if (!bareCr) err(n, message);
4589
- };
4590
- const item = line.match(/^\s*-\s+(.*)$/);
4591
- if (item && listKey) {
4592
- if (unterminatedQuote(item[1])) lineErr(`list item ${item[1].trim()} has an opening quote with no closing one`);
4593
- out[listKey].push(unquote(item[1]));
4594
- continue;
4595
- }
4596
- const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
4597
- if (!kv) {
4598
- if (line.trim() !== "") {
4599
- if (item) lineErr("list item with no key above it \u2014 a list follows a line `key:`");
4600
- else if (/^\s+[A-Za-z_][\w-]*:/.test(line)) lineErr("nested value \u2014 only top-level keys are supported, written at the start of the line");
4601
- else lineErr(`cannot read this line: ${line.trim()}`);
4602
- }
4603
- listKey = null;
4604
- continue;
4605
- }
4606
- const [, key, raw] = kv;
4607
- if (key in out) lineErr(`duplicate key "${key}"`);
4608
- keyLines[key] = n;
4609
- const value = raw.trim();
4610
- if (value === "") {
4611
- out[key] = [];
4612
- listKey = key;
4613
- } else if (value.startsWith("[") && value.endsWith("]")) {
4614
- const items = value.slice(1, -1).split(",");
4615
- for (const it of items) if (unterminatedQuote(it)) lineErr(`list item ${it.trim()} has an opening quote with no closing one`);
4616
- out[key] = items.map(unquote).filter(Boolean);
4617
- listKey = null;
4618
- } else {
4619
- if (value.startsWith("[")) lineErr(`"${key}": the list opened with [ is never closed with ]`);
4620
- else if (unterminatedQuote(value)) lineErr(`"${key}": the value has an opening quote with no closing one`);
4621
- out[key] = unquote(value);
4622
- listKey = null;
4623
- }
4624
- }
4625
- return { values: out, errors, lines: keyLines };
4626
- }
4627
- function readFrontmatter(md) {
4628
- return scan(md).values;
4629
- }
4630
- function readFrontmatterStrict(md) {
4631
- return scan(md);
4632
- }
4633
- function frontmatterBodyOffset(md) {
4634
- const { end } = block(md);
4635
- return end === null || end < 0 ? 0 : end + 1;
4636
- }
4637
- function frontmatterBody(md) {
4638
- const { text, lines, end } = block(md);
4639
- if (end === null) return text;
4640
- if (end < 0) return "";
4641
- return lines.slice(end + 1).join("\n");
4642
- }
4643
-
4644
- // packages/dev-server/src/tree-order.ts
4645
- function parseOrder(value) {
4646
- if (value === void 0) return void 0;
4647
- if (Array.isArray(value)) return { list: value };
4648
- if (/^-?\d+(\.\d+)?$/.test(value)) return { weight: Number(value) };
4649
- const list = value.split(",").map((s) => s.trim()).filter(Boolean);
4650
- return list.length ? { list } : void 0;
4651
- }
4652
- var collator = new Intl.Collator("en", { numeric: true });
4653
- var REST = "...";
4654
- function orderSiblings(items, list) {
4655
- const position = /* @__PURE__ */ new Map();
4656
- list?.forEach((name, i) => {
4657
- if (!position.has(name)) position.set(name, i);
4658
- });
4659
- const restPosition = position.get(REST) ?? Infinity;
4660
- const positionOf2 = (item) => list ? position.get(item.segment) ?? restPosition : 0;
4661
- return [...items].sort((a, b) => {
4662
- const ra = a.weight ?? 0;
4663
- const rb = b.weight ?? 0;
4664
- if (ra !== rb) return ra - rb;
4665
- const pa = positionOf2(a);
4666
- const pb = positionOf2(b);
4667
- if (pa !== pb) return pa - pb;
4668
- return collator.compare(a.segment, b.segment) || (a.segment < b.segment ? -1 : a.segment > b.segment ? 1 : 0);
4669
- });
4670
- }
4671
-
4672
5318
  // packages/dev-server/src/component-tree.ts
4673
5319
  function directChildPaths(parentPath, allIds) {
4674
5320
  const prefix = `${parentPath}/`;
@@ -4686,9 +5332,9 @@ function folderTitle(segment) {
4686
5332
  const text = words.join(" ");
4687
5333
  return text.charAt(0).toUpperCase() + text.slice(1);
4688
5334
  }
4689
- var readText = (path) => readFile14(path, "utf-8").catch(() => null);
4690
- function orderOf(md) {
4691
- return md === null ? void 0 : parseOrder(readFrontmatter(md)["order"]);
5335
+ var readText = (path) => readFile15(path, "utf-8").catch(() => null);
5336
+ function orderOf(md2) {
5337
+ return md2 === null ? void 0 : parseOrder(readFrontmatter(md2)["order"]);
4692
5338
  }
4693
5339
  function hasPublicComponent(node) {
4694
5340
  if (node.kind === "component" && node.visibility !== "private") return true;
@@ -4722,22 +5368,22 @@ async function buildTreeNodeFromMap(nodePath, idsUnderPath, patternsDir, overvie
4722
5368
  let visibility;
4723
5369
  let weight;
4724
5370
  if (isComponent && entry.mdPath) {
4725
- const md = await readText(entry.mdPath);
4726
- if (md !== null) {
4727
- const t = md.match(/^title:\s*(.+)$/m);
4728
- const s = md.match(/^status:\s*(.+)$/m);
4729
- const v = md.match(/^visibility:\s*(public|private)$/m);
5371
+ const md2 = await readText(entry.mdPath);
5372
+ if (md2 !== null) {
5373
+ const t = md2.match(/^title:\s*(.+)$/m);
5374
+ const s = md2.match(/^status:\s*(.+)$/m);
5375
+ const v = md2.match(/^visibility:\s*(public|private)$/m);
4730
5376
  if (t) title = t[1].trim();
4731
5377
  if (s) {
4732
5378
  const sv = s[1].trim();
4733
5379
  if (sv === "stable" || sv === "deprecated" || sv === "draft") status = sv;
4734
5380
  }
4735
5381
  if (v) visibility = v[1];
4736
- const o = orderOf(md);
5382
+ const o = orderOf(md2);
4737
5383
  if (o && "weight" in o) weight = o.weight;
4738
5384
  }
4739
5385
  }
4740
- const indexMd = await readFolderFile(patternsDir, join21(nodePath, "index.md"));
5386
+ const indexMd = await readFolderFile(patternsDir, join23(nodePath, "index.md"));
4741
5387
  const folderOrder = orderOf(indexMd);
4742
5388
  const built = [];
4743
5389
  for (const childPath of directChildPaths(nodePath, idsUnderPath)) {
@@ -4787,10 +5433,10 @@ import { readFile as readFile21 } from "node:fs/promises";
4787
5433
  // packages/dev-server/src/markdown.ts
4788
5434
  import { readFile as readFile19 } from "node:fs/promises";
4789
5435
  import { basename as basename8 } from "node:path";
4790
- import MarkdownIt3 from "markdown-it";
5436
+ import MarkdownIt4 from "markdown-it";
4791
5437
 
4792
5438
  // packages/dev-server/src/placed-tags.ts
4793
- import MarkdownIt from "markdown-it";
5439
+ import MarkdownIt2 from "markdown-it";
4794
5440
  var OPEN = /^<(pd-[a-z0-9-]+(?:~[a-z0-9-]+)?)/;
4795
5441
  var CLOSE = /^<\/(pd-[a-z0-9-~]+)\s*>/;
4796
5442
  var ATTR = /^\s+([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/;
@@ -4874,9 +5520,9 @@ function placedTagRule(state, startLine, endLine, silent) {
4874
5520
  state.line = Math.min(line + 1, endLine);
4875
5521
  return true;
4876
5522
  }
4877
- function usePlacedTags(md) {
4878
- md.block.ruler.before("paragraph", "pd_component", placedTagRule, { alt: ["paragraph", "blockquote", "list"] });
4879
- return md;
5523
+ function usePlacedTags(md2) {
5524
+ md2.block.ruler.before("paragraph", "pd_component", placedTagRule, { alt: ["paragraph", "blockquote", "list"] });
5525
+ return md2;
4880
5526
  }
4881
5527
  var camel = (s) => s.replace(/-([a-z0-9])/g, (_, c3) => c3.toUpperCase());
4882
5528
  function dedent(s) {
@@ -4942,10 +5588,10 @@ async function toPlaced(tag, resolver, problems) {
4942
5588
  // packages/dev-server/src/storefront/placed.ts
4943
5589
  import { readFile as readFile18 } from "node:fs/promises";
4944
5590
  import { basename as basename7 } from "node:path";
4945
- import MarkdownIt2 from "markdown-it";
5591
+ import MarkdownIt3 from "markdown-it";
4946
5592
 
4947
5593
  // packages/dev-server/src/storefront/declared-values.ts
4948
- import { readFile as readFile15 } from "node:fs/promises";
5594
+ import { readFile as readFile16 } from "node:fs/promises";
4949
5595
  var NUMERIC = /^-?\d+(\.\d+)?$/;
4950
5596
  var quote = (v) => Array.isArray(v) ? `[${v.join(", ")}]` : `"${v}"`;
4951
5597
  var describe = (v, mode) => mode === "frontmatter" ? quote(v) : JSON.stringify(v);
@@ -5039,7 +5685,7 @@ function jsonValue(prop, value, path, owner, problems) {
5039
5685
  async function readSchema(schemaPath) {
5040
5686
  if (!schemaPath) return {};
5041
5687
  try {
5042
- return { schema: JSON.parse(await readFile15(schemaPath, "utf8")) };
5688
+ return { schema: JSON.parse(await readFile16(schemaPath, "utf8")) };
5043
5689
  } catch (e) {
5044
5690
  return { error: e instanceof Error ? e.message : String(e) };
5045
5691
  }
@@ -5130,209 +5776,12 @@ async function placedValue(prop, node, where, ctx, problems) {
5130
5776
  import { readFile as readFile17 } from "node:fs/promises";
5131
5777
  import { basename as basename6 } from "node:path";
5132
5778
 
5133
- // packages/dev-server/src/storefront/pages.ts
5134
- import { readdir as readdir8, readFile as readFile16, stat as stat5 } from "node:fs/promises";
5135
- import { join as join22, relative as relative11, sep as sep7 } from "node:path";
5136
-
5137
- // packages/dev-server/src/storefront/redirects.ts
5138
- function redirectsOf(f) {
5139
- const v = f.frontmatter.values["redirectFrom"];
5140
- return Array.isArray(v) ? v : typeof v === "string" ? [v] : [];
5141
- }
5142
- var RESERVED = [PD_ROUTE_PREFIX.preview, "/_pd/", PD_ROUTE_PREFIX.assets, "/api/", PD_ROUTE_PREFIX.source];
5143
- function redirectProblems(files, workbenchPath) {
5144
- const problems = [];
5145
- const claimed = /* @__PURE__ */ new Map();
5146
- for (const f of files.values()) {
5147
- const at = `${f.path}${f.frontmatter.lines["redirectFrom"] !== void 0 ? `:${f.frontmatter.lines["redirectFrom"]}` : ""}`;
5148
- for (const old of redirectsOf(f)) {
5149
- if (typeof old !== "string" || !old.startsWith("/") || !old.endsWith("/")) continue;
5150
- const page = files.get(old);
5151
- if (page) {
5152
- problems.push(`${at}: redirectFrom "${old}" is the page ${page.path}: a route can't both be a page and forward to one`);
5153
- continue;
5154
- }
5155
- const reserved = RESERVED.find((r) => old.startsWith(r));
5156
- if (reserved) {
5157
- problems.push(`${at}: redirectFrom "${old}" is under ${reserved}, which Pattern Dump serves itself`);
5158
- continue;
5159
- }
5160
- if (workbenchPath !== void 0 && old.startsWith(workbenchPath)) {
5161
- problems.push(`${at}: redirectFrom "${old}" is ${workbenchPlace(old, workbenchPath)}`);
5162
- continue;
5163
- }
5164
- const other = claimed.get(old);
5165
- if (other && other !== f) {
5166
- problems.push(`${at}: redirectFrom "${old}" is also claimed by ${other.path}: one old route forwards to one page`);
5167
- continue;
5168
- }
5169
- claimed.set(old, f);
5170
- }
5171
- }
5172
- return problems;
5173
- }
5174
- function workbenchPlace(route, workbenchPath) {
5175
- return `${route === workbenchPath ? "at" : "under"} ${workbenchPath}, where the workbench is published on this host (storefront.workbenchUrl), so the workbench would overwrite it there`;
5176
- }
5177
- function redirectStub(p) {
5178
- const target = escapeAttr(p.route);
5179
- const canonical2 = p.siteUrl !== void 0 ? `
5180
- <link rel="canonical" href="${escapeAttr(new URL(p.route.slice(1), p.siteUrl).href)}">` : "";
5181
- return `<!DOCTYPE html>
5182
- <html lang="${escapeAttr(p.lang)}">
5183
- <head>
5184
- <meta charset="UTF-8">
5185
- <title>Moved to ${escapeHtml(p.title)}</title>${canonical2}
5186
- <meta name="robots" content="noindex">
5187
- <meta http-equiv="refresh" content="0; url=${target}">
5188
- </head>
5189
- <body>
5190
- <p>This page has moved to <a href="${target}">${escapeHtml(p.title)}</a>.</p>
5191
- </body>
5192
- </html>
5193
- `;
5194
- }
5195
-
5196
- // packages/dev-server/src/storefront/pages.ts
5197
- var DEFAULT_LAYOUT = "page";
5198
- var NOT_FOUND_FILE = "404.md";
5199
- function routeFor(path) {
5200
- const parts = path.replace(/\.md$/, "").split("/");
5201
- if (parts.at(-1) === "index") parts.pop();
5202
- return parts.length ? `/${parts.join("/")}/` : "/";
5203
- }
5204
- var NAME = /^[a-z0-9-]+$/;
5205
- async function markdownFiles(dir, root, problems) {
5206
- const out = [];
5207
- const entries = (await readdir8(dir, { withFileTypes: true }).catch(() => [])).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
5208
- for (const e of entries) {
5209
- if (e.name.startsWith(".")) continue;
5210
- const p = join22(dir, e.name);
5211
- const rel = relative11(root, p).split(sep7).join("/");
5212
- if (e.isDirectory()) {
5213
- if (!NAME.test(e.name)) {
5214
- problems.push(`${rel}/: a folder name is part of the URL, so it is lowercase letters, digits and hyphens only`);
5215
- continue;
5216
- }
5217
- out.push(...await markdownFiles(p, root, problems));
5218
- } else if (!e.name.endsWith(".md")) {
5219
- problems.push(`${rel} is not a page: only .md files belong in the pages folder (images and other files go in the project's assets)`);
5220
- } else if (!NAME.test(e.name.slice(0, -".md".length))) {
5221
- problems.push(`${rel}: a page's file name is part of the URL, so it is lowercase letters, digits and hyphens only, then .md`);
5222
- } else {
5223
- out.push(rel);
5224
- }
5225
- }
5226
- return out;
5227
- }
5228
- var scalar = (v) => typeof v === "string" ? v : void 0;
5229
- async function readPages(pagesDir, opts = {}) {
5230
- const problems = [];
5231
- const files = /* @__PURE__ */ new Map();
5232
- let notFound;
5233
- if (!await stat5(pagesDir).then((s) => s.isDirectory(), () => false)) {
5234
- return { pages: [], nav: [], files, problems: [`the pages folder ${pagesDir} does not exist`] };
5235
- }
5236
- for (const path of (await markdownFiles(pagesDir, pagesDir, problems)).sort()) {
5237
- const route = routeFor(path);
5238
- const other = files.get(route);
5239
- if (other) {
5240
- problems.push(`${other.path} and ${path} are both the page at ${route}`);
5241
- continue;
5242
- }
5243
- const file = join22(pagesDir, ...path.split("/"));
5244
- const text = await readFile16(file, "utf8").catch((e) => {
5245
- if (e.code === "ENOENT") return null;
5246
- throw e;
5247
- });
5248
- if (text === null) continue;
5249
- if (path === NOT_FOUND_FILE) {
5250
- notFound = { route: PD_ROUTES.notFound, file, path, frontmatter: readFrontmatterStrict(text) };
5251
- continue;
5252
- }
5253
- files.set(route, { route, file, path, frontmatter: readFrontmatterStrict(text) });
5254
- }
5255
- const pageOf = (f) => {
5256
- const redirectFrom = redirectsOf(f);
5257
- return {
5258
- route: f.route,
5259
- title: scalar(f.frontmatter.values["title"]) ?? f.route,
5260
- layout: scalar(f.frontmatter.values["layout"]) ?? DEFAULT_LAYOUT,
5261
- ...redirectFrom.length ? { redirectFrom } : {}
5262
- };
5263
- };
5264
- const entryOf = (f) => {
5265
- const description = scalar(f.frontmatter.values["description"]);
5266
- return { route: f.route, title: pageOf(f).title, ...description !== void 0 ? { description } : {} };
5267
- };
5268
- const weightOf = (f) => {
5269
- const o = parseOrder(f?.frontmatter.values["order"]);
5270
- return o && "weight" in o ? o.weight : void 0;
5271
- };
5272
- const listOf = (f) => {
5273
- const o = parseOrder(f?.frontmatter.values["order"]);
5274
- return o && "list" in o ? o.list : void 0;
5275
- };
5276
- const folders = /* @__PURE__ */ new Set();
5277
- for (const f of files.values()) {
5278
- const parts = f.path.split("/").slice(0, -1);
5279
- for (let i = 1; i <= parts.length; i++) folders.add(`/${parts.slice(0, i).join("/")}/`);
5280
- }
5281
- if (!files.has("/")) problems.push("there is no home page: the pages folder needs an index.md at its top, the page at /");
5282
- for (const folder of [...folders].sort()) {
5283
- if (!files.has(folder)) problems.push(`the folder ${folder.slice(1)} holds pages but has no index.md \u2014 it needs one, as the folder's own page`);
5284
- }
5285
- const childrenOf = (folder) => {
5286
- const depth = folder === "/" ? 0 : folder.split("/").length - 2;
5287
- const out = /* @__PURE__ */ new Map();
5288
- for (const route of [...files.keys(), ...folders]) {
5289
- if (route === folder || !route.startsWith(folder)) continue;
5290
- const segs = route.split("/").filter(Boolean);
5291
- if (segs.length !== depth + 1) continue;
5292
- out.set(route, segs[depth]);
5293
- }
5294
- return [...out].map(([route, segment]) => ({ route, segment }));
5295
- };
5296
- const ordered = (folder, extra = []) => {
5297
- const items = childrenOf(folder).map((c3) => {
5298
- const w = weightOf(files.get(c3.route));
5299
- return { route: c3.route, segment: c3.segment, ...w !== void 0 ? { weight: w } : {} };
5300
- });
5301
- return orderSiblings([...extra, ...items], listOf(files.get(folder)));
5302
- };
5303
- const home = files.get("/");
5304
- const homeWeight = weightOf(home);
5305
- const top = ordered("/", home ? [{ route: "/", segment: "index", ...homeWeight !== void 0 ? { weight: homeWeight } : {} }] : []);
5306
- const nav = [];
5307
- for (const item of top) {
5308
- const page = files.get(item.route);
5309
- if (!page) continue;
5310
- const children = folders.has(item.route) && item.route !== "/" ? ordered(item.route).flatMap((c3) => {
5311
- const p = files.get(c3.route);
5312
- return p ? [entryOf(p)] : [];
5313
- }) : [];
5314
- nav.push({ ...entryOf(page), ...children.length ? { children } : {} });
5315
- }
5316
- const inNav = nav.flatMap((n) => [n.route, ...(n.children ?? []).map((c3) => c3.route)]);
5317
- const rest = [...files.keys()].filter((r) => !inNav.includes(r)).sort();
5318
- const routes = [...inNav, ...rest];
5319
- const pages = routes.map((r) => pageOf(files.get(r)));
5320
- const { workbenchPath } = opts;
5321
- if (workbenchPath !== void 0) {
5322
- for (const f of files.values()) {
5323
- if (f.route.startsWith(workbenchPath)) problems.push(`${f.path}: the page ${f.route} is ${workbenchPlace(f.route, workbenchPath)}`);
5324
- }
5325
- }
5326
- problems.push(...redirectProblems(files, workbenchPath));
5327
- return { pages, nav, files: new Map(routes.map((r) => [r, files.get(r)])), problems, ...notFound ? { notFound } : {} };
5328
- }
5329
-
5330
5779
  // packages/dev-server/src/storefront/frontmatter-contract.ts
5331
- var RESERVED_KEYS = ["layout", "order", "redirectFrom", "index", "image"];
5780
+ var RESERVED_KEYS = ["layout", "order", "redirectFrom", "index", "image", "date", "tags", "draft", "sort"];
5332
5781
  var ASSEMBLY_KEYS = ["body", "nav", "site", "page"];
5333
5782
  var OLD_ROUTE = /^\/(?:(?!\.\.?\/)[^/?#\s]+\/)+$/;
5334
5783
  var ASSET_PATH = /^\/assets\/[^?#]+[^/?#]$/;
5335
- function checkBuildKeys(values, at) {
5784
+ function checkBuildKeys(values, path, at) {
5336
5785
  const out = {};
5337
5786
  const redirect = values["redirectFrom"];
5338
5787
  if (redirect !== void 0) {
@@ -5352,6 +5801,16 @@ function checkBuildKeys(values, at) {
5352
5801
  if (typeof image === "string" && ASSET_PATH.test(image)) out.image = image;
5353
5802
  else at("image", `image must be a path to a file in the assets, such as "/assets/images/share.png"`);
5354
5803
  }
5804
+ const take = (key, r) => {
5805
+ if (r === void 0) return;
5806
+ if (r.ok) out[key] = r.value;
5807
+ else at(key, r.message);
5808
+ };
5809
+ const v = (key) => values[key];
5810
+ take("date", v("date") === void 0 ? void 0 : readDate(v("date")));
5811
+ take("tags", v("tags") === void 0 ? void 0 : readTags(v("tags")));
5812
+ take("draft", v("draft") === void 0 ? void 0 : readDraft(v("draft")));
5813
+ take("sort", v("sort") === void 0 ? void 0 : readSort(v("sort"), path));
5355
5814
  return out;
5356
5815
  }
5357
5816
  function layoutNames(discoveryMap) {
@@ -5374,12 +5833,12 @@ async function checkPageFrontmatter(page, discoveryMap) {
5374
5833
  if (!Array.isArray(rawLayout)) {
5375
5834
  at("layout", known.length ? `layout "${layout}" is not a layout \u2014 the layouts are ${known.map((n) => `"${n}"`).join(", ")}` : `layout "${layout}" is not a layout \u2014 the project has no layouts (components in a layouts/ folder of its patterns)`);
5376
5835
  }
5377
- return { layoutId, values: {}, build: checkBuildKeys(values, at), problems };
5836
+ return { layoutId, values: {}, build: checkBuildKeys(values, page.path, at), problems };
5378
5837
  }
5379
5838
  const read = await readSchema(entry.schemaPath);
5380
5839
  if (read.error !== void 0) {
5381
5840
  problems.push({ file: page.path, message: `the schema of layout "${layout}" cannot be read: ${read.error}` });
5382
- return { layoutId, values: {}, build: checkBuildKeys(values, at), problems };
5841
+ return { layoutId, values: {}, build: checkBuildKeys(values, page.path, at), problems };
5383
5842
  }
5384
5843
  const schema = read.schema ?? {};
5385
5844
  const hasSchema = read.schema !== void 0;
@@ -5389,7 +5848,7 @@ async function checkPageFrontmatter(page, discoveryMap) {
5389
5848
  problems.push({ file: page.path, message: `layout "${layout}" declares "${key}", which a page sets for the build, not for its layout: rename it in the layout's schema` });
5390
5849
  }
5391
5850
  }
5392
- const build = checkBuildKeys(values, at);
5851
+ const build = checkBuildKeys(values, page.path, at);
5393
5852
  const out = {};
5394
5853
  for (const [key, value] of Object.entries(values)) {
5395
5854
  if (RESERVED_KEYS.includes(key)) continue;
@@ -5420,6 +5879,9 @@ async function checkPageFrontmatter(page, discoveryMap) {
5420
5879
 
5421
5880
  // packages/dev-server/src/storefront/derived.ts
5422
5881
  var LINK = { fields: { route: "value", title: "value" } };
5882
+ var ENTRY = {
5883
+ fields: { route: "value", title: "value", description: "value", date: "value", image: "value", tags: "value", draft: "value" }
5884
+ };
5423
5885
  var PAGE_SHAPE = {
5424
5886
  fields: {
5425
5887
  headings: { items: { fields: { level: "value", text: "value", anchor: "value" } } },
@@ -5428,10 +5890,13 @@ var PAGE_SHAPE = {
5428
5890
  prev: LINK,
5429
5891
  next: LINK,
5430
5892
  created: "value",
5431
- modified: "value"
5893
+ modified: "value",
5894
+ children: { items: ENTRY },
5895
+ date: "value",
5896
+ tags: "value"
5432
5897
  }
5433
5898
  };
5434
- var FILLED = ["headings", "section", "breadcrumbs", "prev", "next", "created", "modified"];
5899
+ var FILLED = ["headings", "section", "breadcrumbs", "prev", "next", "created", "modified", "children", "date", "tags"];
5435
5900
  function positionOf(route, site) {
5436
5901
  const titles = new Map(site.pages.map((p) => [p.route, p.title]));
5437
5902
  const link = (r) => titles.has(r) ? { route: r, title: titles.get(r) } : null;
@@ -5479,7 +5944,7 @@ var NAVIGATIONS_KEY = "navigations";
5479
5944
  var AT = "storefront.site.navigations";
5480
5945
  var KEYS = ["page", "href", "label", "children"];
5481
5946
  var isObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5482
- var show = (v) => typeof v === "string" ? `"${v}"` : JSON.stringify(v) ?? String(v);
5947
+ var show2 = (v) => typeof v === "string" ? `"${v}"` : JSON.stringify(v) ?? String(v);
5483
5948
  var isAllowedHref = (href) => /^(https?:\/\/|mailto:)/i.test(href) || /^\/(?!\/)/.test(href);
5484
5949
  function listedNavigations(declared, owner) {
5485
5950
  if (!declared) return { listed: [], problems: [] };
@@ -5502,7 +5967,7 @@ function checkNavigations(value, declared, owner, pagePaths) {
5502
5967
  const problems = [...names.problems];
5503
5968
  const navigations = {};
5504
5969
  if (value !== void 0 && !isObject5(value)) {
5505
- problems.push(`${AT} must be an object of named navigations, such as { footer: [{ page: "about.md" }] } (got ${show(value)})`);
5970
+ problems.push(`${AT} must be an object of named navigations, such as { footer: [{ page: "about.md" }] } (got ${show2(value)})`);
5506
5971
  return { navigations, listed: names.listed, problems };
5507
5972
  }
5508
5973
  const given = value ?? {};
@@ -5513,7 +5978,7 @@ function checkNavigations(value, declared, owner, pagePaths) {
5513
5978
  continue;
5514
5979
  }
5515
5980
  if (!Array.isArray(entries)) {
5516
- problems.push(`${AT}.${name} must be a list of entries (got ${show(entries)})`);
5981
+ problems.push(`${AT}.${name} must be a list of entries (got ${show2(entries)})`);
5517
5982
  continue;
5518
5983
  }
5519
5984
  problems.push(...checkEntries(entries, `${AT}.${name}`, pagePaths));
@@ -5529,7 +5994,7 @@ function checkEntries(entries, at, pagePaths) {
5529
5994
  }
5530
5995
  function checkEntry(entry, at, pagePaths) {
5531
5996
  const FORMS = `{ page }, { href, label } or { label, children }`;
5532
- if (!isObject5(entry)) return [`${at} must be an entry, one of ${FORMS} (got ${show(entry)})`];
5997
+ if (!isObject5(entry)) return [`${at} must be an entry, one of ${FORMS} (got ${show2(entry)})`];
5533
5998
  const problems = [];
5534
5999
  for (const key of Object.keys(entry)) {
5535
6000
  if (KEYS.includes(key)) continue;
@@ -5542,11 +6007,11 @@ function checkEntry(entry, at, pagePaths) {
5542
6007
  return problems;
5543
6008
  }
5544
6009
  const label = entry["label"];
5545
- if (label !== void 0 && typeof label !== "string") problems.push(`${at}.label must be text (got ${show(label)})`);
6010
+ if (label !== void 0 && typeof label !== "string") problems.push(`${at}.label must be text (got ${show2(label)})`);
5546
6011
  const form = forms[0];
5547
6012
  if (form === "page") {
5548
6013
  const page = entry["page"];
5549
- if (typeof page !== "string") problems.push(`${at}.page must be the path of a page in the pages folder, such as "about.md" (got ${show(page)})`);
6014
+ if (typeof page !== "string") problems.push(`${at}.page must be the path of a page in the pages folder, such as "about.md" (got ${show2(page)})`);
5550
6015
  else if (!pagePaths.has(page)) {
5551
6016
  const near = suggest(page, [...pagePaths]);
5552
6017
  problems.push(`${at}: "${page}" is not a page in the pages folder${near ? ` \u2014 did you mean "${near}"?` : ""}`);
@@ -5555,11 +6020,11 @@ function checkEntry(entry, at, pagePaths) {
5555
6020
  if (label === void 0) problems.push(`${at} needs a "label"`);
5556
6021
  if (form === "href") {
5557
6022
  const href = entry["href"];
5558
- if (typeof href !== "string") problems.push(`${at}.href must be text (got ${show(href)})`);
5559
- else if (!isAllowedHref(href)) problems.push(`${at}.href ${show(href)} must start with https:, http: or mailto:, or be a path from the site's root (/\u2026)`);
6023
+ if (typeof href !== "string") problems.push(`${at}.href must be text (got ${show2(href)})`);
6024
+ else if (!isAllowedHref(href)) problems.push(`${at}.href ${show2(href)} must start with https:, http: or mailto:, or be a path from the site's root (/\u2026)`);
5560
6025
  } else {
5561
6026
  const children = entry["children"];
5562
- if (!Array.isArray(children)) problems.push(`${at}.children must be a list of entries (got ${show(children)})`);
6027
+ if (!Array.isArray(children)) problems.push(`${at}.children must be a list of entries (got ${show2(children)})`);
5563
6028
  else problems.push(...checkEntries(children, `${at}.children`, pagePaths));
5564
6029
  }
5565
6030
  }
@@ -5580,10 +6045,86 @@ function resolveNavigations(navigations, listed, model, route) {
5580
6045
  return Object.fromEntries(listed.map((name) => [name, Object.hasOwn(navigations, name) ? resolve15(navigations[name]) : null]));
5581
6046
  }
5582
6047
 
6048
+ // packages/dev-server/src/storefront/collections.ts
6049
+ async function dateOf(route, model, dates, mode) {
6050
+ const own = model.entries.get(route)?.date ?? null;
6051
+ const file = model.files.get(route);
6052
+ if (own !== null || !dates || !file) return { date: own, problems: [], notes: [] };
6053
+ const dated = await dates.of(file.path, mode);
6054
+ return { date: dated.shallow && mode === "build" ? null : dated.dates.created?.slice(0, 10) ?? null, problems: dated.problems, notes: dated.notes };
6055
+ }
6056
+ async function entriesOf(folder, model, o) {
6057
+ const listing = model.folders.get(folder);
6058
+ if (!listing) return { entries: [], problems: [], notes: [] };
6059
+ const titles = new Map(model.pages.map((p) => [p.route, p.title]));
6060
+ const problems = [], notes = [];
6061
+ const entries = [];
6062
+ for (const route of listing.children) {
6063
+ const fields = model.entries.get(route);
6064
+ if (!fields) continue;
6065
+ const dated = await dateOf(route, model, o.dates, o.mode);
6066
+ problems.push(...dated.problems);
6067
+ if (listing.sort === "date") notes.push(...dated.notes);
6068
+ entries.push({ route, title: titles.get(route) ?? route, ...fields, date: dated.date });
6069
+ }
6070
+ if (listing.sort === "date") {
6071
+ const index = model.files.get(folder).path;
6072
+ for (const e of entries.filter((e2) => e2.date === null)) {
6073
+ const text = `${index}: sort: date, but ${model.files.get(e.route).path} has no date: give it one (date: YYYY-MM-DD), or commit it so git dates it`;
6074
+ if (o.mode === "build") problems.push(text);
6075
+ else notes.push(`${text}; it is listed last`);
6076
+ }
6077
+ entries.sort((a, b) => a.date === b.date ? a.route < b.route ? -1 : 1 : a.date === null ? 1 : b.date === null ? -1 : a.date < b.date ? 1 : -1);
6078
+ }
6079
+ return { entries, problems: [...new Set(problems)], notes: [...new Set(notes)] };
6080
+ }
6081
+ var COLLECTIONS_KEY = "collections";
6082
+ var isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6083
+ function checkCollections(declared, owner, folders, leftOut, configured) {
6084
+ const problems = [], notes = [];
6085
+ if (configured && Object.hasOwn(configured, COLLECTIONS_KEY)) {
6086
+ problems.push(`storefront.site.${COLLECTIONS_KEY} is filled by Pattern Dump from the pages folders: list the names in ${owner}'s schema, and don't set it in the config`);
6087
+ }
6088
+ if (!declared) return { listed: [], problems, notes };
6089
+ const names = isObject6(declared["properties"]) ? declared["properties"] : {};
6090
+ const extra = Object.keys(declared).filter((k) => !["type", "description", "title", "properties", "required"].includes(k));
6091
+ if (extra.length || declared["type"] !== void 0 && declared["type"] !== "object") {
6092
+ problems.push(`${owner} declares a shape for site.${COLLECTIONS_KEY}: the shape of an entry is Pattern Dump's, so list the folders' names under properties only`);
6093
+ }
6094
+ const required = Array.isArray(declared["required"]) ? declared["required"].map(String) : [];
6095
+ const listed = [.../* @__PURE__ */ new Set([...Object.keys(names), ...required])];
6096
+ for (const name of listed) {
6097
+ const schema = names[name];
6098
+ const own = Object.keys(isObject6(schema) ? schema : {}).filter((k) => k !== "description" && k !== "title");
6099
+ if (own.length) {
6100
+ problems.push(`${owner} declares ${own.map((k) => `"${k}"`).join(", ")} for site.${COLLECTIONS_KEY}.${name}: the shape of an entry is Pattern Dump's, so list the name only (a description at most)`);
6101
+ }
6102
+ if (!folders.has(name) && !leftOut.includes(`${name}/index.md`)) {
6103
+ const near = suggest(name, [...folders]);
6104
+ const text = `${owner} lists site.${COLLECTIONS_KEY}.${name}, but the pages folder has no folder "${name}" with an index.md`;
6105
+ const hint = near ? ` \u2014 did you mean "${near}"?` : folders.size ? ` (its folders: ${[...folders].map((f) => `"${f}"`).join(", ")})` : "";
6106
+ if (required.includes(name)) problems.push(`${text}, and ${owner} requires it${hint}`);
6107
+ else notes.push(`${text}, so it is an empty list${hint}`);
6108
+ }
6109
+ }
6110
+ return { listed, problems, notes };
6111
+ }
6112
+ async function resolveCollections(listed, model, o) {
6113
+ const collections = {};
6114
+ const problems = [], notes = [];
6115
+ for (const name of listed) {
6116
+ const r = await entriesOf(`/${name}/`, model, o);
6117
+ collections[name] = r.entries;
6118
+ problems.push(...r.problems);
6119
+ notes.push(...r.notes);
6120
+ }
6121
+ return { collections, problems, notes };
6122
+ }
6123
+
5583
6124
  // packages/dev-server/src/storefront/site.ts
5584
6125
  var SITE_SCHEMA_OWNER = "partials/base";
5585
- var none = (problems = []) => ({ values: {}, navigations: {}, listed: [], problems });
5586
- async function checkSite(site, discoveryMap, pagePaths = /* @__PURE__ */ new Set()) {
6126
+ var none = (problems = [], collections = []) => ({ values: {}, navigations: {}, listed: [], collections, problems });
6127
+ async function checkSite(site, discoveryMap, pagePaths = /* @__PURE__ */ new Set(), pages = { folders: /* @__PURE__ */ new Set(), leftOut: [] }) {
5587
6128
  const problems = [];
5588
6129
  const entry = discoveryMap.get(SITE_SCHEMA_OWNER);
5589
6130
  if (!entry) {
@@ -5600,24 +6141,32 @@ async function checkSite(site, discoveryMap, pagePaths = /* @__PURE__ */ new Set
5600
6141
  }
5601
6142
  const navDeclared = declared.properties?.[NAVIGATIONS_KEY];
5602
6143
  const navs = checkNavigations(site?.[NAVIGATIONS_KEY], navDeclared, SITE_SCHEMA_OWNER, pagePaths);
6144
+ const cols = checkCollections(
6145
+ declared.properties?.[COLLECTIONS_KEY],
6146
+ SITE_SCHEMA_OWNER,
6147
+ pages.folders,
6148
+ pages.leftOut,
6149
+ site
6150
+ );
5603
6151
  if (site === void 0) {
5604
6152
  if (schema.required?.includes("site")) problems.push(`storefront.site is required by ${SITE_SCHEMA_OWNER}`);
5605
- problems.push(...navs.problems);
5606
- return { ...none(problems), listed: navs.listed };
6153
+ problems.push(...navs.problems, ...cols.problems);
6154
+ return { ...none(problems, cols.listed), listed: navs.listed, notes: cols.notes };
5607
6155
  }
5608
- const { [NAVIGATIONS_KEY]: _navigations, ...rest } = site;
6156
+ const own = (k) => k !== NAVIGATIONS_KEY && k !== COLLECTIONS_KEY;
6157
+ const rest = Object.fromEntries(Object.entries(site).filter(([k]) => own(k)));
5609
6158
  const restDeclared = {
5610
6159
  ...declared,
5611
- properties: Object.fromEntries(Object.entries(declared.properties ?? {}).filter(([k]) => k !== NAVIGATIONS_KEY)),
5612
- ...declared.required ? { required: declared.required.filter((k) => k !== NAVIGATIONS_KEY) } : {}
6160
+ properties: Object.fromEntries(Object.entries(declared.properties ?? {}).filter(([k]) => own(k))),
6161
+ ...declared.required ? { required: declared.required.filter(own) } : {}
5613
6162
  };
5614
6163
  const checked = checkJsonKeys(restDeclared, rest, "storefront.site", SITE_SCHEMA_OWNER);
5615
- problems.push(...checked.problems, ...navs.problems);
6164
+ problems.push(...checked.problems, ...navs.problems, ...cols.problems);
5616
6165
  if (declared.required?.includes(NAVIGATIONS_KEY) && !Object.hasOwn(site, NAVIGATIONS_KEY)) {
5617
6166
  problems.push(`storefront.site.${NAVIGATIONS_KEY} is required by ${SITE_SCHEMA_OWNER}`);
5618
6167
  }
5619
- if (problems.length) return { ...none(problems), listed: navs.listed };
5620
- return { values: checked.value, navigations: navs.navigations, listed: navs.listed, problems };
6168
+ if (problems.length) return { ...none(problems, cols.listed), listed: navs.listed, notes: cols.notes };
6169
+ return { values: checked.value, navigations: navs.navigations, listed: navs.listed, collections: cols.listed, problems, notes: cols.notes };
5621
6170
  }
5622
6171
 
5623
6172
  // packages/dev-server/src/storefront/sample-data.ts
@@ -5635,7 +6184,7 @@ function includeTree(layoutId, composition) {
5635
6184
  }
5636
6185
  return [layoutId, ...[...seen].filter((id) => id !== layoutId).sort()];
5637
6186
  }
5638
- async function checkSampleData(layoutId, discoveryMap, composition, layoutDeclared, siteDeclared, pageDeclared, navigationNames = []) {
6187
+ async function checkSampleData(layoutId, discoveryMap, composition, layoutDeclared, siteDeclared, pageDeclared, navigationNames = [], collectionNames = []) {
5639
6188
  const problems = [];
5640
6189
  const allowed = new Set([...layoutDeclared, ...ASSEMBLY_KEYS].filter((k) => k !== "page" || pageDeclared !== void 0));
5641
6190
  const shapeFields = PAGE_SHAPE.fields;
@@ -5672,10 +6221,11 @@ async function checkSampleData(layoutId, discoveryMap, composition, layoutDeclar
5672
6221
  for (const [sub, v] of Object.entries(value)) {
5673
6222
  if (!siteDeclared.includes(sub)) {
5674
6223
  problems.push(`${where} sets "site.${sub}", which ${SITE_SCHEMA_OWNER} does not declare for site, so it would show on every page using ${layoutId}`);
5675
- } else if (sub === "navigations" && isPlainObject2(v)) {
6224
+ } else if ((sub === "navigations" || sub === "collections") && isPlainObject2(v)) {
6225
+ const names = sub === "navigations" ? navigationNames : collectionNames;
5676
6226
  for (const name of Object.keys(v)) {
5677
- if (!navigationNames.includes(name)) {
5678
- problems.push(`${where} sets "site.navigations.${name}", which ${SITE_SCHEMA_OWNER} does not list, so it would show on every page using ${layoutId}`);
6227
+ if (!names.includes(name)) {
6228
+ problems.push(`${where} sets "site.${sub}.${name}", which ${SITE_SCHEMA_OWNER} does not list, so it would show on every page using ${layoutId}`);
5679
6229
  }
5680
6230
  }
5681
6231
  }
@@ -5687,7 +6237,7 @@ async function checkSampleData(layoutId, discoveryMap, composition, layoutDeclar
5687
6237
  }
5688
6238
 
5689
6239
  // packages/dev-server/src/storefront/placed.ts
5690
- var contentMd = new MarkdownIt2({ html: false, linkify: false });
6240
+ var contentMd = new MarkdownIt3({ html: false, linkify: false });
5691
6241
  var kebab = (k) => k.replace(/[A-Z]/g, (c3) => `-${c3.toLowerCase()}`);
5692
6242
  async function placementComponents(id, deps) {
5693
6243
  const seen = /* @__PURE__ */ new Set();
@@ -5736,7 +6286,7 @@ async function unsafeOutputs(id, at, deps) {
5736
6286
  }
5737
6287
  return problems;
5738
6288
  }
5739
- var isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6289
+ var isObject7 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5740
6290
  function undeclaredSampleKeys(sample, props, prefix) {
5741
6291
  const out = [];
5742
6292
  for (const [key, value] of Object.entries(sample)) {
@@ -5745,7 +6295,7 @@ function undeclaredSampleKeys(sample, props, prefix) {
5745
6295
  out.push(`${prefix}${key}`);
5746
6296
  continue;
5747
6297
  }
5748
- if (isObject6(value) && prop.properties) out.push(...undeclaredSampleKeys(value, prop.properties, `${prefix}${key}.`));
6298
+ if (isObject7(value) && prop.properties) out.push(...undeclaredSampleKeys(value, prop.properties, `${prefix}${key}.`));
5749
6299
  }
5750
6300
  return out;
5751
6301
  }
@@ -5764,7 +6314,7 @@ async function sampleDataProblems(id, at, deps) {
5764
6314
  } catch {
5765
6315
  continue;
5766
6316
  }
5767
- if (!isObject6(sample)) continue;
6317
+ if (!isObject7(sample)) continue;
5768
6318
  for (const key of undeclaredSampleKeys(sample, declared, "")) {
5769
6319
  problems.push({ at, message: `the sample data of ${template} (${basename7(jsonPath)}) sets "${key}", which ${owner} does not declare, so it would show wherever ${owner} is placed` });
5770
6320
  }
@@ -5784,6 +6334,11 @@ async function schemaProblems(id, at, deps) {
5784
6334
  })) {
5785
6335
  problems.push({ at, message: `${component} cannot be placed: its schema (${deps.shown(entry.schemaPath)}, ${p.path}): ${p.message}` });
5786
6336
  }
6337
+ const rendered = await deps.render({ component }).catch(() => void 0);
6338
+ for (const d of rendered?.meta?.diagnostics ?? []) {
6339
+ if (d.kind !== "SCHEMA_INVALID" || !isHelperNameProblem(d.message)) continue;
6340
+ problems.push({ at, message: `${component} cannot be placed: its schema (${deps.shown(entry.schemaPath)}, ${d.path ?? "#"}): ${d.message}` });
6341
+ }
5787
6342
  }
5788
6343
  return problems;
5789
6344
  }
@@ -5839,23 +6394,23 @@ var Slugger = class {
5839
6394
  // packages/dev-server/src/markdown.ts
5840
6395
  var INLINE = { embed: "inline" };
5841
6396
  function createDocMarkdown(placing) {
5842
- const md = new MarkdownIt3({ html: false, linkify: false });
5843
- md.validateLink = (url) => {
6397
+ const md2 = new MarkdownIt4({ html: false, linkify: false });
6398
+ md2.validateLink = (url) => {
5844
6399
  const t = url.trim();
5845
6400
  return /^(https?|mailto|tel):/i.test(t) || /^[.#/]/.test(t) || t.startsWith("pattern:");
5846
6401
  };
5847
- md.renderer.rules["fence"] = (tokens, idx, _opts, env) => {
6402
+ md2.renderer.rules["fence"] = (tokens, idx, _opts, env) => {
5848
6403
  const map = env.resolvedFences;
5849
6404
  return map?.get(idx) ?? _mdPlainPre(tokens[idx]?.content ?? "");
5850
6405
  };
5851
- const image = md.renderer.rules["image"];
5852
- md.renderer.rules["image"] = (tokens, idx, opts, env, self) => tokens[idx]?.meta?.pdImage ?? image(tokens, idx, opts, env, self);
6406
+ const image = md2.renderer.rules["image"];
6407
+ md2.renderer.rules["image"] = (tokens, idx, opts, env, self) => tokens[idx]?.meta?.pdImage ?? image(tokens, idx, opts, env, self);
5853
6408
  if (placing) {
5854
- usePlacedTags(md);
5855
- md.renderer.rules["pd_component"] = (_tokens, idx, _opts, env) => env.resolvedPlaced?.get(idx) ?? `<p class="doc-error">[component not placed: see the page's problems]</p>
6409
+ usePlacedTags(md2);
6410
+ md2.renderer.rules["pd_component"] = (_tokens, idx, _opts, env) => env.resolvedPlaced?.get(idx) ?? `<p class="doc-error">[component not placed: see the page's problems]</p>
5856
6411
  `;
5857
6412
  }
5858
- return md;
6413
+ return md2;
5859
6414
  }
5860
6415
  var _md = createDocMarkdown(false);
5861
6416
  var _mdPlaced = createDocMarkdown(true);
@@ -5909,10 +6464,10 @@ function _resolvePatternLinks(tokens, discoveryMap, embed) {
5909
6464
  }
5910
6465
  async function renderDoc(body, renderComponent, discoveryMap, embed = INLINE) {
5911
6466
  const placing = embed.embed === "frame" ? embed.placed : void 0;
5912
- const md = placing ? _mdPlaced : _md;
5913
- const tokens = md.parse(body, { pdLineOffset: placing?.lineOffset ?? 0 });
6467
+ const md2 = placing ? _mdPlaced : _md;
6468
+ const tokens = md2.parse(body, { pdLineOffset: placing?.lineOffset ?? 0 });
5914
6469
  _resolvePatternLinks(tokens, discoveryMap, embed);
5915
- if (embed.embed === "frame" && embed.prose) proseImages(md, tokens, embed.prose);
6470
+ if (embed.embed === "frame" && embed.prose) proseImages(md2, tokens, embed.prose);
5916
6471
  if (embed.embed === "frame") {
5917
6472
  const headings = headingAnchors(tokens, embed.takenIds);
5918
6473
  embed.headings?.push(...headings);
@@ -5933,9 +6488,9 @@ async function renderDoc(body, renderComponent, discoveryMap, embed = INLINE) {
5933
6488
  `);
5934
6489
  }
5935
6490
  }
5936
- return md.renderer.render(tokens, md.options, { resolvedFences, resolvedPlaced });
6491
+ return md2.renderer.render(tokens, md2.options, { resolvedFences, resolvedPlaced });
5937
6492
  }
5938
- function proseImages(md, tokens, prose) {
6493
+ function proseImages(md2, tokens, prose) {
5939
6494
  let eager = prose.slot?.eager ?? 0;
5940
6495
  for (const block2 of tokens) {
5941
6496
  if (block2.type !== "inline" || !block2.children) continue;
@@ -5948,7 +6503,7 @@ function proseImages(md, tokens, prose) {
5948
6503
  if (child.type !== "image") continue;
5949
6504
  const src = child.attrGet("src") ?? "";
5950
6505
  const problem = (message) => prose.problems.push({ line, message: `the image ${src}${message}` });
5951
- const alt = md.renderer.renderInlineAsText(child.children ?? [], md.options, {});
6506
+ const alt = md2.renderer.renderInlineAsText(child.children ?? [], md2.options, {});
5952
6507
  if (alt.trim() === "") {
5953
6508
  problem(` has no alt text: write what it shows between the brackets; a decorative image goes in through a component, with alt=""`);
5954
6509
  continue;
@@ -6135,7 +6690,7 @@ import postcss4 from "postcss";
6135
6690
  import * as sass3 from "sass";
6136
6691
  import { SourceMapConsumer as SourceMapConsumer2 } from "source-map";
6137
6692
  import { parse as parseHtml2 } from "node-html-parser";
6138
- async function recomputeComponentCss(entryFile, compId, stylesDir, componentIds, inBase = () => false) {
6693
+ async function recomputeComponentCss(entryFile, compId, stylesDir, componentIds, inBase = () => false, out) {
6139
6694
  try {
6140
6695
  const compiled = sass3.compile(entryFile, { sourceMap: true, ...sassPackageOptions(entryFile) });
6141
6696
  const rawMap = compiled.sourceMap;
@@ -6143,16 +6698,22 @@ async function recomputeComponentCss(entryFile, compId, stylesDir, componentIds,
6143
6698
  const consumer = new SourceMapConsumer2(rawMap);
6144
6699
  const root = postcss4.parse(compiled.css);
6145
6700
  const bucket = /* @__PURE__ */ new Set();
6701
+ const base2 = /* @__PURE__ */ new Set();
6146
6702
  root.each((node) => {
6147
6703
  const pos = node.source?.start;
6148
6704
  if (!pos) return;
6149
6705
  const original = consumer.originalPositionFor({ line: pos.line, column: pos.column - 1 });
6150
6706
  if (!original.source) return;
6151
6707
  const originPath = original.source.startsWith("file://") ? fileURLToPath4(original.source) : resolve8(dirname8(entryFile), original.source);
6152
- if (inBase(originPath)) return;
6708
+ if (inBase(originPath)) {
6709
+ addLastOccurrence(base2, cssNodeText(node));
6710
+ return;
6711
+ }
6153
6712
  const originComp = resolveScssToComponent(originPath, stylesDir, componentIds);
6154
6713
  if (originComp === compId) addLastOccurrence(bucket, cssNodeText(node));
6714
+ else if (originComp === null) addLastOccurrence(base2, cssNodeText(node));
6155
6715
  });
6716
+ if (out) out.baseRules = joinCssChunks(base2);
6156
6717
  return joinCssChunks(bucket);
6157
6718
  } catch {
6158
6719
  return null;
@@ -6345,13 +6906,13 @@ async function buildComponentDetail(componentId, patternsDir, renderFn, tracker,
6345
6906
  }
6346
6907
 
6347
6908
  // packages/dev-server/src/overview.ts
6348
- import { join as join23 } from "node:path";
6909
+ import { join as join24 } from "node:path";
6349
6910
  async function buildFolderOverview(folderPath, patternsDir, renderFn, discoveryMap) {
6350
6911
  const category = folderPath.split("/")[0];
6351
6912
  let folderLayout = FULL_CATEGORIES.has(stripOrderPrefixes(category)) ? "full" : "centered";
6352
6913
  let doc;
6353
6914
  try {
6354
- const rawIndex = await readFolderFile(patternsDir, join23(folderPath, "index.md"));
6915
+ const rawIndex = await readFolderFile(patternsDir, join24(folderPath, "index.md"));
6355
6916
  if (rawIndex === null) throw new Error("no index.md");
6356
6917
  const m = rawIndex.match(/^preview:\s*(centered|full)$/m);
6357
6918
  if (m) folderLayout = m[1];
@@ -6388,9 +6949,9 @@ async function collectPublicLeaves(folderPath, patternsDir, discoveryMap) {
6388
6949
  }
6389
6950
 
6390
6951
  // packages/dev-server/src/content-hash.ts
6391
- import { createHash as createHash5 } from "node:crypto";
6952
+ import { createHash as createHash6 } from "node:crypto";
6392
6953
  function contentHash(bytes) {
6393
- return createHash5("sha256").update(bytes).digest("hex").slice(0, 10);
6954
+ return createHash6("sha256").update(bytes).digest("hex").slice(0, 10);
6394
6955
  }
6395
6956
 
6396
6957
  // packages/dev-server/src/scripts.ts
@@ -6560,7 +7121,11 @@ async function stampSrcset(value, hashOf) {
6560
7121
  }));
6561
7122
  return entries.join(",");
6562
7123
  }
6563
- async function stampTag(name, attrs, hashOf) {
7124
+ async function stampAbsolute(path, hashOf, base2) {
7125
+ if (base2 === "/" || !path.startsWith(base2)) return stamp(path, hashOf);
7126
+ return `${base2}${(await stamp(`/${path.slice(base2.length)}`, hashOf)).slice(1)}`;
7127
+ }
7128
+ async function stampTag(name, attrs, hashOf, base2) {
6564
7129
  const isPreview = name.toLowerCase() === "meta" && /\s(?:property="og:image"|name="twitter:image")/.test(attrs);
6565
7130
  return replaceAsync(attrs, ATTRIBUTE, async (m) => {
6566
7131
  const [whole, lead, attr2, eq, quoted] = m;
@@ -6573,22 +7138,22 @@ async function stampTag(name, attrs, hashOf) {
6573
7138
  else if (key === "srcset") stamped = await stampSrcset(value, hashOf);
6574
7139
  else if (key === "content" && isPreview) {
6575
7140
  const at = /^(https?:\/\/[^/"']+)(.*)$/.exec(value);
6576
- if (at) stamped = `${at[1]}${await stamp(at[2], hashOf)}`;
7141
+ if (at) stamped = `${at[1]}${await stampAbsolute(at[2], hashOf, base2)}`;
6577
7142
  }
6578
7143
  return `${lead}${attr2}${eq}${q}${stamped}${q}`;
6579
7144
  });
6580
7145
  }
6581
- async function stampHtml(html, hashOf) {
7146
+ async function stampHtml(html, hashOf, base2 = "/") {
6582
7147
  const parts = [];
6583
7148
  let last = 0;
6584
7149
  for (const tag of startTags(html)) {
6585
7150
  parts.push(html.slice(last, tag.start));
6586
7151
  const attrsAt = tag.start + 1 + tag.name.length;
6587
- parts.push(`<${tag.name}${await stampTag(tag.name, tag.attrs, hashOf)}${html.slice(attrsAt + tag.attrs.length, tag.end)}`);
7152
+ parts.push(`<${tag.name}${await stampTag(tag.name, tag.attrs, hashOf, base2)}${html.slice(attrsAt + tag.attrs.length, tag.end)}`);
6588
7153
  last = tag.end;
6589
7154
  if (!tag.content) continue;
6590
7155
  const content = html.slice(tag.content.start, tag.content.end);
6591
- parts.push(tag.name.toLowerCase() === "script" && /\stype=["']?application\/ld\+json/i.test(tag.attrs) ? await replaceAsync(content, /("image":"https?:\/\/[^/"]+)([^"]*)"/g, async (j) => `${j[1]}${await stamp(j[2], hashOf)}"`) : content);
7156
+ parts.push(tag.name.toLowerCase() === "script" && /\stype=["']?application\/ld\+json/i.test(tag.attrs) ? await replaceAsync(content, /("image":"https?:\/\/[^/"]+)([^"]*)"/g, async (j) => `${j[1]}${await stampAbsolute(j[2], hashOf, base2)}"`) : content);
6592
7157
  last = tag.content.end;
6593
7158
  }
6594
7159
  parts.push(html.slice(last));
@@ -6605,7 +7170,7 @@ async function stampCss(css, hashOf) {
6605
7170
 
6606
7171
  // packages/dev-server/src/assets.ts
6607
7172
  import { existsSync as existsSync5 } from "node:fs";
6608
- import { createRequire as createRequire4 } from "node:module";
7173
+ import { createRequire as createRequire5 } from "node:module";
6609
7174
  import { fileURLToPath as fileURLToPath5 } from "node:url";
6610
7175
  function resolveDevUiDist(moduleUrl, exists = existsSync5) {
6611
7176
  const packaged = fileURLToPath5(new URL("./dev-ui/", moduleUrl));
@@ -6613,7 +7178,7 @@ function resolveDevUiDist(moduleUrl, exists = existsSync5) {
6613
7178
  return fileURLToPath5(new URL("../../../apps/dev-ui/dist/", moduleUrl));
6614
7179
  }
6615
7180
  function resolveAxeCore(moduleUrl) {
6616
- return createRequire4(moduleUrl).resolve("axe-core/axe.min.js");
7181
+ return createRequire5(moduleUrl).resolve("axe-core/axe.min.js");
6617
7182
  }
6618
7183
 
6619
7184
  // packages/dev-server/src/storefront/page-image.ts
@@ -6674,6 +7239,7 @@ function jsonLdScript(p) {
6674
7239
  name: p.title,
6675
7240
  ...p.description ? { description: p.description } : {},
6676
7241
  isPartOf: { "@id": site },
7242
+ ...p.published ? { datePublished: p.published } : {},
6677
7243
  ...p.modified ? { dateModified: p.modified } : {},
6678
7244
  ...p.breadcrumbs.length ? {
6679
7245
  breadcrumb: {
@@ -6705,9 +7271,9 @@ function writtenByPatternDump(tag, attrs) {
6705
7271
  if (tag === "script" && attr(attrs, "type") === "application/ld+json") return `<script type="application/ld+json">`;
6706
7272
  return void 0;
6707
7273
  }
6708
- function checkHeadMarkup(html) {
7274
+ function checkHeadMarkup(html, source2 = { label: HEAD_PARTIAL, subject: "a head partial" }) {
6709
7275
  const problems = [];
6710
- const say = (what) => problems.push(`${HEAD_PARTIAL}: ${what}`);
7276
+ const say = (what) => problems.push(`${source2.label}: ${what.replaceAll("a head partial", source2.subject)}`);
6711
7277
  let rest = html.replace(/<!--[\s\S]*?-->/g, "");
6712
7278
  rest = rest.replace(/<(script|style|noscript|title)\b([^>]*)>[\s\S]*?<\/\1\s*>/gi, (_m, rawTag, attrs) => {
6713
7279
  const tag = rawTag.toLowerCase();
@@ -6781,13 +7347,14 @@ var LIVE_NOTICE = `function(t){var n=document.querySelector("[data-pd-live-notic
6781
7347
  function exampleHeightCheck() {
6782
7348
  return `(function(){var notice=(${LIVE_NOTICE});document.querySelectorAll("figure.doc-example iframe[height]").forEach(function(f){var check=function(){var d=f.contentDocument;if(!d||d.URL==="about:blank")return;d.fonts.ready.then(function(){var declared=Number(f.getAttribute("height")),root=d.documentElement.getBoundingClientRect().height,id=f.title,outcome="fits";if(root===f.clientHeight)outcome="can't tell (the content is as tall as its frame)";else if(root<declared-1){outcome="too tall";var t=id+": its live example is declared "+declared+"px tall (x-pd-example-height), but its content is "+Math.round(root)+"px. A frame is never fitted shorter than it starts, so it stays too tall: declare at most "+Math.floor(root)+".";console.warn(t);notice(t);}console.debug("[pd] example height of "+id+": "+outcome);});};f.addEventListener("load",check);if(f.contentDocument&&f.contentDocument.readyState==="complete")check();});})();`;
6783
7349
  }
6784
- function document(head, body, live, lang = "en") {
7350
+ function document2(head, body, live, lang = "en", base2) {
6785
7351
  return `<!DOCTYPE html>
6786
7352
  <html lang="${escapeAttr(lang)}">
6787
7353
  <head>
6788
7354
  <meta charset="UTF-8">
6789
7355
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6790
- ${head}
7356
+ ${base2 !== void 0 ? `<meta name="pd-base-path" content="${escapeAttr(base2)}">
7357
+ ` : ""}${head}
6791
7358
  </head>
6792
7359
  <body>
6793
7360
  ${body}
@@ -6844,7 +7411,7 @@ var BUILT_IN_NOT_FOUND_DOCUMENT = `<main>
6844
7411
  </main>`;
6845
7412
  function errorDocument(route, problems, live, siteWide = []) {
6846
7413
  const items = problems.map((p) => `<li${siteWide.includes(p) ? " data-pd-site" : ""}>${escapeHtml(p)}</li>`).join("\n");
6847
- return document(
7414
+ return document2(
6848
7415
  `<title>Problems \u2014 ${escapeHtml(route)}</title>`,
6849
7416
  `<main>
6850
7417
  <h1>The page ${escapeHtml(route)} cannot be built</h1>
@@ -6878,7 +7445,7 @@ async function assemblePage(route, model, a) {
6878
7445
  if (!a.discoveryMap.has(layoutId)) {
6879
7446
  return {
6880
7447
  status: 404,
6881
- html: document(`<title>Page not found</title>`, BUILT_IN_NOT_FOUND_DOCUMENT, a.live, a.lang),
7448
+ html: document2(`<title>Page not found</title>`, BUILT_IN_NOT_FOUND_DOCUMENT, a.live, a.lang, a.basePath ?? "/"),
6882
7449
  problems: [],
6883
7450
  notes: [`there is no ${NOT_FOUND_FILE} and no ${layoutId}, so the 404 page is a plain one of Pattern Dump's`]
6884
7451
  };
@@ -6903,7 +7470,10 @@ async function assemblePage(route, model, a) {
6903
7470
  const line = page.frontmatter.lines["redirectFrom"];
6904
7471
  problems.push(`${page.path}${line !== void 0 ? `:${line}` : ""}: the 404 page can't have redirectFrom: it is served for every address that has no page`);
6905
7472
  }
6906
- const site = await checkSite(a.site, a.discoveryMap, new Set([...model.files.values()].map((f) => f.path)));
7473
+ const site = await checkSite(a.site, a.discoveryMap, new Set([...model.files.values()].map((f) => f.path)), {
7474
+ folders: new Set([...model.folders.keys()].filter((r) => r !== "/").map((r) => r.slice(1, -1))),
7475
+ leftOut: model.drafts
7476
+ });
6907
7477
  siteWide = site.problems;
6908
7478
  problems.push(...site.problems);
6909
7479
  const imageLine = page.frontmatter.lines["image"];
@@ -6919,6 +7489,7 @@ async function assemblePage(route, model, a) {
6919
7489
  const layoutSchema = (await readSchema(a.discoveryMap.get(checked.layoutId)?.schemaPath)).schema;
6920
7490
  const proseSlot = readProseSlot(layoutSchema);
6921
7491
  const imageLookup = a.images ? await a.images() : void 0;
7492
+ const placedIds = /* @__PURE__ */ new Set();
6922
7493
  const renderBody = async (takenIds = []) => {
6923
7494
  const prose = imageLookup ? {
6924
7495
  layoutId: checked.layoutId,
@@ -6929,6 +7500,7 @@ async function assemblePage(route, model, a) {
6929
7500
  ...proseSlot.problem ? { slotProblem: proseSlot.problem } : {}
6930
7501
  } : void 0;
6931
7502
  const docProblems = [];
7503
+ placedIds.clear();
6932
7504
  const placedProblems = [];
6933
7505
  const headings = [];
6934
7506
  const body = await renderDoc(
@@ -6950,7 +7522,11 @@ async function assemblePage(route, model, a) {
6950
7522
  discoveryMap: a.discoveryMap,
6951
7523
  handleMap: a.handleMap,
6952
7524
  composition: a.composition,
6953
- render: a.render,
7525
+ // Each component rendered for a placement: the page loads its behaviour scripts (page-head.ts).
7526
+ render: (input) => {
7527
+ placedIds.add(input.component.split("~")[0]);
7528
+ return a.render(input);
7529
+ },
6954
7530
  schemaOf: async (id) => (await readSchema(a.discoveryMap.get(id)?.schemaPath)).schema,
6955
7531
  shown: a.shown
6956
7532
  }
@@ -6971,37 +7547,65 @@ async function assemblePage(route, model, a) {
6971
7547
  const notes = [];
6972
7548
  let dates = {};
6973
7549
  let modified = null;
6974
- const showsDates = pageDeclaration.declared?.some((k) => k === "created" || k === "modified") ?? false;
7550
+ let published = checked.build.date ?? null;
7551
+ const mode = a.live ? "dev" : "build";
7552
+ const declares = (k) => pageDeclaration.declared?.includes(k) ?? false;
7553
+ const showsDates = declares("created") || declares("modified");
6975
7554
  if (a.dates && !isNotFound && (showsDates || a.siteUrl !== void 0)) {
6976
- const dated = await a.dates.of(page.path, a.live ? "dev" : "build");
7555
+ const dated = await a.dates.of(page.path, mode);
6977
7556
  if (showsDates) {
6978
7557
  dates = dated.dates;
6979
7558
  notes.push(...dated.notes);
6980
7559
  problems.push(...dated.problems);
6981
7560
  }
6982
7561
  if (dated.shallow) notes.push(SHALLOW_SEO_NOTE);
6983
- else modified = dated.dates.modified;
6984
- }
7562
+ else {
7563
+ modified = dated.dates.modified;
7564
+ published ??= dated.dates.created;
7565
+ }
7566
+ }
7567
+ const listing = {};
7568
+ if (declares("children")) {
7569
+ const listed = isNotFound ? { entries: [], problems: [], notes: [] } : await entriesOf(route, model, { dates: a.dates, mode });
7570
+ listing.children = listed.entries;
7571
+ problems.push(...listed.problems);
7572
+ notes.push(...listed.notes);
7573
+ }
7574
+ if (declares("date")) {
7575
+ const dated = isNotFound ? { date: null, problems: [], notes: [] } : await dateOf(route, model, a.dates, mode);
7576
+ listing.date = dated.date;
7577
+ problems.push(...dated.problems);
7578
+ notes.push(...dated.notes);
7579
+ }
7580
+ if (declares("tags")) listing.tags = model.entries.get(route)?.tags ?? [];
7581
+ const collections = site.collections.length ? await resolveCollections(site.collections, model, { dates: a.dates, mode }) : void 0;
7582
+ if (collections) {
7583
+ problems.push(...collections.problems);
7584
+ notes.push(...collections.notes);
7585
+ }
7586
+ notes.push(...site.notes ?? []);
6985
7587
  if (a.discoveryMap.has(checked.layoutId)) {
6986
- problems.push(...await checkSampleData(checked.layoutId, a.discoveryMap, a.composition, layoutDeclared, siteDeclared, pageDeclaration.declared, site.listed));
7588
+ problems.push(...await checkSampleData(checked.layoutId, a.discoveryMap, a.composition, layoutDeclared, siteDeclared, pageDeclaration.declared, site.listed, site.collections));
6987
7589
  }
6988
7590
  const hasHead = a.discoveryMap.has(HEAD_PARTIAL);
6989
7591
  if (hasHead) {
6990
- const headSample = await checkSampleData(HEAD_PARTIAL, a.discoveryMap, a.composition, [], siteDeclared, void 0, site.listed);
7592
+ const headSample = await checkSampleData(HEAD_PARTIAL, a.discoveryMap, a.composition, [], siteDeclared, void 0, site.listed, site.collections);
6991
7593
  siteWide = [...siteWide, ...headSample];
6992
7594
  problems.push(...headSample);
6993
7595
  }
6994
7596
  if (problems.length) return fail(problems);
7597
+ if (a.live) notes.push(...model.drafts.map((d) => `${d} is a draft: pd build leaves it out`));
6995
7598
  const siteData = {
6996
7599
  ...withDeclaredAbsentAsNull(site.values, siteDeclared),
6997
- ...site.listed.length ? { navigations: resolveNavigations(site.navigations, site.listed, model, route) } : {}
7600
+ ...site.listed.length ? { navigations: resolveNavigations(site.navigations, site.listed, model, route) } : {},
7601
+ ...collections ? { collections: collections.collections } : {}
6998
7602
  };
6999
7603
  const dataFor = (body, headings) => ({
7000
7604
  ...withDeclaredAbsentAsNull(checked.values, layoutDeclared, ["body", "nav", "site", "page"]),
7001
7605
  body,
7002
7606
  nav: markCurrent(model.nav, route),
7003
7607
  site: siteData,
7004
- ...pageDeclaration.declared ? { page: pageValue(pageDeclaration.declared, { headings, ...positionOf(route, model), ...dates }) } : {}
7608
+ ...pageDeclaration.declared ? { page: pageValue(pageDeclaration.declared, { headings, ...positionOf(route, model), ...dates, ...listing }) } : {}
7005
7609
  });
7006
7610
  const renderLayout = async (body, headings) => {
7007
7611
  try {
@@ -7051,6 +7655,30 @@ async function assemblePage(route, model, a) {
7051
7655
  return fail(markup);
7052
7656
  }
7053
7657
  }
7658
+ let siteHead = "";
7659
+ if (a.siteHead) {
7660
+ const label = `storefront.head (${a.siteHead.name})`;
7661
+ let source3;
7662
+ try {
7663
+ source3 = await readFile23(a.siteHead.path, "utf8");
7664
+ } catch {
7665
+ const p = `${label}: there is no such file`;
7666
+ siteWide = [...siteWide, p];
7667
+ return fail([p]);
7668
+ }
7669
+ try {
7670
+ siteHead = renderPlainTemplate(source3, { site: siteData }).trim();
7671
+ } catch (e) {
7672
+ const p = `${label}: ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`;
7673
+ siteWide = [...siteWide, p];
7674
+ return fail([p]);
7675
+ }
7676
+ const markup = checkHeadMarkup(siteHead, { label, subject: "the site's head" });
7677
+ if (markup.length) {
7678
+ siteWide = [...siteWide, ...markup];
7679
+ return fail(markup);
7680
+ }
7681
+ }
7054
7682
  const title = typeof checked.values["title"] === "string" ? checked.values["title"] : isNotFound ? "Page not found" : route;
7055
7683
  const siteTitle = typeof site.values["title"] === "string" ? site.values["title"] : void 0;
7056
7684
  const description = typeof checked.values["description"] === "string" ? checked.values["description"] : void 0;
@@ -7067,9 +7695,10 @@ async function assemblePage(route, model, a) {
7067
7695
  image: imageCheck?.ok ? imageCheck.image : void 0,
7068
7696
  index: !isNotFound && a.siteIndex !== false && checked.build.index !== false
7069
7697
  }),
7070
- ...a.siteUrl !== void 0 && !isNotFound ? [jsonLdScript({ siteUrl: a.siteUrl, siteTitle, route, title, description, breadcrumbs: positionOf(route, model).breadcrumbs, modified })] : [],
7698
+ ...a.siteUrl !== void 0 && !isNotFound ? [jsonLdScript({ siteUrl: a.siteUrl, siteTitle, route, title, description, breadcrumbs: positionOf(route, model).breadcrumbs, modified, published })] : [],
7071
7699
  ...headExtra ? [headExtra] : [],
7072
- await a.head(checked.layoutId, html)
7700
+ ...siteHead ? [siteHead] : [],
7701
+ await a.head(checked.layoutId, html, [...placedIds])
7073
7702
  ].join("\n");
7074
7703
  if (isNotFound) {
7075
7704
  const relative19 = relativeUrls(`${head}
@@ -7106,21 +7735,33 @@ ${html}`);
7106
7735
  const self = isNotFound ? PD_ROUTES.notFound : route;
7107
7736
  const routes = /* @__PURE__ */ new Set([...model.files.keys(), ...[...model.files.values()].flatMap(redirectsOf), PD_ROUTES.notFound]);
7108
7737
  routes.add(self);
7109
- const links = checkLinks({ built: routes, documents: /* @__PURE__ */ new Map([[self, html]]), pages: [self], pagesOnly: true });
7738
+ const links = checkLinks({
7739
+ built: routes,
7740
+ documents: /* @__PURE__ */ new Map([[self, html]]),
7741
+ pages: [self],
7742
+ pagesOnly: true,
7743
+ ...a.basePath !== void 0 ? { pathHint: a.basePath } : {}
7744
+ });
7110
7745
  notes.push(...links.problems.map((p) => `warning: ${page.path}${p.slice(self.length)}`));
7111
7746
  notes.push(...checkPageBasics(html).map((p) => `warning: ${page.path}: ${p}`));
7112
7747
  }
7113
- return { status: isNotFound ? 404 : 200, html: document(head, `${html}
7114
- ${fitterScript()}`, a.live, a.lang), problems: [], notes };
7748
+ return { status: isNotFound ? 404 : 200, html: document2(head, `${html}
7749
+ ${fitterScript()}`, a.live, a.lang, a.basePath ?? "/"), problems: [], notes };
7115
7750
  }
7116
7751
 
7117
7752
  // packages/dev-server/src/storefront/page-head.ts
7118
- async function pageStylesheets(cfg, state, discovery, layoutId, html) {
7119
- const { discoveryMap } = discovery;
7753
+ async function pageStylesheets(cfg, state, discovery, layoutId, html, placed = []) {
7754
+ const { discoveryMap, handleMap } = discovery;
7755
+ const requireScripts = [];
7756
+ for (const id of [layoutId, ...placed]) {
7757
+ for (const s of (await aggregateRequires(id, cfg.patternDirs, handleMap, discoveryMap)).scripts) {
7758
+ if (!requireScripts.includes(s)) requireScripts.push(s);
7759
+ }
7760
+ }
7120
7761
  const assets = await resolveDocumentAssets(cfg, state, {
7121
7762
  componentId: layoutId,
7122
7763
  html,
7123
- requireScripts: void 0,
7764
+ requireScripts,
7124
7765
  requireAssets: void 0,
7125
7766
  compositionRegistry: await compositionFor(state, discovery, cfg.patternDirs),
7126
7767
  discoveryMap
@@ -7133,7 +7774,8 @@ async function pageStylesheets(cfg, state, discovery, layoutId, html) {
7133
7774
  themeSwitch: cfg.themeSwitch,
7134
7775
  baseCssVersion: state.baseCssVersion
7135
7776
  });
7136
- return [layers.base, layers.assets, layers.themes, layers.closure].filter(Boolean).join("\n");
7777
+ const scripts = assets.scripts.map((src) => src.includes("://") ? `<script type="module" src="${escapeAttr(src)}"></script>` : `<script type="module" src="${escapeAttr(scriptUrl(src, assets.scriptVersions?.get(src) ?? void 0))}"></script>`);
7778
+ return [layers.base, layers.assets, layers.themes, layers.closure, ...scripts].filter(Boolean).join("\n");
7137
7779
  }
7138
7780
 
7139
7781
  // packages/dev-server/src/routes.ts
@@ -7147,9 +7789,31 @@ async function handleRequest(req, res, ctx) {
7147
7789
  return;
7148
7790
  }
7149
7791
  const host = (req.headers.host ?? "").replace(/:\d+$/, "").toLowerCase();
7150
- if (cfg.storefront && host === cfg.storefront.host) return stampedResponse(req, res, ctx);
7792
+ if (cfg.storefront && host === cfg.storefront.host) {
7793
+ if (!underBasePath(req, res, cfg.storefront.basePath)) return;
7794
+ return stampedResponse(req, res, ctx);
7795
+ }
7151
7796
  return routeRequest(req, res, ctx);
7152
7797
  }
7798
+ function underBasePath(req, res, base2) {
7799
+ if (base2 === "/") return true;
7800
+ const raw = req.url ?? "/";
7801
+ const at = raw.search(/[?#]/);
7802
+ const path = at < 0 ? raw : raw.slice(0, at);
7803
+ const query = at < 0 ? "" : raw.slice(at);
7804
+ if (path === TEST_SETTLE_ROUTE) return true;
7805
+ if (path.startsWith(base2)) {
7806
+ req.url = `/${raw.slice(base2.length)}`;
7807
+ return true;
7808
+ }
7809
+ if (path === "/" || `${path}/` === base2) {
7810
+ res.writeHead(302, { Location: `${base2}${query}` });
7811
+ res.end();
7812
+ return false;
7813
+ }
7814
+ req.url = PD_ROUTES.notFound;
7815
+ return true;
7816
+ }
7153
7817
  function holdResponse() {
7154
7818
  let status = 200;
7155
7819
  const headers = {};
@@ -7184,12 +7848,12 @@ function holdResponse() {
7184
7848
  var stampCaches = /* @__PURE__ */ new WeakMap();
7185
7849
  function hashOfFor(req, ctx) {
7186
7850
  const generation = ctx.generation?.() ?? "";
7187
- let cache = stampCaches.get(ctx);
7188
- if (!cache || cache.generation !== generation) {
7189
- cache = { generation, hashes: /* @__PURE__ */ new Map() };
7190
- stampCaches.set(ctx, cache);
7851
+ let cache2 = stampCaches.get(ctx);
7852
+ if (!cache2 || cache2.generation !== generation) {
7853
+ cache2 = { generation, hashes: /* @__PURE__ */ new Map() };
7854
+ stampCaches.set(ctx, cache2);
7191
7855
  }
7192
- const { hashes } = cache;
7856
+ const { hashes } = cache2;
7193
7857
  return (path) => {
7194
7858
  let hash = hashes.get(path);
7195
7859
  if (!hash) {
@@ -7215,8 +7879,13 @@ async function stampedResponse(req, res, ctx) {
7215
7879
  await routeRequest(req, held.res, ctx);
7216
7880
  const type = String(held.headers["content-type"] ?? "");
7217
7881
  let body = held.body();
7218
- if (/^text\/html\b/.test(type)) body = await stampHtml(body.toString("utf8"), hashOfFor(req, ctx));
7219
- else if (/^text\/css\b/.test(type)) body = await stampCss(body.toString("utf8"), hashOfFor(req, ctx));
7882
+ const sf = ctx.cfg.storefront;
7883
+ const base2 = { base: sf.basePath, workbenchPath: sameHostWorkbenchPath(sf) };
7884
+ const preview = sf.basePath !== "/" && (req.url ?? "").startsWith(PD_ROUTE_PREFIX.preview);
7885
+ if (/^text\/html\b/.test(type)) {
7886
+ const html = withBaseHtml(await stampHtml(body.toString("utf8"), hashOfFor(req, ctx), sf.basePath), base2);
7887
+ body = preview ? withNoindex(html) : html;
7888
+ } else if (/^text\/css\b/.test(type)) body = withBaseCss(await stampCss(body.toString("utf8"), hashOfFor(req, ctx)), base2);
7220
7889
  res.writeHead(held.status(), held.headers);
7221
7890
  res.end(body);
7222
7891
  }
@@ -7265,10 +7934,10 @@ async function routeRequest(req, res, ctx) {
7265
7934
  return;
7266
7935
  }
7267
7936
  if (pathname === PD_ROUTES.mainJs) {
7268
- return await serveFile(res, join24(devUiDist2, "main.js"), "application/javascript");
7937
+ return await serveFile(res, join25(devUiDist2, "main.js"), "application/javascript");
7269
7938
  }
7270
7939
  if (pathname === PD_ROUTES.mainCss) {
7271
- return await serveFile(res, join24(devUiDist2, "main.css"), "text/css");
7940
+ return await serveFile(res, join25(devUiDist2, "main.css"), "text/css");
7272
7941
  }
7273
7942
  if (pathname === PD_ROUTES.axeCore) {
7274
7943
  return await serveFile(res, resolveAxeCore(import.meta.url), "application/javascript");
@@ -7586,7 +8255,7 @@ async function routeRequest(req, res, ctx) {
7586
8255
  if (cfg.storefront && host === cfg.storefront.host) {
7587
8256
  const discovery = await currentDiscovery(state, patternRoots);
7588
8257
  const discoveryMap = discovery.discoveryMap;
7589
- const model = await readPages(cfg.storefront.pagesDir, { workbenchPath: sameHostWorkbenchPath(cfg.storefront) });
8258
+ const model = await readPages(cfg.storefront.pagesDir, { workbenchPath: sameHostWorkbenchPath(cfg.storefront), drafts: ctx.live ? "show" : "leave-out" });
7590
8259
  if (pathname === PD_ROUTES.pages) {
7591
8260
  if (model.problems.length) {
7592
8261
  json(res, 500, { error: model.problems.join("\n") });
@@ -7595,8 +8264,8 @@ async function routeRequest(req, res, ctx) {
7595
8264
  json(res, 200, { pages: model.pages, nav: model.nav });
7596
8265
  return;
7597
8266
  }
7598
- const siteUrl = cfg.storefront.url === void 0 ? void 0 : ctx.live ? `http://${req.headers.host ?? cfg.storefront.host}/` : cfg.storefront.url;
7599
- if (pathname === PD_ROUTES.robots) {
8267
+ const siteUrl = cfg.storefront.url === void 0 ? void 0 : ctx.live ? `http://${req.headers.host ?? cfg.storefront.host}${cfg.storefront.basePath}` : cfg.storefront.url;
8268
+ if (pathname === PD_ROUTES.robots && cfg.storefront.basePath === "/") {
7600
8269
  res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
7601
8270
  res.end(storefrontRobotsTxt({ siteUrl, index: cfg.storefront.index, workbenchPath: sameHostWorkbenchPath(cfg.storefront) }));
7602
8271
  return;
@@ -7635,13 +8304,15 @@ async function routeRequest(req, res, ctx) {
7635
8304
  render,
7636
8305
  site: ctx.site.get(),
7637
8306
  workbenchUrl: ctx.storefrontWorkbenchUrl ?? `http://${PD_HOSTS.workbench}${port2 ? `:${port2}` : ""}/`,
7638
- head: (layoutId, html) => pageStylesheets(cfg, state, discovery, layoutId, html),
8307
+ head: (layoutId, html, placed) => pageStylesheets(cfg, state, discovery, layoutId, html, placed),
7639
8308
  ...ctx.dates ? { dates: ctx.dates } : {},
7640
8309
  ...ctx.live ? { live: { wsPort, generation: generation ?? "" } } : {},
7641
8310
  image: (path) => checkPageImage(path, assetRoots, assetRootNames),
7642
8311
  ...siteUrl !== void 0 ? { siteUrl } : {},
7643
8312
  lang: cfg.storefront.lang,
7644
8313
  siteIndex: cfg.storefront.index,
8314
+ basePath: cfg.storefront.basePath,
8315
+ ...cfg.storefront.head !== void 0 ? { siteHead: { path: cfg.storefront.head, name: cfg.storefront.headName ?? cfg.storefront.head } } : {},
7645
8316
  ...cfg.storefront.image !== void 0 ? { siteImage: cfg.storefront.image } : {},
7646
8317
  ...ctx.images ? { images: ctx.images } : {}
7647
8318
  });
@@ -7734,7 +8405,7 @@ function createRenderingHandle(build, registrars, globalData) {
7734
8405
 
7735
8406
  // packages/dev-server/src/watch-roots.ts
7736
8407
  import { statSync as statSync3 } from "node:fs";
7737
- import { dirname as dirname9, extname as extname3, join as join25, resolve as resolve9, sep as sep8 } from "node:path";
8408
+ import { dirname as dirname9, extname as extname3, join as join26, resolve as resolve9, sep as sep8 } from "node:path";
7738
8409
  var MODULE = /\.[mc]?js$/;
7739
8410
  function helperScope(path) {
7740
8411
  let isDir = false;
@@ -7753,23 +8424,23 @@ function watchedRoots(cfg) {
7753
8424
  styles: [...projectStyles, ...packageStyles].map((r) => ({ dir: r.dir, inBase: documentBaseMatcher(r.dir, r.base) })),
7754
8425
  themes: [
7755
8426
  ...cfg.config.themes?.dir && cfg.themesDir ? [cfg.themesDir] : [],
7756
- ...[...linked.values()].filter((p) => p.manifest.themes).map((p) => join25(p.dir, p.manifest.themes.dir))
8427
+ ...[...linked.values()].filter((p) => p.manifest.themes).map((p) => join26(p.dir, p.manifest.themes.dir))
7757
8428
  ],
7758
8429
  scripts: [
7759
8430
  ...cfg.config.scriptsDir && cfg.scriptsDirs[0] ? [cfg.scriptsDirs[0]] : [],
7760
- ...[...linked.values()].filter((p) => p.manifest.scripts).map((p) => join25(p.dir, p.manifest.scripts))
8431
+ ...[...linked.values()].filter((p) => p.manifest.scripts).map((p) => join26(p.dir, p.manifest.scripts))
7761
8432
  ],
7762
8433
  data: [
7763
8434
  ...cfg.config.dataDir ? [resolve9(cfg.cwd, cfg.config.dataDir)] : [],
7764
- ...[...linked.values()].filter((p) => p.manifest.data).map((p) => join25(p.dir, p.manifest.data))
8435
+ ...[...linked.values()].filter((p) => p.manifest.data).map((p) => join26(p.dir, p.manifest.data))
7765
8436
  ],
7766
8437
  assets: [
7767
8438
  ...[cfg.config.assets?.dir ?? []].flat().map((d) => resolve9(cfg.cwd, d)),
7768
- ...[...linked.values()].flatMap((p) => (p.manifest.assets ?? []).map((a) => join25(p.dir, a)))
8439
+ ...[...linked.values()].flatMap((p) => (p.manifest.assets ?? []).map((a) => join26(p.dir, a)))
7769
8440
  ],
7770
8441
  helpers: [
7771
8442
  ...cfg.config.helpers ? [resolve9(cfg.cwd, cfg.config.helpers)] : [],
7772
- ...[...linked.values()].filter((p) => p.manifest.helpers).map((p) => join25(p.dir, p.manifest.helpers))
8443
+ ...[...linked.values()].filter((p) => p.manifest.helpers).map((p) => join26(p.dir, p.manifest.helpers))
7773
8444
  ].map(helperScope)
7774
8445
  };
7775
8446
  }
@@ -7800,10 +8471,10 @@ function rootMatchers(watched) {
7800
8471
  }
7801
8472
 
7802
8473
  // packages/dev-server/src/watch.ts
7803
- import { createHash as createHash6 } from "node:crypto";
8474
+ import { createHash as createHash7 } from "node:crypto";
7804
8475
  import { existsSync as existsSync6, readFileSync as readFileSync4, statSync as statSync4 } from "node:fs";
7805
8476
  import { appendFile, readdir as readdir9 } from "node:fs/promises";
7806
- import { basename as basename10, extname as extname4, join as join26, relative as relative13, resolve as resolve10, sep as sep9 } from "node:path";
8477
+ import { basename as basename10, extname as extname4, join as join27, relative as relative13, resolve as resolve10, sep as sep9 } from "node:path";
7807
8478
  import chokidar from "chokidar";
7808
8479
 
7809
8480
  // packages/dev-server/src/config-change.ts
@@ -7862,7 +8533,7 @@ function seenNow(path) {
7862
8533
  const s = statSync4(path);
7863
8534
  const stamp2 = `${s.ino}:${s.ctimeMs}:${s.mtimeMs}:${s.size}`;
7864
8535
  const forced = globalThis.__testOnly_watchStamp;
7865
- return { stamp: forced ? forced(path, stamp2) : stamp2, real: stamp2, hash: createHash6("sha1").update(readFileSync4(path)).digest("hex") };
8536
+ return { stamp: forced ? forced(path, stamp2) : stamp2, real: stamp2, hash: createHash7("sha1").update(readFileSync4(path)).digest("hex") };
7866
8537
  } catch {
7867
8538
  return null;
7868
8539
  }
@@ -7893,7 +8564,7 @@ async function startWatching(ctx) {
7893
8564
  const barrierWaiters = /* @__PURE__ */ new Set();
7894
8565
  const SETTLE_MS = 1e4;
7895
8566
  async function settle() {
7896
- const files = [...new Set(watchDirs)].filter((d) => existsSync6(d)).map((d) => join26(d, BARRIER));
8567
+ const files = [...new Set(watchDirs)].filter((d) => existsSync6(d)).map((d) => join27(d, BARRIER));
7897
8568
  const waiting = new Set(files);
7898
8569
  const handled = new Promise((resolve15, reject) => {
7899
8570
  if (!waiting.size) {
@@ -8151,7 +8822,7 @@ async function startWatching(ctx) {
8151
8822
  }
8152
8823
  for (const entry of entries) {
8153
8824
  if (closed) return true;
8154
- const path = join26(dir, entry.name);
8825
+ const path = join27(dir, entry.name);
8155
8826
  if (entry.isDirectory()) {
8156
8827
  if (firstReport(path)) dispatchers.addDir?.(path);
8157
8828
  await scanFolder(path);
@@ -8261,7 +8932,11 @@ async function startWatching(ctx) {
8261
8932
  const compId = componentOfScss(resolve10(recompiledFile), allIds);
8262
8933
  const root = styleRootOf(resolve10(recompiledFile));
8263
8934
  if (compId && root) {
8264
- const newCss = await recomputeComponentCss(recompiledFile, compId, root.dir, allIds, root.inBase);
8935
+ const out = {};
8936
+ const newCss = await recomputeComponentCss(recompiledFile, compId, root.dir, allIds, root.inBase, out);
8937
+ if (newCss !== null && (out.baseRules ?? "") !== (state.entryBaseRules.get(resolve10(recompiledFile)) ?? "")) {
8938
+ return restyleAll(discovery, timestamp, effectiveFiles, compileErrors.length > 0);
8939
+ }
8265
8940
  if (newCss !== null) {
8266
8941
  const wasEmpty = (state.perComponentCss.get(compId) ?? "") === "";
8267
8942
  state.perComponentCss.set(compId, newCss);
@@ -8463,7 +9138,7 @@ async function startWatching(ctx) {
8463
9138
  async function pagesFingerprint() {
8464
9139
  if (!pagesDir) return null;
8465
9140
  const model = await readPages(pagesDir, { workbenchPath });
8466
- return JSON.stringify({ nav: model.nav, pages: model.pages, problems: model.problems });
9141
+ return JSON.stringify({ nav: model.nav, pages: model.pages, problems: model.problems, entries: [...model.entries], folders: [...model.folders] });
8467
9142
  }
8468
9143
  async function handlePages(filePath, event) {
8469
9144
  if (!inPages(filePath)) return false;
@@ -8768,7 +9443,7 @@ function createGitDates(pagesDir, listPages, git = runGit) {
8768
9443
 
8769
9444
  // packages/dev-server/src/export-io.ts
8770
9445
  import { readFile as readFile25, writeFile as writeFile6, mkdir as mkdir6 } from "node:fs/promises";
8771
- import { dirname as dirname10, join as join27, relative as relative15, resolve as resolve11 } from "node:path";
9446
+ import { dirname as dirname10, join as join28, relative as relative15, resolve as resolve11 } from "node:path";
8772
9447
 
8773
9448
  // packages/dev-server/src/export.ts
8774
9449
  var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -8961,20 +9636,20 @@ async function writeExport(families, sink) {
8961
9636
  const files = [];
8962
9637
  for (const family of families) {
8963
9638
  const baseRel = `${family.baseId}.hbs`;
8964
- const basePath = join27(sink, baseRel);
9639
+ const basePath = join28(sink, baseRel);
8965
9640
  await mkdir6(dirname10(basePath), { recursive: true });
8966
9641
  await writeFile6(basePath, family.baseHbs, "utf-8");
8967
9642
  files.push(baseRel);
8968
9643
  if (family.baseData) {
8969
9644
  const dataRel = `${family.baseId}.json`;
8970
- await writeFile6(join27(sink, dataRel), JSON.stringify(family.baseData, null, 2) + "\n", "utf-8");
9645
+ await writeFile6(join28(sink, dataRel), JSON.stringify(family.baseData, null, 2) + "\n", "utf-8");
8971
9646
  files.push(dataRel);
8972
9647
  }
8973
9648
  for (const v of family.variants) {
8974
9649
  if (v.token === "") continue;
8975
9650
  const overlayRel = `${family.baseId}~${v.token}.json`;
8976
9651
  const overlay = { modifier: v.modifier, ...v.data ?? {} };
8977
- await writeFile6(join27(sink, overlayRel), JSON.stringify(overlay, null, 2) + "\n", "utf-8");
9652
+ await writeFile6(join28(sink, overlayRel), JSON.stringify(overlay, null, 2) + "\n", "utf-8");
8978
9653
  files.push(overlayRel);
8979
9654
  }
8980
9655
  }
@@ -9006,7 +9681,7 @@ async function writeThreadedIncluders(threaded, patternsDir, sink) {
9006
9681
  if (res.rewrites === 0) continue;
9007
9682
  rewrites += res.rewrites;
9008
9683
  const rel = relative15(patternsDir, hbsPath);
9009
- const outPath = join27(sink, rel);
9684
+ const outPath = join28(sink, rel);
9010
9685
  await mkdir6(dirname10(outPath), { recursive: true });
9011
9686
  await writeFile6(outPath, res.hbs, "utf-8");
9012
9687
  files.push(rel);
@@ -9162,6 +9837,16 @@ async function startDevServer(config, cwd, opts = {}) {
9162
9837
  build: opts.live === false,
9163
9838
  warn: (message) => console.warn(`[dev-server] ${message}`)
9164
9839
  });
9840
+ const helperNamesSaid = /* @__PURE__ */ new Set();
9841
+ const reportHelperNames = (diagnostics) => {
9842
+ for (const d of diagnostics ?? []) {
9843
+ if (d.kind !== "SCHEMA_INVALID" || !isHelperNameProblem(d.message)) continue;
9844
+ const line = `${d.location?.file ?? "a schema"} (${d.path ?? "#"}): ${d.message}`;
9845
+ if (helperNamesSaid.has(line)) continue;
9846
+ helperNamesSaid.add(line);
9847
+ console.error(`[dev-server] error: ${line}`);
9848
+ }
9849
+ };
9165
9850
  const buildRenderer = (registrars, data) => {
9166
9851
  const renderWith = createRenderer({
9167
9852
  patternsDir,
@@ -9175,7 +9860,9 @@ async function startDevServer(config, cwd, opts = {}) {
9175
9860
  });
9176
9861
  return async (input) => {
9177
9862
  await images.ready(generationNow());
9178
- return renderWith(input);
9863
+ const result = await renderWith(input);
9864
+ reportHelperNames(result.meta?.diagnostics);
9865
+ return result;
9179
9866
  };
9180
9867
  };
9181
9868
  const rendering = createRenderingHandle(buildRenderer, helperRegistrars, globalData);
@@ -9199,7 +9886,7 @@ async function startDevServer(config, cwd, opts = {}) {
9199
9886
  if (cfg.themeDirs.length) {
9200
9887
  for (const { name, dir } of await discoverThemeFiles(cfg.themeDirs)) {
9201
9888
  try {
9202
- const css = await compileSassFile(join28(dir, `${name}.theme.scss`));
9889
+ const css = await compileSassFile(join29(dir, `${name}.theme.scss`));
9203
9890
  state.themeCss.set(name, css);
9204
9891
  const slots = config.themes?.dir && dir === cfg.themeDirs[0] ? swatchSlots : cfg.packageSwatchSlots;
9205
9892
  state.themeTokens.set(name, await extractThemeTokens(name, css, slots, dir));
@@ -9275,7 +9962,7 @@ async function startDevServer(config, cwd, opts = {}) {
9275
9962
  httpPort = actualPort;
9276
9963
  console.log(`[dev-server] Listening at http://127.0.0.1:${actualPort}`);
9277
9964
  if (live) {
9278
- if (cfg.storefront) console.log(`[dev-server] Website: http://${cfg.storefront.host}:${actualPort}/`);
9965
+ if (cfg.storefront) console.log(`[dev-server] Website: http://${cfg.storefront.host}:${actualPort}${cfg.storefront.basePath}`);
9279
9966
  console.log(`[dev-server] Workbench: http://${cfg.storefront ? PD_HOSTS.workbench : "localhost"}:${actualPort}/`);
9280
9967
  }
9281
9968
  const startupDiscovery = await cachedDiscovery(state);
@@ -9320,10 +10007,10 @@ async function startDevServer(config, cwd, opts = {}) {
9320
10007
  import { build as esbuild } from "esbuild";
9321
10008
  import { existsSync as existsSync7 } from "node:fs";
9322
10009
  import { readFile as readFile26, writeFile as writeFile7, unlink, mkdir as mkdir7 } from "node:fs/promises";
9323
- import { basename as basename11, join as join29, dirname as dirname11, resolve as resolve12, relative as relative16, isAbsolute as isAbsolute4 } from "node:path";
10010
+ import { basename as basename11, join as join30, dirname as dirname11, resolve as resolve12, relative as relative16, isAbsolute as isAbsolute4 } from "node:path";
9324
10011
  import { tmpdir as tmpdir2 } from "node:os";
9325
- import { createHash as createHash7, randomUUID as randomUUID3 } from "node:crypto";
9326
- import { pathToFileURL as pathToFileURL2 } from "node:url";
10012
+ import { createHash as createHash8, randomUUID as randomUUID3 } from "node:crypto";
10013
+ import { pathToFileURL as pathToFileURL3 } from "node:url";
9327
10014
  async function runDev(root, opts, configPath) {
9328
10015
  const config = await loadConfig(root, configPath);
9329
10016
  const file = configFileOf(root, configPath);
@@ -9336,6 +10023,7 @@ async function runBuild(root, configPath, options) {
9336
10023
  outDir,
9337
10024
  ...options.base !== void 0 ? { basePath: options.base } : {},
9338
10025
  ...options.audit !== void 0 ? { audit: options.audit } : {},
10026
+ ...options.browser ? { browser: true } : {},
9339
10027
  pdVersion: options.pdVersion,
9340
10028
  log: (line) => console.log(`[pd build] ${line}`)
9341
10029
  });
@@ -9343,16 +10031,24 @@ async function runBuild(root, configPath, options) {
9343
10031
  console.log(`[pd build] ${report.components} components \u2192 ${report.files} files, ${mb} MB in ${report.ms} ms`);
9344
10032
  console.log(`[pd build] ${report.outDir} (served from ${report.basePath})`);
9345
10033
  if (report.storefront) {
9346
- console.log(`[pd build] storefront: ${report.storefront.pages} pages \u2192 ${report.storefront.files} files in ${report.storefront.outDir} (served from /)`);
10034
+ console.log(`[pd build] storefront: ${report.storefront.pages} pages \u2192 ${report.storefront.files} files in ${report.storefront.outDir} (served from ${report.storefront.basePath})`);
9347
10035
  }
9348
10036
  if (report.links) {
9349
10037
  console.log(`[pd build] links: ${report.links.references} references on ${report.links.pages} pages, none broken` + (report.links.external ? `; ${report.links.external} external, not checked (notes)` : ""));
9350
10038
  }
9351
10039
  if (report.accessibility) {
9352
10040
  const n = report.accessibility.warnings.length;
9353
- console.log(`[pd build] accessibility: ${report.accessibility.pages} pages, no errors` + (n ? `; ${n} rule${n === 1 ? "" : "s"} with warnings:` : ""));
10041
+ console.log(`[pd build] accessibility: ${report.accessibility.pages} pages${report.accessibility.browser ? ", in jsdom and Chromium" : ""}, no errors` + (n ? `; ${n} rule${n === 1 ? "" : "s"} with warnings:` : ""));
9354
10042
  for (const w of report.accessibility.warnings) console.log(`[pd build] warning: ${w}`);
9355
10043
  }
10044
+ if (report.images) {
10045
+ const n = report.images.warnings.length;
10046
+ console.log(`[pd build] images: ${n ? `${n} with a variant that doesn't fit:` : "every variant fits its box"}`);
10047
+ for (const w of report.images.warnings) console.log(`[pd build] warning: ${w}`);
10048
+ }
10049
+ if (report.drafts) {
10050
+ console.log(`[pd build] drafts: ${report.drafts.length} left out (${report.drafts.join(", ")})`);
10051
+ }
9356
10052
  for (const w of report.warnings) console.log(`[pd build] not built: ${w}`);
9357
10053
  for (const n of report.notes) console.log(`[pd build] note: ${n}`);
9358
10054
  }
@@ -9361,7 +10057,7 @@ async function runAudit(root, configPath, format = "md") {
9361
10057
  const { graph, reaudited, reused, artifactPath, imageWarning } = await runDiagnosticsAudit(config, root);
9362
10058
  const report = formatAudit(graph, format);
9363
10059
  const ext = format === "json" ? "json" : format === "text" ? "txt" : "md";
9364
- const reportPath = join29(dirname11(artifactPath), `diagnostics-report.${ext}`);
10060
+ const reportPath = join30(dirname11(artifactPath), `diagnostics-report.${ext}`);
9365
10061
  await writeFile7(reportPath, report, "utf-8");
9366
10062
  console.log(report);
9367
10063
  const meta = format === "json" ? console.error : console.log;
@@ -9379,8 +10075,8 @@ function auditSummaryLines(r) {
9379
10075
  ];
9380
10076
  }
9381
10077
  function defaultExportDir(root) {
9382
- const key = createHash7("sha1").update(resolve12(root)).digest("hex").slice(0, 16);
9383
- return join29(tmpdir2(), "pattern-dump", key, "export");
10078
+ const key = createHash8("sha1").update(resolve12(root)).digest("hex").slice(0, 16);
10079
+ return join30(tmpdir2(), "pattern-dump", key, "export");
9384
10080
  }
9385
10081
  function assertOutsideRoot(outDir, root) {
9386
10082
  const rel = relative16(resolve12(root), resolve12(outDir));
@@ -9457,18 +10153,30 @@ async function loadConfig(root, configPath) {
9457
10153
  if (!existsSync7(configPath)) {
9458
10154
  throw new ConfigError(`--config file not found: ${configPath}`);
9459
10155
  }
9460
- return validateConfig(await readConfigFile(configPath), configPath);
10156
+ return validateConfig(await withRootManifest(root, await readConfigFile(configPath), configPath), configPath);
9461
10157
  }
9462
10158
  const path = configFileOf(root);
9463
- if (path) return validateConfig(await readConfigFile(path), basename11(path));
10159
+ if (path) return validateConfig(await withRootManifest(root, await readConfigFile(path), basename11(path)), basename11(path));
9464
10160
  throw new ConfigError(
9465
10161
  `No pattern-dump.config.ts or pattern-dump.config.json found in ${root} (and no --config given)`
9466
10162
  );
9467
10163
  }
10164
+ async function withRootManifest(root, raw, configName) {
10165
+ const pkgPath = join30(root, "package.json");
10166
+ if (!existsSync7(pkgPath)) return raw;
10167
+ let pkg;
10168
+ try {
10169
+ pkg = JSON.parse(await readFile26(pkgPath, "utf-8"));
10170
+ } catch {
10171
+ return raw;
10172
+ }
10173
+ const manifest = typeof pkg === "object" && pkg !== null ? pkg.patternDump : void 0;
10174
+ return manifest === void 0 ? raw : withOwnManifest(raw, manifest, { config: configName, manifest: "package.json" });
10175
+ }
9468
10176
  function configFileOf(root, configPath) {
9469
10177
  if (configPath) return configPath;
9470
10178
  for (const name of CONFIG_NAMES) {
9471
- const path = join29(root, name);
10179
+ const path = join30(root, name);
9472
10180
  if (existsSync7(path)) return path;
9473
10181
  }
9474
10182
  return void 0;
@@ -9478,7 +10186,7 @@ async function readConfigFile(path) {
9478
10186
  return path.endsWith(".json") ? JSON.parse(await readFile26(path, "utf-8")) : loadTsConfig(path);
9479
10187
  }
9480
10188
  async function loadTsConfig(tsPath) {
9481
- const tmp = join29(tmpdir2(), `pd-config-${process.pid}-${randomUUID3()}.mjs`);
10189
+ const tmp = join30(tmpdir2(), `pd-config-${process.pid}-${randomUUID3()}.mjs`);
9482
10190
  try {
9483
10191
  await esbuild({
9484
10192
  entryPoints: [tsPath],
@@ -9488,7 +10196,7 @@ async function loadTsConfig(tsPath) {
9488
10196
  // No packages:"external" — the temp file runs from /tmp so we must inline deps.
9489
10197
  outfile: tmp
9490
10198
  });
9491
- const { default: cfg } = await import(pathToFileURL2(tmp).href);
10199
+ const { default: cfg } = await import(pathToFileURL3(tmp).href);
9492
10200
  return cfg;
9493
10201
  } finally {
9494
10202
  await unlink(tmp).catch(() => {
@@ -9568,7 +10276,8 @@ function parseCommand(argv, cwd) {
9568
10276
  out: { type: "string" },
9569
10277
  base: { type: "string" },
9570
10278
  audit: { type: "boolean" },
9571
- "no-audit": { type: "boolean" }
10279
+ "no-audit": { type: "boolean" },
10280
+ browser: { type: "boolean" }
9572
10281
  }
9573
10282
  });
9574
10283
  if (values2.help) return { kind: "help", topic: cmd };
@@ -9579,7 +10288,8 @@ function parseCommand(argv, cwd) {
9579
10288
  ...base(values2, cwd),
9580
10289
  ...values2.out !== void 0 ? { out: values2.out } : {},
9581
10290
  ...values2.base !== void 0 ? { base: values2.base } : {},
9582
- ...audit !== void 0 ? { audit } : {}
10291
+ ...audit !== void 0 ? { audit } : {},
10292
+ ...values2.browser ? { browser: true } : {}
9583
10293
  };
9584
10294
  }
9585
10295
  const { values } = parseArgs({
@@ -9676,6 +10386,9 @@ var USAGE = `Usage: pd <command> [options]
9676
10386
  --config <path> Use a config file outside the root
9677
10387
  --out <dir> Output folder, relative to the root (default: build.outDir, outDir, or dist)
9678
10388
  --base <path> Where it will be hosted: / (default) or a subpath such as /docs/
10389
+ --browser Also check the storefront's pages in Chromium: contrast and the accessibility
10390
+ rules that need a rendered page, and each image's variant against its size.
10391
+ Uses the project's own Playwright (npm install --save-dev playwright)
9679
10392
  --no-audit Leave the corpus audit out: it is not run, and the site has no Audit tab
9680
10393
  (default: build.audit in the config; --audit forces it back in)
9681
10394
 
@@ -9689,12 +10402,12 @@ var USAGE = `Usage: pd <command> [options]
9689
10402
  // packages/cli/src/init/run.ts
9690
10403
  import { existsSync as existsSync8 } from "node:fs";
9691
10404
  import { readFile as readFile28 } from "node:fs/promises";
9692
- import { basename as basename12, dirname as dirname13, join as join31, relative as relative18, resolve as resolve14 } from "node:path";
10405
+ import { basename as basename12, dirname as dirname13, join as join32, relative as relative18, resolve as resolve14 } from "node:path";
9693
10406
 
9694
10407
  // packages/cli/package.json
9695
10408
  var package_default = {
9696
10409
  name: "@pattern-dump/cli",
9697
- version: "0.3.0",
10410
+ version: "0.4.0",
9698
10411
  private: true,
9699
10412
  description: "The pd command and the published pattern-dump package.",
9700
10413
  type: "module",
@@ -9730,12 +10443,12 @@ var PD_VERSION = package_default.version;
9730
10443
 
9731
10444
  // packages/cli/src/init/files.ts
9732
10445
  import { mkdir as mkdir8, readdir as readdir10, readFile as readFile27, writeFile as writeFile8 } from "node:fs/promises";
9733
- import { dirname as dirname12, join as join30, relative as relative17, sep as sep12 } from "node:path";
10446
+ import { dirname as dirname12, join as join31, relative as relative17, sep as sep12 } from "node:path";
9734
10447
  async function readTemplateDir(dir) {
9735
10448
  const out = [];
9736
10449
  async function walk(current) {
9737
10450
  for (const entry of await readdir10(current, { withFileTypes: true })) {
9738
- const full = join30(current, entry.name);
10451
+ const full = join31(current, entry.name);
9739
10452
  if (entry.isDirectory()) await walk(full);
9740
10453
  else out.push({ path: relative17(dir, full).split(sep12).join("/"), content: await readFile27(full, "utf8") });
9741
10454
  }
@@ -9744,11 +10457,11 @@ async function readTemplateDir(dir) {
9744
10457
  return out.sort((a, b) => a.path.localeCompare(b.path));
9745
10458
  }
9746
10459
  async function readTemplates(templatesDir) {
9747
- const fill = (name) => readFile27(join30(templatesDir, "storefront-fills", name), "utf8");
10460
+ const fill = (name) => readFile27(join31(templatesDir, "storefront-fills", name), "utf8");
9748
10461
  const [base2, examples, storefront, config, readme, gitignore] = await Promise.all([
9749
- readTemplateDir(join30(templatesDir, "base")),
9750
- readTemplateDir(join30(templatesDir, "examples")),
9751
- readTemplateDir(join30(templatesDir, "storefront")),
10462
+ readTemplateDir(join31(templatesDir, "base")),
10463
+ readTemplateDir(join31(templatesDir, "examples")),
10464
+ readTemplateDir(join31(templatesDir, "storefront")),
9752
10465
  fill("config.txt"),
9753
10466
  fill("readme.md"),
9754
10467
  fill("gitignore.txt")
@@ -9758,12 +10471,12 @@ async function readTemplates(templatesDir) {
9758
10471
  async function applyPlan(target, plan) {
9759
10472
  await mkdir8(target, { recursive: true });
9760
10473
  for (const file of plan.writes) {
9761
- const abs = join30(target, file.path);
10474
+ const abs = join31(target, file.path);
9762
10475
  await mkdir8(dirname12(abs), { recursive: true });
9763
10476
  await writeFile8(abs, file.content, { flag: "wx" });
9764
10477
  }
9765
- await writeFile8(join30(target, "package.json"), plan.packageJson.content);
9766
- for (const dir of ["assets", "src/patterns"]) await mkdir8(join30(target, dir), { recursive: true });
10478
+ await writeFile8(join31(target, "package.json"), plan.packageJson.content);
10479
+ for (const dir of ["assets", "src/patterns"]) await mkdir8(join31(target, dir), { recursive: true });
9767
10480
  }
9768
10481
 
9769
10482
  // packages/cli/src/init/plan.ts
@@ -9885,7 +10598,7 @@ async function runInit(cmd, io) {
9885
10598
  } else {
9886
10599
  dependency = dependencySpec(void 0, io.cwd);
9887
10600
  }
9888
- const pkgPath = join31(target, "package.json");
10601
+ const pkgPath = join32(target, "package.json");
9889
10602
  let packageJson = null;
9890
10603
  if (existsSync8(pkgPath)) {
9891
10604
  try {
@@ -9906,7 +10619,7 @@ async function runInit(cmd, io) {
9906
10619
  const install = cmd.install ?? (prompt ? await io.confirm("Run npm install", true) : true);
9907
10620
  const answers = { name, brand, examples, storefront, dependency };
9908
10621
  const templates = await readTemplates(io.templatesDir);
9909
- const result = planInit(templates, answers, { packageJson, exists: (p) => existsSync8(join31(target, p)) }, PD_PACKAGE_NAME);
10622
+ const result = planInit(templates, answers, { packageJson, exists: (p) => existsSync8(join32(target, p)) }, PD_PACKAGE_NAME);
9910
10623
  if (!result.ok) {
9911
10624
  io.log(`\u2717 ${result.reason}`);
9912
10625
  return 1;
@@ -9955,7 +10668,7 @@ async function runInit(cmd, io) {
9955
10668
 
9956
10669
  // packages/cli/src/init/local-build.ts
9957
10670
  import { existsSync as existsSync9, statSync as statSync5 } from "node:fs";
9958
- import { join as join32 } from "node:path";
10671
+ import { join as join33 } from "node:path";
9959
10672
  import { fileURLToPath as fileURLToPath7 } from "node:url";
9960
10673
  var BUILT = [
9961
10674
  "packages/cli/dist/index.js",
@@ -9973,10 +10686,10 @@ var mtime = (path) => {
9973
10686
  };
9974
10687
  function detectLocalBuild(moduleUrl, packageName, version) {
9975
10688
  const repo = fileURLToPath7(new URL("../../../", moduleUrl)).replace(/[\\/]$/, "");
9976
- if (!existsSync9(join32(repo, "packages/cli/src/distribution.ts"))) return null;
9977
- const tarball = join32(repo, ".pd-pack", `${packageName}-${version}.tgz`);
10689
+ if (!existsSync9(join33(repo, "packages/cli/src/distribution.ts"))) return null;
10690
+ const tarball = join33(repo, ".pd-pack", `${packageName}-${version}.tgz`);
9978
10691
  if (!existsSync9(tarball)) return { repo, tarball: null, stale: false };
9979
- const newestBuild = Math.max(...BUILT.map((p) => mtime(join32(repo, p))));
10692
+ const newestBuild = Math.max(...BUILT.map((p) => mtime(join33(repo, p))));
9980
10693
  return { repo, tarball, stale: newestBuild > mtime(tarball) };
9981
10694
  }
9982
10695
 
@@ -10043,6 +10756,7 @@ switch (command.kind) {
10043
10756
  ...command.out !== void 0 ? { out: command.out } : {},
10044
10757
  ...command.base !== void 0 ? { base: command.base } : {},
10045
10758
  ...command.audit !== void 0 ? { audit: command.audit } : {},
10759
+ ...command.browser ? { browser: true } : {},
10046
10760
  pdVersion: PD_VERSION
10047
10761
  }).then(() => process.stdout.write("", () => process.exit(0))).catch((e) => {
10048
10762
  console.error("[pd] Error:", e instanceof Error ? e.message : e);