pattern-dump 0.2.0-alpha.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-ESMZAOP2.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 = {
@@ -335,9 +337,9 @@ async function buildDiscoveryMap(roots) {
335
337
  const dir = dirname(absPath);
336
338
  const stem = basename(absPath, ".hbs");
337
339
  const fileSet = dirFileSets.get(dir) ?? /* @__PURE__ */ new Set();
338
- const source = rootOf.get(absPath)?.root.source;
340
+ const source2 = rootOf.get(absPath)?.root.source;
339
341
  return {
340
- ...source ? { source } : {},
342
+ ...source2 ? { source: source2 } : {},
341
343
  hbsPath: absPath,
342
344
  jsonPath: fileSet.has(`${stem}.json`) ? join(dir, `${stem}.json`) : void 0,
343
345
  mdPath: fileSet.has(`${stem}.md`) ? join(dir, `${stem}.md`) : void 0,
@@ -395,8 +397,8 @@ async function buildDiscoveryMap(roots) {
395
397
  const baseName = basename(varPaths[0], ".hbs").split("~")[0];
396
398
  const fileSet = dirFileSets.get(varDir) ?? /* @__PURE__ */ new Set();
397
399
  const mdPath = fileSet.has(`${baseName}.md`) ? join(varDir, `${baseName}.md`) : void 0;
398
- const source = rootOf.get(varPaths[0])?.root.source;
399
- discoveryMap.set(baseId, { ...source ? { source } : {}, hbsPath: "", ownMarkup: false, variants: variantTokens, mdPath });
400
+ const source2 = rootOf.get(varPaths[0])?.root.source;
401
+ discoveryMap.set(baseId, { ...source2 ? { source: source2 } : {}, hbsPath: "", ownMarkup: false, variants: variantTokens, mdPath });
400
402
  handleMap.set(handle, baseId);
401
403
  }
402
404
  }
@@ -426,8 +428,8 @@ async function buildDiscoveryMap(roots) {
426
428
  if (!baseEntry?.hbsPath) continue;
427
429
  const variantHandle = `${deriveHandle(baseId)}~${variantToken}`;
428
430
  if (!handleMap.has(variantHandle)) {
429
- const source = rootOf.get(jsonPath)?.root.source;
430
- discoveryMap.set(variantId, { ...source ? { source } : {}, hbsPath: baseEntry.hbsPath, jsonPath, ownMarkup: false });
431
+ const source2 = rootOf.get(jsonPath)?.root.source;
432
+ discoveryMap.set(variantId, { ...source2 ? { source: source2 } : {}, hbsPath: baseEntry.hbsPath, jsonPath, ownMarkup: false });
431
433
  handleMap.set(variantHandle, variantId);
432
434
  }
433
435
  }
@@ -899,21 +901,82 @@ 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
- for (const source of sources) {
915
- if (!source) continue;
916
- for (const [key, value] of Object.entries(source)) {
977
+ for (const source2 of sources) {
978
+ if (!source2) continue;
979
+ for (const [key, value] of Object.entries(source2)) {
917
980
  const targetVal = target[key];
918
981
  if (value !== null && typeof value === "object" && !Array.isArray(value) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
919
982
  deepMerge(targetVal, value);
@@ -955,16 +1018,16 @@ async function readDataFile(path, display) {
955
1018
  };
956
1019
  }
957
1020
  }
958
- function imageTag(source, o) {
959
- const esc = Handlebars3.escapeExpression;
1021
+ function imageTag(source2, o) {
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
- if (source.kind === "single") return `<img src="${esc(source.url)}" width="${source.width}" height="${source.height}" ${rest}>`;
962
- const widths = o.widths.filter((w) => w <= source.width);
963
- if (widths.length === 0) widths.push(source.width);
964
- const urls = widths.map((w) => source.variantUrl(w, o.settings));
1024
+ if (source2.kind === "single") return `<img src="${esc(source2.url)}" width="${source2.width}" height="${source2.height}" ${rest}>`;
1025
+ const widths = o.widths.filter((w) => w <= source2.width);
1026
+ if (widths.length === 0) widths.push(source2.width);
1027
+ const urls = widths.map((w) => source2.variantUrl(w, o.settings));
965
1028
  const top = widths[widths.length - 1];
966
1029
  const srcset = urls.map((u, i) => `${esc(u)} ${widths[i]}w`).join(", ");
967
- return `<img src="${esc(urls[urls.length - 1])}" srcset="${srcset}" sizes="${esc(o.sizes)}" width="${top}" height="${Math.round(top * source.height / source.width)}" ${rest}>`;
1030
+ return `<img src="${esc(urls[urls.length - 1])}" srcset="${srcset}" sizes="${esc(o.sizes)}" width="${top}" height="${Math.round(top * source2.height / source2.width)}" ${rest}>`;
968
1031
  }
969
1032
  function registerImageHelper(env, images) {
970
1033
  env.registerHelper("pd-image", function(value, options) {
@@ -979,11 +1042,11 @@ function registerImageHelper(env, images) {
979
1042
  }
980
1043
  if (typeof value !== "string" || value === "") throw new Error(`{{pd-image}}: the image field is empty`);
981
1044
  if (!images) throw new Error(`{{pd-image}}: this renderer was given no image index`);
982
- const source = images(value);
983
- if (!source) throw new Error(`{{pd-image}}: "${value}" is not an image in the assets`);
984
- if (source.kind === "unavailable") throw new Error(source.reason);
1045
+ const source2 = images(value);
1046
+ if (!source2) throw new Error(`{{pd-image}}: "${value}" is not an image in the assets`);
1047
+ if (source2.kind === "unavailable") throw new Error(source2.reason);
985
1048
  const settings = options.data?.pdImages;
986
- return new env.SafeString(imageTag(source, {
1049
+ return new env.SafeString(imageTag(source2, {
987
1050
  widths: slot.widths,
988
1051
  sizes: slot.sizes,
989
1052
  alt: String(alt),
@@ -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
  };
@@ -1373,21 +1446,21 @@ function loadSharp(importer) {
1373
1446
  if (!importer) loaded = attempt;
1374
1447
  return attempt;
1375
1448
  }
1376
- function imageVariantKey(source, width, encoding) {
1377
- return imageVariantKeyFromSourceHash(imageSourceHash(source), width, encoding);
1449
+ function imageVariantKey(source2, width, encoding) {
1450
+ return imageVariantKeyFromSourceHash(imageSourceHash(source2), width, encoding);
1378
1451
  }
1379
- function imageSourceHash(source) {
1380
- return createHash("sha256").update(source).digest("hex");
1452
+ function imageSourceHash(source2) {
1453
+ return createHash("sha256").update(source2).digest("hex");
1381
1454
  }
1382
1455
  function imageVariantKeyFromSourceHash(sourceHash, width, encoding) {
1383
1456
  return createHash("sha256").update(`${sourceHash}|webp|orient=auto|w=${width}|q=${encoding.quality}|e=${encoding.effort}|sharp=${SHARP_VERSION}`).digest("hex").slice(0, 10);
1384
1457
  }
1385
- async function imageSize(sharp, source) {
1386
- const meta = await sharp(source).metadata();
1458
+ async function imageSize(sharp, source2) {
1459
+ const meta = await sharp(source2).metadata();
1387
1460
  return { width: meta.autoOrient?.width ?? meta.width ?? 0, height: meta.autoOrient?.height ?? meta.height ?? 0 };
1388
1461
  }
1389
- function encodeWebp(sharp, source, width, encoding) {
1390
- return sharp(source, { autoOrient: true }).resize({ width, withoutEnlargement: true }).webp({ quality: encoding.quality, effort: encoding.effort }).toBuffer();
1462
+ function encodeWebp(sharp, source2, width, encoding) {
1463
+ return sharp(source2, { autoOrient: true }).resize({ width, withoutEnlargement: true }).webp({ quality: encoding.quality, effort: encoding.effort }).toBuffer();
1391
1464
  }
1392
1465
  function createImageCache(dir) {
1393
1466
  const file = (key) => join2(dir, `${key}.webp`);
@@ -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;
@@ -2960,6 +3041,165 @@ async function runA11yProducer(html) {
2960
3041
  return diagnostics;
2961
3042
  }
2962
3043
 
3044
+ // packages/a11y/src/page.ts
3045
+ import { readFileSync as readFileSync2 } from "node:fs";
3046
+ import { createRequire as createRequire3 } from "node:module";
3047
+ import { JSDOM as JSDOM2 } from "jsdom";
3048
+
3049
+ // packages/a11y/src/page-rule-tiers.generated.json
3050
+ var page_rule_tiers_generated_default = {
3051
+ _note: "Generated by packages/a11y/src/page-classifier.ts \u2014 do not hand-edit. Re-run: npm run classify-pages --workspace @pattern-dump/a11y",
3052
+ axeVersion: "4.11.4",
3053
+ tags: [
3054
+ "wcag2a",
3055
+ "wcag2aa",
3056
+ "wcag21a",
3057
+ "wcag21aa",
3058
+ "wcag22aa",
3059
+ "best-practice"
3060
+ ],
3061
+ jsdom: [
3062
+ "accesskeys",
3063
+ "area-alt",
3064
+ "aria-allowed-attr",
3065
+ "aria-allowed-role",
3066
+ "aria-command-name",
3067
+ "aria-conditional-attr",
3068
+ "aria-deprecated-role",
3069
+ "aria-dialog-name",
3070
+ "aria-hidden-body",
3071
+ "aria-input-field-name",
3072
+ "aria-meter-name",
3073
+ "aria-progressbar-name",
3074
+ "aria-required-attr",
3075
+ "aria-required-children",
3076
+ "aria-required-parent",
3077
+ "aria-roledescription",
3078
+ "aria-roles",
3079
+ "aria-text",
3080
+ "aria-toggle-field-name",
3081
+ "aria-tooltip-name",
3082
+ "aria-treeitem-name",
3083
+ "aria-valid-attr",
3084
+ "aria-valid-attr-value",
3085
+ "autocomplete-valid",
3086
+ "blink",
3087
+ "button-name",
3088
+ "definition-list",
3089
+ "dlitem",
3090
+ "document-title",
3091
+ "empty-heading",
3092
+ "empty-table-header",
3093
+ "frame-title",
3094
+ "heading-order",
3095
+ "html-has-lang",
3096
+ "html-lang-valid",
3097
+ "html-xml-lang-mismatch",
3098
+ "image-alt",
3099
+ "image-redundant-alt",
3100
+ "input-button-name",
3101
+ "input-image-alt",
3102
+ "label",
3103
+ "label-title-only",
3104
+ "landmark-banner-is-top-level",
3105
+ "landmark-contentinfo-is-top-level",
3106
+ "landmark-main-is-top-level",
3107
+ "landmark-no-duplicate-banner",
3108
+ "landmark-no-duplicate-contentinfo",
3109
+ "landmark-no-duplicate-main",
3110
+ "landmark-unique",
3111
+ "link-name",
3112
+ "list",
3113
+ "listitem",
3114
+ "meta-refresh",
3115
+ "meta-viewport",
3116
+ "nested-interactive",
3117
+ "presentation-role-conflict",
3118
+ "region",
3119
+ "role-img-alt",
3120
+ "scope-attr-valid",
3121
+ "select-name",
3122
+ "summary-name",
3123
+ "svg-img-alt",
3124
+ "tabindex",
3125
+ "table-duplicate-name",
3126
+ "td-headers-attr",
3127
+ "valid-lang"
3128
+ ],
3129
+ browser: [
3130
+ "aria-braille-equivalent",
3131
+ "aria-hidden-focus",
3132
+ "aria-prohibited-attr",
3133
+ "audio-caption",
3134
+ "avoid-inline-spacing",
3135
+ "bypass",
3136
+ "color-contrast",
3137
+ "css-orientation-lock",
3138
+ "duplicate-id-aria",
3139
+ "focus-order-semantics",
3140
+ "form-field-multiple-labels",
3141
+ "frame-focusable-content",
3142
+ "frame-tested",
3143
+ "frame-title-unique",
3144
+ "hidden-content",
3145
+ "label-content-name-mismatch",
3146
+ "landmark-complementary-is-top-level",
3147
+ "landmark-one-main",
3148
+ "link-in-text-block",
3149
+ "marquee",
3150
+ "meta-viewport-large",
3151
+ "no-autoplay-audio",
3152
+ "object-alt",
3153
+ "p-as-heading",
3154
+ "page-has-heading-one",
3155
+ "scrollable-region-focusable",
3156
+ "server-side-image-map",
3157
+ "skip-link",
3158
+ "table-fake-caption",
3159
+ "target-size",
3160
+ "td-has-header",
3161
+ "th-has-data-cells",
3162
+ "video-caption"
3163
+ ]
3164
+ };
3165
+
3166
+ // packages/a11y/src/page.ts
3167
+ var require4 = createRequire3(import.meta.url);
3168
+ var axeSource;
3169
+ var source = () => axeSource ??= readFileSync2(require4.resolve("axe-core/axe.min.js"), "utf8");
3170
+ var axeScript = () => source();
3171
+ var PAGE_TAGS = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa", "best-practice"];
3172
+ async function runAxeOnPage(html, rules) {
3173
+ const dom = new JSDOM2(html, { url: "http://site.invalid/", runScripts: "outside-only", pretendToBeVisual: true });
3174
+ dom.virtualConsole.removeAllListeners();
3175
+ try {
3176
+ dom.window.eval(source());
3177
+ const axe2 = dom.window.axe;
3178
+ const ruleIds = axe2.getRules(PAGE_TAGS).map((r2) => r2.ruleId);
3179
+ const options = rules.length ? { runOnly: { type: "rule", values: rules } } : { runOnly: { type: "tag", values: PAGE_TAGS } };
3180
+ const r = await axe2.run(dom.window.document, { ...options, resultTypes: ["violations", "incomplete"] });
3181
+ return { violations: r.violations, incomplete: r.incomplete, axeVersion: axe2.version, ruleIds };
3182
+ } finally {
3183
+ dom.window.close();
3184
+ }
3185
+ }
3186
+ function severityOf(impact, tags) {
3187
+ const wcag = tags.some((t) => /^wcag\d/.test(t));
3188
+ return wcag && (impact === "critical" || impact === "serious") ? "error" : "warning";
3189
+ }
3190
+ async function runPageA11y(html) {
3191
+ const { violations } = await runAxeOnPage(html, page_rule_tiers_generated_default.jsdom);
3192
+ return violations.map((v) => ({
3193
+ ruleId: v.id,
3194
+ severity: severityOf(v.impact, v.tags),
3195
+ impact: v.impact ?? "minor",
3196
+ help: v.help,
3197
+ helpUrl: v.helpUrl,
3198
+ nodes: v.nodes.map((n) => ({ target: n.target.map(String).join(" "), html: n.html }))
3199
+ }));
3200
+ }
3201
+ var pageRuleTiers = page_rule_tiers_generated_default;
3202
+
2963
3203
  // packages/dev-server/src/diagnostics.ts
2964
3204
  async function checkComponent(componentId, variant, render, quarantineEntry) {
2965
3205
  if (quarantineEntry) {
@@ -2980,7 +3220,7 @@ async function checkComponent(componentId, variant, render, quarantineEntry) {
2980
3220
  const a11yDiags = await runA11yProducer(previewHtml);
2981
3221
  return { previewHtml, diagnostics: [...renderDiags, ...a11yDiags] };
2982
3222
  } catch (e) {
2983
- 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;
2984
3224
  return {
2985
3225
  previewHtml: "",
2986
3226
  diagnostics: [{
@@ -2988,7 +3228,7 @@ async function checkComponent(componentId, variant, render, quarantineEntry) {
2988
3228
  severity: "error",
2989
3229
  source: "render",
2990
3230
  message: e instanceof Error ? e.message : String(e),
2991
- ...location && Object.keys(location).length ? { location } : {}
3231
+ ...location2 && Object.keys(location2).length ? { location: location2 } : {}
2992
3232
  }]
2993
3233
  };
2994
3234
  }
@@ -3075,7 +3315,8 @@ async function componentContentHash(id, discoveryMap, globalDataDigest2, checkCo
3075
3315
  const entry = discoveryMap.get(id);
3076
3316
  const hbs = entry?.hbsPath ? await readFile8(entry.hbsPath, "utf-8").catch(() => "") : "";
3077
3317
  const json2 = entry?.jsonPath ? await readFile8(entry.jsonPath, "utf-8").catch(() => "") : "";
3078
- 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");
3079
3320
  }
3080
3321
  function globalDataDigest(globalData) {
3081
3322
  return createHash4("sha1").update(JSON.stringify(globalData ?? {})).digest("hex");
@@ -3119,7 +3360,7 @@ async function persistSlices(root, slices) {
3119
3360
  }
3120
3361
 
3121
3362
  // packages/dev-server/src/packages.ts
3122
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
3363
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
3123
3364
  import { dirname as dirname5, join as join13, sep as sep3 } from "node:path";
3124
3365
  function findPackageDir(name, root) {
3125
3366
  let dir = root;
@@ -3164,7 +3405,7 @@ function loadPatternPackages(references, root) {
3164
3405
  throw new Error(`pattern package "${name}" is not installed \u2014 run: npm install --save-dev ${name}`);
3165
3406
  }
3166
3407
  const dir = realpathSync2(via);
3167
- const pkg = JSON.parse(readFileSync2(join13(dir, "package.json"), "utf8"));
3408
+ const pkg = JSON.parse(readFileSync3(join13(dir, "package.json"), "utf8"));
3168
3409
  if (!pkg.patternDump) {
3169
3410
  if (!direct) return;
3170
3411
  throw new Error(
@@ -3289,8 +3530,8 @@ async function runDiagnosticsAudit(config, cwd) {
3289
3530
  }
3290
3531
 
3291
3532
  // packages/dev-server/src/static-build.ts
3292
- import { cp, mkdir as mkdir5, readdir as readdir7, rm as rm3, stat as stat4, writeFile as writeFile5 } from "node:fs/promises";
3293
- 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";
3294
3535
 
3295
3536
  // packages/dev-server/src/http-utils.ts
3296
3537
  import { readFile as readFile9 } from "node:fs/promises";
@@ -3411,12 +3652,12 @@ function imageCacheDir(cwd) {
3411
3652
  }
3412
3653
  var caches = /* @__PURE__ */ new WeakMap();
3413
3654
  var cacheFor = (cfg) => {
3414
- let cache = caches.get(cfg);
3415
- if (!cache) {
3416
- cache = createImageCache(imageCacheDir(cfg.cwd));
3417
- caches.set(cfg, cache);
3655
+ let cache2 = caches.get(cfg);
3656
+ if (!cache2) {
3657
+ cache2 = createImageCache(imageCacheDir(cfg.cwd));
3658
+ caches.set(cfg, cache2);
3418
3659
  }
3419
- return cache;
3660
+ return cache2;
3420
3661
  };
3421
3662
  async function variantSource(name, hash, width, encoding, cfg) {
3422
3663
  const base2 = PD_ROUTE_PREFIX.assets + name.split("/").map(encodeURIComponent).join("/");
@@ -3441,8 +3682,8 @@ async function serveImageVariant(pathname, host, res, cfg) {
3441
3682
  const variant = parseImageUrl(pathname);
3442
3683
  if (!variant) return notFound();
3443
3684
  const encoding = cfg.storefront && host === cfg.storefront.host ? cfg.storefront.images : cfg.images;
3444
- const source = await variantSource(variant.name, variant.hash, variant.width, encoding, cfg);
3445
- if (!source) return notFound();
3685
+ const source2 = await variantSource(variant.name, variant.hash, variant.width, encoding, cfg);
3686
+ if (!source2) return notFound();
3446
3687
  let sharp;
3447
3688
  try {
3448
3689
  sharp = await loadSharp();
@@ -3453,12 +3694,12 @@ async function serveImageVariant(pathname, host, res, cfg) {
3453
3694
  res.end(e.message);
3454
3695
  return;
3455
3696
  }
3456
- if (variant.width > (await imageSize(sharp, source)).width) return notFound();
3457
- const cache = cacheFor(cfg);
3458
- let bytes = await cache.read(variant.hash);
3697
+ if (variant.width > (await imageSize(sharp, source2)).width) return notFound();
3698
+ const cache2 = cacheFor(cfg);
3699
+ let bytes = await cache2.read(variant.hash);
3459
3700
  if (!bytes) {
3460
- bytes = await encodeWebp(sharp, source, variant.width, encoding);
3461
- await cache.write(variant.hash, bytes);
3701
+ bytes = await encodeWebp(sharp, source2, variant.width, encoding);
3702
+ await cache2.write(variant.hash, bytes);
3462
3703
  }
3463
3704
  res.writeHead(200, { "Content-Type": "image/webp", "Cache-Control": "public, max-age=31536000, immutable" });
3464
3705
  res.end(bytes);
@@ -3524,156 +3765,602 @@ function createSnapshotWriter({ outDir, basePath, origin, report, fetch: get = (
3524
3765
  return { capture, store, failures };
3525
3766
  }
3526
3767
 
3527
- // packages/dev-server/src/storefront/crawl.ts
3528
- import { request } from "node:http";
3529
- function hostTransport(port2, host) {
3530
- return (liveUrl) => new Promise((resolve15, reject) => {
3531
- const req = request({ host: "127.0.0.1", port: port2, path: liveUrl, headers: { host: `${host}:${port2}` } }, (res) => {
3532
- const chunks = [];
3533
- res.on("data", (c3) => chunks.push(c3));
3534
- res.on("end", () => {
3535
- const headers = new Headers();
3536
- for (const [k, v] of Object.entries(res.headers)) if (typeof v === "string") headers.set(k, v);
3537
- resolve15(new Response(Buffer.concat(chunks), { status: res.statusCode ?? 0, headers }));
3538
- });
3539
- });
3540
- req.on("error", reject);
3541
- req.end();
3542
- });
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() === "---") };
3543
3787
  }
3544
- var unescapeHtml = (s) => s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, `"`).replace(/&amp;/g, "&");
3545
- var markupRefs = (html) => [
3546
- ...[...html.matchAll(/(?:href|src)="(\/(?:_pd|assets|preview)\/[^"]+)"/g)].map((m) => unescapeHtml(m[1])),
3547
- // Each candidate of a srcset: its URL, before the width or density that describes it.
3548
- ...[...html.matchAll(/srcset="([^"]+)"/g)].flatMap((m) => unescapeHtml(m[1]).split(",").map((c3) => c3.trim().split(/\s+/)[0] ?? "")).filter((u) => /^\/(?:_pd|assets)\//.test(u)),
3549
- ...[...html.matchAll(/<meta property="og:image" content="https?:\/\/[^/"]+(\/assets\/[^"]+)">/g)].map((m) => unescapeHtml(m[1]))
3550
- ];
3551
- var cssRefs = (css) => [...css.matchAll(/url\(\s*(['"]?)(\/(?:_pd|assets)\/[^'")\s]+)\1\s*\)/g)].map((m) => m[2]);
3552
- async function crawlStorefront(get, files = []) {
3553
- const problems = [];
3554
- const siteProblems = /* @__PURE__ */ new Set();
3555
- const list = await get(PD_ROUTES.pages);
3556
- if (list.status !== 200) {
3557
- const body = await list.text();
3558
- let error = body;
3559
- try {
3560
- error = JSON.parse(body).error ?? body;
3561
- } catch {
3562
- }
3563
- 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: {} };
3564
3797
  }
3565
- const { pages } = await list.json();
3566
- const urls = [];
3567
- const documents = /* @__PURE__ */ new Map();
3568
- const seen = /* @__PURE__ */ new Set();
3569
- const queue = [];
3570
- const add = (url) => {
3571
- if (!seen.has(url)) {
3572
- seen.add(url);
3573
- 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;
3574
3819
  }
3575
- };
3576
- for (const p of pages) add(p.route);
3577
- for (const p of pages) for (const old of p.redirectFrom ?? []) add(old);
3578
- for (const f of files) add(f);
3579
- while (queue.length) {
3580
- const url = queue.shift();
3581
- const res = await get(url);
3582
- const type = res.headers.get("content-type") ?? "";
3583
- if (res.status !== (url === PD_ROUTES.notFound ? 404 : 200)) {
3584
- const body = await res.text();
3585
- const items = [...body.matchAll(/<li( data-pd-site)?>([\s\S]*?)<\/li>/g)].map((m) => ({ site: !!m[1], text: unescapeHtml(m[2]) }));
3586
- for (const i of items) {
3587
- if (!i.site) problems.push(`${url}: ${i.text}`);
3588
- else if (!siteProblems.has(i.text)) {
3589
- siteProblems.add(i.text);
3590
- problems.push(i.text);
3591
- }
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()}`);
3592
3826
  }
3593
- if (!items.length) problems.push(`${url}: ${res.status} ${body.split("\n")[0]?.slice(0, 160) ?? ""}`.trimEnd());
3827
+ listKey = null;
3594
3828
  continue;
3595
3829
  }
3596
- urls.push(url);
3597
- if (/text\/html/.test(type)) {
3598
- const html = await res.text();
3599
- documents.set(url, html);
3600
- for (const ref of markupRefs(html)) add(ref);
3601
- } else if (/text\/css/.test(type)) for (const ref of cssRefs(await res.text())) add(ref);
3602
- }
3603
- return { urls, problems, documents };
3604
- }
3605
-
3606
- // packages/dev-server/src/storefront/html-tags.ts
3607
- var MARKUP = /<!--[\s\S]*?-->|<([a-zA-Z][\w:-]*)((?:\s+[^\s"'>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*)\s*\/?>/g;
3608
- var ATTRIBUTE = /(\s+)([^\s"'>/=]+)(?:(\s*=\s*)("[^"]*"|'[^']*'|[^\s"'=<>`]+))?/g;
3609
- var RAW_TEXT = /* @__PURE__ */ new Set(["script", "style", "textarea", "title"]);
3610
- function startTags(html) {
3611
- const tags = [];
3612
- const markup = new RegExp(MARKUP.source, "g");
3613
- const lower = html.toLowerCase();
3614
- for (let m = markup.exec(html); m; m = markup.exec(html)) {
3615
- const name = m[1];
3616
- if (name === void 0) continue;
3617
- const tag = { name, start: m.index, end: markup.lastIndex, attrs: m[2] ?? "" };
3618
- const key = name.toLowerCase();
3619
- if (RAW_TEXT.has(key)) {
3620
- const close = lower.indexOf(`</${key}`, tag.end);
3621
- tag.content = { start: tag.end, end: close < 0 ? html.length : close };
3622
- 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;
3623
3847
  }
3624
- tags.push(tag);
3625
3848
  }
3626
- return tags;
3849
+ return { values: out, errors, lines: keyLines };
3627
3850
  }
3628
- function decodeAttribute(written) {
3629
- const q = written[0] === '"' || written[0] === "'" ? written.slice(1, -1) : written;
3630
- return q.replace(/&(#x[0-9a-f]+|#[0-9]+|amp|lt|gt|quot|apos|#39);/gi, (_, e) => {
3631
- const k = e.toLowerCase();
3632
- if (k.startsWith("#x")) return String.fromCodePoint(parseInt(k.slice(2), 16));
3633
- if (k.startsWith("#")) return String.fromCodePoint(parseInt(k.slice(1), 10));
3634
- return { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" }[k] ?? `&${e};`;
3635
- });
3851
+ function readFrontmatter(md2) {
3852
+ return scan(md2).values;
3636
3853
  }
3637
- function attributesOf(tag) {
3638
- 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);
3639
3856
  }
3640
-
3641
- // packages/dev-server/src/storefront/links.ts
3642
- var SKIPPED = /^(?:mailto|tel|data|javascript):/i;
3643
- var OTHER_HOST = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
3644
- function idsOf(html) {
3645
- const ids = /* @__PURE__ */ new Set();
3646
- for (const tag of startTags(html)) {
3647
- for (const a of attributesOf(tag)) {
3648
- if (a.value === void 0) continue;
3649
- if (a.name === "id" || a.name === "name" && tag.name.toLowerCase() === "a") ids.add(a.value);
3650
- }
3651
- }
3652
- return ids;
3857
+ function frontmatterBodyOffset(md2) {
3858
+ const { end } = block(md2);
3859
+ return end === null || end < 0 ? 0 : end + 1;
3653
3860
  }
3654
- function referencesOf(html) {
3655
- const refs = [];
3656
- for (const tag of startTags(html)) {
3657
- for (const a of attributesOf(tag)) {
3658
- if (a.value === void 0) continue;
3659
- if (a.name === "href" || a.name === "src") refs.push([tag.name.toLowerCase(), a.name, a.value.trim()]);
3660
- else if (a.name === "srcset") {
3661
- for (const c3 of a.value.split(",")) {
3662
- const url = c3.trim().split(/\s+/)[0];
3663
- if (url) refs.push([tag.name.toLowerCase(), a.name, url]);
3664
- }
3665
- }
3666
- }
3667
- }
3668
- 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");
3669
3866
  }
3670
- function linkedWorkbenchRoutes(documents, pages, workbenchUrl) {
3671
- const routes = /* @__PURE__ */ new Set();
3672
- for (const page of pages) {
3673
- for (const [, , ref] of referencesOf(documents.get(page) ?? "")) {
3674
- let rest;
3675
- if (OTHER_HOST.test(workbenchUrl)) {
3676
- if (ref.startsWith(workbenchUrl)) rest = ref.slice(workbenchUrl.length);
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;
3875
+ }
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);
3677
4364
  } else if (!OTHER_HOST.test(ref)) {
3678
4365
  const path = new URL(ref, `http://site.invalid${page}`).pathname;
3679
4366
  if (path.startsWith(workbenchUrl)) rest = path.slice(workbenchUrl.length);
@@ -3710,6 +4397,8 @@ function checkLinks(site) {
3710
4397
  if (hash && !(wb.variants.get(route) ?? []).includes(decodeURIComponent(hash))) return `${route} has no variant "${hash}" in the workbench`;
3711
4398
  return void 0;
3712
4399
  };
4400
+ const base2 = site.basePath ?? "/";
4401
+ const hint = site.pathHint ?? base2;
3713
4402
  let references = 0;
3714
4403
  for (const page of site.pages) {
3715
4404
  const html = site.documents.get(page);
@@ -3725,7 +4414,7 @@ function checkLinks(site) {
3725
4414
  continue;
3726
4415
  }
3727
4416
  let href = ref;
3728
- 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)}`;
3729
4418
  else if (OTHER_HOST.test(ref)) {
3730
4419
  if (site.pagesOnly) continue;
3731
4420
  const list = external.get(ref) ?? [];
@@ -3733,18 +4422,24 @@ function checkLinks(site) {
3733
4422
  external.set(ref, list);
3734
4423
  continue;
3735
4424
  }
3736
- const url = new URL(href, `http://site.invalid${page}`);
3737
- const path = decodeURIComponent(url.pathname);
4425
+ const url = new URL(href, `http://site.invalid${base2}${page.slice(1)}`);
4426
+ const served = decodeURIComponent(url.pathname);
3738
4427
  const hash = url.hash.slice(1);
3739
- if (wb && wb.url.startsWith("/") && path.startsWith(wb.url)) {
3740
- 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);
3741
4430
  if (why) say(why);
3742
4431
  continue;
3743
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)}`;
3744
4438
  if (site.pagesOnly && /^\/(?:_pd|assets|preview)\//.test(path)) continue;
3745
4439
  const target = builtAs(path);
3746
4440
  if (target === void 0) {
3747
- 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}`);
3748
4443
  continue;
3749
4444
  }
3750
4445
  if (!hash) continue;
@@ -3755,6 +4450,151 @@ function checkLinks(site) {
3755
4450
  return { problems, external, references };
3756
4451
  }
3757
4452
 
4453
+ // packages/dev-server/src/storefront/page-basics.ts
4454
+ function checkPageBasics(html) {
4455
+ let main = 0, h1 = 0;
4456
+ for (const tag of startTags(html)) {
4457
+ const name = tag.name.toLowerCase();
4458
+ if (name === "main" || attributesOf(tag).some((a) => a.name === "role" && a.value?.trim() === "main")) main++;
4459
+ if (name === "h1") h1++;
4460
+ }
4461
+ const problems = [];
4462
+ if (main === 0) problems.push("the page has no <main>: the page's own content goes in one, so assistive technology can go straight to it");
4463
+ else if (main > 1) problems.push(`the page has ${main} main landmarks (<main> or role="main"): it has exactly one, holding the page's own content`);
4464
+ if (h1 === 0) problems.push("the page has no <h1>: one heading names the page");
4465
+ else if (h1 > 1) problems.push(`the page has ${h1} <h1> headings: one names the page, the others are <h2> or lower`);
4466
+ return problems;
4467
+ }
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
+
3758
4598
  // packages/dev-server/src/storefront/site-files.ts
3759
4599
  var escapeXml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
3760
4600
  function sitemapXml(entries) {
@@ -3781,6 +4621,68 @@ Sitemap: ${p.siteUrl}sitemap.xml
3781
4621
  ` : ""}`;
3782
4622
  }
3783
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
+
3784
4686
  // packages/dev-server/src/static-build.ts
3785
4687
  var notABarrier = (path) => basename4(path) !== ".__testOnly_barrier";
3786
4688
  async function buildStaticSite(config, root, opts) {
@@ -3793,23 +4695,23 @@ async function buildStaticSite(config, root, opts) {
3793
4695
  const outDir = resolve7(opts.outDir);
3794
4696
  const cfg = resolveServerConfig(config, root, {}, loadPatternPackages(config.packages, root));
3795
4697
  const inside2 = (child, parent) => {
3796
- const rel = relative9(parent, child);
3797
- 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);
3798
4700
  };
3799
4701
  const project = resolve7(root);
3800
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`);
3801
- for (const source of [cfg.patternsDir, cfg.stylesDir, ...cfg.themeDirs, ...cfg.assetRoots]) {
3802
- if (inside2(outDir, source) || inside2(source, outDir)) {
3803
- throw new Error(`pd build: --out ${outDir} overlaps ${source}, which Pattern Dump reads \u2014 choose another folder`);
4703
+ for (const source2 of [cfg.patternsDir, cfg.stylesDir, ...cfg.themeDirs, ...cfg.assetRoots]) {
4704
+ if (inside2(outDir, source2) || inside2(source2, outDir)) {
4705
+ throw new Error(`pd build: --out ${outDir} overlaps ${source2}, which Pattern Dump reads \u2014 choose another folder`);
3804
4706
  }
3805
4707
  }
3806
4708
  const storefront = cfg.storefront;
3807
4709
  if (storefront) {
3808
4710
  const sfOut = storefront.outDir;
3809
4711
  if (inside2(project, sfOut)) throw new Error(`pd build: storefront.outDir ${sfOut} would contain the project itself \u2014 choose a folder inside or beside it`);
3810
- for (const source of [cfg.patternsDir, cfg.stylesDir, storefront.pagesDir, ...cfg.themeDirs, ...cfg.assetRoots]) {
3811
- if (inside2(sfOut, source) || inside2(source, sfOut)) {
3812
- throw new Error(`pd build: storefront.outDir ${sfOut} overlaps ${source}, which Pattern Dump reads \u2014 choose another folder`);
4712
+ for (const source2 of [cfg.patternsDir, cfg.stylesDir, storefront.pagesDir, ...cfg.themeDirs, ...cfg.assetRoots]) {
4713
+ if (inside2(sfOut, source2) || inside2(source2, sfOut)) {
4714
+ throw new Error(`pd build: storefront.outDir ${sfOut} overlaps ${source2}, which Pattern Dump reads \u2014 choose another folder`);
3813
4715
  }
3814
4716
  }
3815
4717
  if (inside2(sfOut, outDir) || inside2(outDir, sfOut)) {
@@ -3823,6 +4725,7 @@ async function buildStaticSite(config, root, opts) {
3823
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}`);
3824
4726
  }
3825
4727
  }
4728
+ const playwright = opts.browser && storefront ? await (opts.loadPlaywright ?? loadProjectPlaywright)(project) : void 0;
3826
4729
  const fixed = opts.now;
3827
4730
  const now = fixed ? () => new Date(fixed.getTime()) : () => /* @__PURE__ */ new Date();
3828
4731
  const storefrontNotes = /* @__PURE__ */ new Set();
@@ -3836,9 +4739,14 @@ async function buildStaticSite(config, root, opts) {
3836
4739
  }
3837
4740
  });
3838
4741
  log("\u2026 server ready");
3839
- const sfGet = storefront ? hostTransport(server.port, storefront.host) : null;
3840
- const sfFiles = storefront ? [PD_ROUTES.robots, PD_ROUTES.notFound, ...storefront.url !== void 0 && storefront.index ? [PD_ROUTES.sitemap] : []] : [];
3841
- 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;
3842
4750
  if (sfCrawl && sfCrawl.problems.length) {
3843
4751
  await server.close();
3844
4752
  throw new Error(`pd build: the storefront has problems, so nothing was written:
@@ -3847,6 +4755,8 @@ ${sfCrawl.problems.map((p) => ` - ${p}`).join("\n")}`);
3847
4755
  if (sfCrawl) log(`\u2026 storefront checked: ${sfCrawl.urls.length} files`);
3848
4756
  const origin = `http://127.0.0.1:${server.port}`;
3849
4757
  let linkSummary;
4758
+ let a11ySummary;
4759
+ let imageSummary;
3850
4760
  const externalNotes = [];
3851
4761
  if (storefront && sfGet && sfCrawl) {
3852
4762
  const { pages: list } = await (await sfGet(PD_ROUTES.pages)).json();
@@ -3874,6 +4784,7 @@ ${sfCrawl.problems.map((p) => ` - ${p}`).join("\n")}`);
3874
4784
  documents: sfCrawl.documents,
3875
4785
  pages,
3876
4786
  ...storefront.url !== void 0 ? { siteUrl: storefront.url.endsWith("/") ? storefront.url : `${storefront.url}/` } : {},
4787
+ basePath: sfBase,
3877
4788
  workbench: { url: wbUrl, routes, variants }
3878
4789
  });
3879
4790
  if (links.problems.length) {
@@ -3884,10 +4795,57 @@ ${links.problems.map((p) => ` - ${p}`).join("\n")}`);
3884
4795
  for (const [url, on] of links.external) externalNotes.push(`an external link, not checked: ${url} (on ${on.join(", ")})`);
3885
4796
  linkSummary = { pages: pages.length, references: links.references, external: links.external.size };
3886
4797
  log(`\u2026 links checked: ${links.references} references on ${pages.length} pages`);
4798
+ const errors = [];
4799
+ const warned = /* @__PURE__ */ new Map();
4800
+ for (const page of pages) {
4801
+ const html = sfCrawl.documents.get(page);
4802
+ if (html === void 0) continue;
4803
+ for (const p of checkPageBasics(html)) errors.push(`${page}: ${p}`);
4804
+ for (const f of await runPageA11y(html)) {
4805
+ const where = f.nodes.map((n) => n.target).join(", ");
4806
+ if (f.severity === "error") {
4807
+ errors.push(`${page}: ${f.ruleId} (${f.impact}): ${f.help}, on ${where} (${f.helpUrl})`);
4808
+ continue;
4809
+ }
4810
+ const w = warned.get(f.ruleId) ?? { finding: f, pages: [], elements: 0 };
4811
+ w.pages.push(page);
4812
+ w.elements += f.nodes.length;
4813
+ warned.set(f.ruleId, w);
4814
+ }
4815
+ }
4816
+ if (errors.length) {
4817
+ await server.close();
4818
+ throw new Error(`pd build: the storefront has accessibility problems, so nothing was written:
4819
+ ${errors.map((e) => ` - ${e}`).join("\n")}`);
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;
4838
+ a11ySummary = {
4839
+ pages: pages.length,
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]
4842
+ };
4843
+ log(`\u2026 accessibility checked on ${pages.length} pages`);
3887
4844
  }
3888
4845
  await rm3(outDir, { recursive: true, force: true });
3889
4846
  await mkdir5(outDir, { recursive: true });
3890
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");
3891
4849
  const { capture, store } = createSnapshotWriter({ outDir, basePath, origin, report });
3892
4850
  const referenced = (html) => [...html.matchAll(/(?:href|src)="(\/_pd\/(?:components|themes|scripts)\/[^"]+)"/g)].map((m) => m[1].replace(/&amp;/g, "&"));
3893
4851
  try {
@@ -3926,9 +4884,9 @@ ${links.problems.map((p) => ` - ${p}`).join("\n")}`);
3926
4884
  for (const variant of [void 0, ...base2.variants.map((v) => v.name)]) {
3927
4885
  const detailText = variant ? await capture(componentUrl(id, variant)) : baseText;
3928
4886
  if (detailText) {
3929
- const { document: document2 } = JSON.parse(detailText);
3930
- await store(previewUrl(id, variant), document2);
3931
- 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);
3932
4890
  }
3933
4891
  await capture(renderedSourceUrl(id, variant), { optional: true });
3934
4892
  await capture(dataSourceUrl(id, variant), { optional: true });
@@ -3950,13 +4908,13 @@ ${links.problems.map((p) => ` - ${p}`).join("\n")}`);
3950
4908
  for (const [i, rootDir] of cfg.assetRoots.entries()) {
3951
4909
  const by = shadowed.get(rootDir);
3952
4910
  if (by !== void 0) {
3953
- 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`);
3954
4912
  continue;
3955
4913
  }
3956
- const exists = await stat4(rootDir).then((s) => s.isDirectory(), () => false);
4914
+ const exists = await stat5(rootDir).then((s) => s.isDirectory(), () => false);
3957
4915
  if (!exists) continue;
3958
- await cp(rootDir, join17(outDir, "assets", cfg.assetRootNames[i]), { recursive: true, filter: notABarrier });
3959
- 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 });
3960
4918
  }
3961
4919
  const runtime = {
3962
4920
  mode: "static",
@@ -3972,12 +4930,12 @@ ${links.problems.map((p) => ` - ${p}`).join("\n")}`);
3972
4930
  report.warnings.push(`shell page for route ${route} not written: ${staticPathFor(route)} already holds the server's response to ${holder}`);
3973
4931
  }
3974
4932
  }
3975
- await writeFile5(join17(outDir, ".nojekyll"), "");
4933
+ await writeFile5(join19(outDir, ".nojekyll"), "");
3976
4934
  report.files++;
3977
- await writeFile5(join17(outDir, "robots.txt"), workbenchRobotsTxt(basePath));
4935
+ await writeFile5(join19(outDir, "robots.txt"), workbenchRobotsTxt(basePath));
3978
4936
  report.files++;
3979
- if (basePath !== "/" && sameHostWorkbenchPath(storefront ?? void 0) !== basePath) {
3980
- 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 /`);
3981
4939
  }
3982
4940
  if (storefront && sfGet && sfCrawl) {
3983
4941
  const sfOut = storefront.outDir;
@@ -3990,21 +4948,34 @@ ${links.problems.map((p) => ` - ${p}`).join("\n")}`);
3990
4948
  throw new Error(`pd build: the storefront changed while it was being written:
3991
4949
  ${sf.failures.map((f) => ` - ${f}`).join("\n")}`);
3992
4950
  }
3993
- await writeFile5(join17(sfOut, ".nojekyll"), "");
4951
+ await writeFile5(join19(sfOut, ".nojekyll"), "");
3994
4952
  counts.files++;
3995
4953
  const stubs = new Set((await (await sfGet(PD_ROUTES.pages)).json()).pages.flatMap((p) => p.redirectFrom ?? []));
3996
4954
  const pages = sfCrawl.urls.filter((u) => !/^\/(?:_pd|assets|preview)\//.test(u) && !sfFiles.includes(u) && !stubs.has(u)).length;
3997
- 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
+ }
3998
4965
  if (storefront.url === void 0) {
3999
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/"`);
4000
4967
  }
4001
4968
  report.notes.push(...storefrontNotes, ...externalNotes);
4002
4969
  if (linkSummary) report.links = linkSummary;
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;
4003
4974
  log(`\u2026 storefront: ${pages} pages, ${counts.files} files`);
4004
4975
  }
4005
4976
  const used = /* @__PURE__ */ new Set();
4006
4977
  for (const root2 of [outDir, ...storefront ? [storefront.outDir] : []]) {
4007
- const files = await readdir7(join17(root2, "_pd", "images"), { recursive: true }).catch(() => []);
4978
+ const files = await readdir8(join19(root2, "_pd", "images"), { recursive: true }).catch(() => []);
4008
4979
  for (const f of files) {
4009
4980
  const m = /\.([0-9a-f]{10})\.[1-9][0-9]*\.webp$/.exec(String(f));
4010
4981
  if (m) used.add(m[1]);
@@ -4019,19 +4990,19 @@ ${sf.failures.map((f) => ` - ${f}`).join("\n")}`);
4019
4990
  }
4020
4991
 
4021
4992
  // packages/dev-server/src/requires.ts
4022
- import { readFile as readFile11 } from "node:fs/promises";
4993
+ import { readFile as readFile12 } from "node:fs/promises";
4023
4994
  import { existsSync as existsSync4 } from "node:fs";
4024
- 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";
4025
4996
  function normalizeScriptId(raw) {
4026
4997
  let s = raw.trim().replace(/^\[(.+)\]$/, "$1").trim();
4027
4998
  s = basename5(s);
4028
4999
  s = s.replace(/\.(ts|js)$/, "");
4029
5000
  return s;
4030
5001
  }
4031
- function parseFrontmatterList(md, key) {
5002
+ function parseFrontmatterList(md2, key) {
4032
5003
  const values = [];
4033
5004
  const re = new RegExp(`^${key}:\\s*(.+)$`, "mg");
4034
- for (const [, val] of md.matchAll(re)) {
5005
+ for (const [, val] of md2.matchAll(re)) {
4035
5006
  values.push(val.trim());
4036
5007
  }
4037
5008
  return values;
@@ -4044,9 +5015,9 @@ async function aggregateRequires(componentId, patternsDir, flatNameToPath, disco
4044
5015
  let ownAssets = [];
4045
5016
  if (entry?.mdPath) {
4046
5017
  try {
4047
- const md = await readFile11(entry.mdPath, "utf-8");
4048
- ownScripts = parseFrontmatterList(md, "requireScript").map(normalizeScriptId);
4049
- 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());
4050
5021
  } catch {
4051
5022
  }
4052
5023
  }
@@ -4055,7 +5026,7 @@ async function aggregateRequires(componentId, patternsDir, flatNameToPath, disco
4055
5026
  const hbsPath = entry?.hbsPath ?? "";
4056
5027
  let hbs = "";
4057
5028
  try {
4058
- hbs = hbsPath ? await readFile11(hbsPath, "utf-8") : "";
5029
+ hbs = hbsPath ? await readFile12(hbsPath, "utf-8") : "";
4059
5030
  } catch {
4060
5031
  }
4061
5032
  for (const flatName of parsePartialsFromHbs(hbs)) {
@@ -4076,7 +5047,7 @@ async function loadSvgAssets(ids, assetsDir) {
4076
5047
  for (const id of ids) {
4077
5048
  for (const candidate of [`${id}.svg`, `images/${id}.svg`]) {
4078
5049
  try {
4079
- const raw = await readFile11(join18(assetsDir, candidate), "utf-8");
5050
+ const raw = await readFile12(join20(assetsDir, candidate), "utf-8");
4080
5051
  contents.push(hideSvg(raw));
4081
5052
  break;
4082
5053
  } catch {
@@ -4089,21 +5060,21 @@ function hideSvg(svg) {
4089
5060
  return svg.replace(/<svg(\s|>)/, '<svg$1style="display:none;position:absolute" aria-hidden="true" ');
4090
5061
  }
4091
5062
  async function reportUnresolvableScripts(discoveryMap, scriptsDirs, globalScripts, cwd) {
4092
- const resolves = (id) => scriptsDirs.some((d) => existsSync4(join18(d, `${id}.ts`)));
4093
- 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";
4094
5065
  for (const id of globalScripts) {
4095
5066
  if (id.includes("://") || resolves(normalizeScriptId(id))) continue;
4096
5067
  console.warn(`[dev-server] globalScripts "${id}": no ${normalizeScriptId(id)}.ts in ${where}`);
4097
5068
  }
4098
5069
  for (const [componentId, entry] of discoveryMap) {
4099
5070
  if (!entry.mdPath) continue;
4100
- let md;
5071
+ let md2;
4101
5072
  try {
4102
- md = await readFile11(entry.mdPath, "utf-8");
5073
+ md2 = await readFile12(entry.mdPath, "utf-8");
4103
5074
  } catch {
4104
5075
  continue;
4105
5076
  }
4106
- for (const raw of parseFrontmatterList(md, "requireScript")) {
5077
+ for (const raw of parseFrontmatterList(md2, "requireScript")) {
4107
5078
  const id = normalizeScriptId(raw);
4108
5079
  if (resolves(id)) continue;
4109
5080
  console.warn(`[dev-server] requireScript "${raw}" on ${componentId}: no ${id}.ts in ${where}`);
@@ -4220,6 +5191,7 @@ function createServerState() {
4220
5191
  baseProvenance: null,
4221
5192
  perComponentCss: /* @__PURE__ */ new Map(),
4222
5193
  baseCss: "",
5194
+ entryBaseRules: /* @__PURE__ */ new Map(),
4223
5195
  baseCssVersion: 0,
4224
5196
  discovery: createLazy("discovery", { slow: { afterMs: DISCOVERY_SLOW_MS, what: "the component index" } }),
4225
5197
  audit: createLazy("audit"),
@@ -4250,6 +5222,7 @@ function applyStyleData(state, data) {
4250
5222
  state.baseProvenance = data.baseProvenance;
4251
5223
  state.perComponentCss = data.componentCssMap;
4252
5224
  state.baseCss = data.baseCss;
5225
+ state.entryBaseRules = new Map(data.baseRulesByEntry);
4253
5226
  return { changedComponents: changedComponents.sort(), baseChanged };
4254
5227
  }
4255
5228
  function scriptFor(state, scriptId) {
@@ -4295,24 +5268,24 @@ function cachedDiscovery(state) {
4295
5268
  }
4296
5269
 
4297
5270
  // packages/dev-server/src/routes.ts
4298
- 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";
4299
5272
  import { readFile as readFile24 } from "node:fs/promises";
4300
5273
 
4301
5274
  // packages/dev-server/src/component-tree.ts
4302
- import { readFile as readFile14 } from "node:fs/promises";
4303
- import { join as join21 } from "node:path";
5275
+ import { readFile as readFile15 } from "node:fs/promises";
5276
+ import { join as join23 } from "node:path";
4304
5277
 
4305
5278
  // packages/dev-server/src/layout-mode.ts
4306
- import { readFile as readFile13 } from "node:fs/promises";
4307
- import { join as join20 } from "node:path";
5279
+ import { readFile as readFile14 } from "node:fs/promises";
5280
+ import { join as join22 } from "node:path";
4308
5281
 
4309
5282
  // packages/dev-server/src/pattern-dirs.ts
4310
- import { readFile as readFile12 } from "node:fs/promises";
4311
- import { join as join19 } from "node:path";
5283
+ import { readFile as readFile13 } from "node:fs/promises";
5284
+ import { join as join21 } from "node:path";
4312
5285
  async function readFolderFile(dirs, rel) {
4313
5286
  for (const dir of typeof dirs === "string" ? [dirs] : dirs) {
4314
5287
  try {
4315
- return await readFile12(join19(dir, rel), "utf-8");
5288
+ return await readFile13(join21(dir, rel), "utf-8");
4316
5289
  } catch {
4317
5290
  }
4318
5291
  }
@@ -4325,7 +5298,7 @@ async function resolveLayoutMode(componentId, patternsDir, discoveryMap) {
4325
5298
  const parts = componentId.split("/");
4326
5299
  const category = parts[0];
4327
5300
  let layout = FULL_CATEGORIES.has(stripOrderPrefixes(category)) ? "full" : "centered";
4328
- const categoryIndex = await readFolderFile(patternsDir, join20(category, "index.md"));
5301
+ const categoryIndex = await readFolderFile(patternsDir, join22(category, "index.md"));
4329
5302
  if (categoryIndex !== null) {
4330
5303
  const m = categoryIndex.match(/^preview:\s*(centered|full)$/m);
4331
5304
  if (m) layout = m[1];
@@ -4333,8 +5306,8 @@ async function resolveLayoutMode(componentId, patternsDir, discoveryMap) {
4333
5306
  const mdPath = discoveryMap?.get(componentId)?.mdPath;
4334
5307
  if (mdPath) {
4335
5308
  try {
4336
- const md = await readFile13(mdPath, "utf-8");
4337
- 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);
4338
5311
  if (m) layout = m[1];
4339
5312
  } catch {
4340
5313
  }
@@ -4342,130 +5315,6 @@ async function resolveLayoutMode(componentId, patternsDir, discoveryMap) {
4342
5315
  return layout;
4343
5316
  }
4344
5317
 
4345
- // packages/dev-server/src/frontmatter.ts
4346
- var BOM = "\uFEFF";
4347
- var unquote = (s) => {
4348
- const t = s.trim();
4349
- return t.length >= 2 && (t[0] === '"' || t[0] === "'") && t.at(-1) === t[0] ? t.slice(1, -1) : t;
4350
- };
4351
- var unterminatedQuote = (s) => {
4352
- const t = s.trim();
4353
- return (t[0] === '"' || t[0] === "'") && (t.length < 2 || t.at(-1) !== t[0]);
4354
- };
4355
- function block(md) {
4356
- const text = md.startsWith(BOM) ? md.slice(1) : md;
4357
- const lines = text.split(/\r?\n/);
4358
- if (lines[0]?.trim() !== "---") return { text, lines, end: null };
4359
- return { text, lines, end: lines.findIndex((l, i) => i > 0 && l.trim() === "---") };
4360
- }
4361
- function scan(md) {
4362
- const errors = [];
4363
- const err = (line, message) => {
4364
- errors.push({ line, message });
4365
- };
4366
- const { text, lines, end: blockEnd } = block(md);
4367
- if (blockEnd === null) {
4368
- if (/^---\r(?!\n)/.test(text)) err(1, "lines end in a bare CR \u2014 use LF or CRLF line endings");
4369
- return { values: {}, errors, lines: {} };
4370
- }
4371
- const end = blockEnd;
4372
- if (end < 0) {
4373
- err(1, "the frontmatter block opened here is never closed \u2014 end it with a line holding only ---");
4374
- return { values: {}, errors, lines: {} };
4375
- }
4376
- const out = {};
4377
- const keyLines = {};
4378
- let listKey = null;
4379
- for (let i = 1; i < end; i++) {
4380
- const line = lines[i];
4381
- const n = i + 1;
4382
- const bareCr = line.includes("\r");
4383
- if (bareCr) err(n, "line ends in a bare CR \u2014 use LF or CRLF line endings");
4384
- const lineErr = (message) => {
4385
- if (!bareCr) err(n, message);
4386
- };
4387
- const item = line.match(/^\s*-\s+(.*)$/);
4388
- if (item && listKey) {
4389
- if (unterminatedQuote(item[1])) lineErr(`list item ${item[1].trim()} has an opening quote with no closing one`);
4390
- out[listKey].push(unquote(item[1]));
4391
- continue;
4392
- }
4393
- const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
4394
- if (!kv) {
4395
- if (line.trim() !== "") {
4396
- if (item) lineErr("list item with no key above it \u2014 a list follows a line `key:`");
4397
- 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");
4398
- else lineErr(`cannot read this line: ${line.trim()}`);
4399
- }
4400
- listKey = null;
4401
- continue;
4402
- }
4403
- const [, key, raw] = kv;
4404
- if (key in out) lineErr(`duplicate key "${key}"`);
4405
- keyLines[key] = n;
4406
- const value = raw.trim();
4407
- if (value === "") {
4408
- out[key] = [];
4409
- listKey = key;
4410
- } else if (value.startsWith("[") && value.endsWith("]")) {
4411
- const items = value.slice(1, -1).split(",");
4412
- for (const it of items) if (unterminatedQuote(it)) lineErr(`list item ${it.trim()} has an opening quote with no closing one`);
4413
- out[key] = items.map(unquote).filter(Boolean);
4414
- listKey = null;
4415
- } else {
4416
- if (value.startsWith("[")) lineErr(`"${key}": the list opened with [ is never closed with ]`);
4417
- else if (unterminatedQuote(value)) lineErr(`"${key}": the value has an opening quote with no closing one`);
4418
- out[key] = unquote(value);
4419
- listKey = null;
4420
- }
4421
- }
4422
- return { values: out, errors, lines: keyLines };
4423
- }
4424
- function readFrontmatter(md) {
4425
- return scan(md).values;
4426
- }
4427
- function readFrontmatterStrict(md) {
4428
- return scan(md);
4429
- }
4430
- function frontmatterBodyOffset(md) {
4431
- const { end } = block(md);
4432
- return end === null || end < 0 ? 0 : end + 1;
4433
- }
4434
- function frontmatterBody(md) {
4435
- const { text, lines, end } = block(md);
4436
- if (end === null) return text;
4437
- if (end < 0) return "";
4438
- return lines.slice(end + 1).join("\n");
4439
- }
4440
-
4441
- // packages/dev-server/src/tree-order.ts
4442
- function parseOrder(value) {
4443
- if (value === void 0) return void 0;
4444
- if (Array.isArray(value)) return { list: value };
4445
- if (/^-?\d+(\.\d+)?$/.test(value)) return { weight: Number(value) };
4446
- const list = value.split(",").map((s) => s.trim()).filter(Boolean);
4447
- return list.length ? { list } : void 0;
4448
- }
4449
- var collator = new Intl.Collator("en", { numeric: true });
4450
- var REST = "...";
4451
- function orderSiblings(items, list) {
4452
- const position = /* @__PURE__ */ new Map();
4453
- list?.forEach((name, i) => {
4454
- if (!position.has(name)) position.set(name, i);
4455
- });
4456
- const restPosition = position.get(REST) ?? Infinity;
4457
- const positionOf2 = (item) => list ? position.get(item.segment) ?? restPosition : 0;
4458
- return [...items].sort((a, b) => {
4459
- const ra = a.weight ?? 0;
4460
- const rb = b.weight ?? 0;
4461
- if (ra !== rb) return ra - rb;
4462
- const pa = positionOf2(a);
4463
- const pb = positionOf2(b);
4464
- if (pa !== pb) return pa - pb;
4465
- return collator.compare(a.segment, b.segment) || (a.segment < b.segment ? -1 : a.segment > b.segment ? 1 : 0);
4466
- });
4467
- }
4468
-
4469
5318
  // packages/dev-server/src/component-tree.ts
4470
5319
  function directChildPaths(parentPath, allIds) {
4471
5320
  const prefix = `${parentPath}/`;
@@ -4483,9 +5332,9 @@ function folderTitle(segment) {
4483
5332
  const text = words.join(" ");
4484
5333
  return text.charAt(0).toUpperCase() + text.slice(1);
4485
5334
  }
4486
- var readText = (path) => readFile14(path, "utf-8").catch(() => null);
4487
- function orderOf(md) {
4488
- 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"]);
4489
5338
  }
4490
5339
  function hasPublicComponent(node) {
4491
5340
  if (node.kind === "component" && node.visibility !== "private") return true;
@@ -4519,22 +5368,22 @@ async function buildTreeNodeFromMap(nodePath, idsUnderPath, patternsDir, overvie
4519
5368
  let visibility;
4520
5369
  let weight;
4521
5370
  if (isComponent && entry.mdPath) {
4522
- const md = await readText(entry.mdPath);
4523
- if (md !== null) {
4524
- const t = md.match(/^title:\s*(.+)$/m);
4525
- const s = md.match(/^status:\s*(.+)$/m);
4526
- 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);
4527
5376
  if (t) title = t[1].trim();
4528
5377
  if (s) {
4529
5378
  const sv = s[1].trim();
4530
5379
  if (sv === "stable" || sv === "deprecated" || sv === "draft") status = sv;
4531
5380
  }
4532
5381
  if (v) visibility = v[1];
4533
- const o = orderOf(md);
5382
+ const o = orderOf(md2);
4534
5383
  if (o && "weight" in o) weight = o.weight;
4535
5384
  }
4536
5385
  }
4537
- const indexMd = await readFolderFile(patternsDir, join21(nodePath, "index.md"));
5386
+ const indexMd = await readFolderFile(patternsDir, join23(nodePath, "index.md"));
4538
5387
  const folderOrder = orderOf(indexMd);
4539
5388
  const built = [];
4540
5389
  for (const childPath of directChildPaths(nodePath, idsUnderPath)) {
@@ -4584,10 +5433,10 @@ import { readFile as readFile21 } from "node:fs/promises";
4584
5433
  // packages/dev-server/src/markdown.ts
4585
5434
  import { readFile as readFile19 } from "node:fs/promises";
4586
5435
  import { basename as basename8 } from "node:path";
4587
- import MarkdownIt3 from "markdown-it";
5436
+ import MarkdownIt4 from "markdown-it";
4588
5437
 
4589
5438
  // packages/dev-server/src/placed-tags.ts
4590
- import MarkdownIt from "markdown-it";
5439
+ import MarkdownIt2 from "markdown-it";
4591
5440
  var OPEN = /^<(pd-[a-z0-9-]+(?:~[a-z0-9-]+)?)/;
4592
5441
  var CLOSE = /^<\/(pd-[a-z0-9-~]+)\s*>/;
4593
5442
  var ATTR = /^\s+([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/;
@@ -4671,9 +5520,9 @@ function placedTagRule(state, startLine, endLine, silent) {
4671
5520
  state.line = Math.min(line + 1, endLine);
4672
5521
  return true;
4673
5522
  }
4674
- function usePlacedTags(md) {
4675
- md.block.ruler.before("paragraph", "pd_component", placedTagRule, { alt: ["paragraph", "blockquote", "list"] });
4676
- return md;
5523
+ function usePlacedTags(md2) {
5524
+ md2.block.ruler.before("paragraph", "pd_component", placedTagRule, { alt: ["paragraph", "blockquote", "list"] });
5525
+ return md2;
4677
5526
  }
4678
5527
  var camel = (s) => s.replace(/-([a-z0-9])/g, (_, c3) => c3.toUpperCase());
4679
5528
  function dedent(s) {
@@ -4739,10 +5588,10 @@ async function toPlaced(tag, resolver, problems) {
4739
5588
  // packages/dev-server/src/storefront/placed.ts
4740
5589
  import { readFile as readFile18 } from "node:fs/promises";
4741
5590
  import { basename as basename7 } from "node:path";
4742
- import MarkdownIt2 from "markdown-it";
5591
+ import MarkdownIt3 from "markdown-it";
4743
5592
 
4744
5593
  // packages/dev-server/src/storefront/declared-values.ts
4745
- import { readFile as readFile15 } from "node:fs/promises";
5594
+ import { readFile as readFile16 } from "node:fs/promises";
4746
5595
  var NUMERIC = /^-?\d+(\.\d+)?$/;
4747
5596
  var quote = (v) => Array.isArray(v) ? `[${v.join(", ")}]` : `"${v}"`;
4748
5597
  var describe = (v, mode) => mode === "frontmatter" ? quote(v) : JSON.stringify(v);
@@ -4836,7 +5685,7 @@ function jsonValue(prop, value, path, owner, problems) {
4836
5685
  async function readSchema(schemaPath) {
4837
5686
  if (!schemaPath) return {};
4838
5687
  try {
4839
- return { schema: JSON.parse(await readFile15(schemaPath, "utf8")) };
5688
+ return { schema: JSON.parse(await readFile16(schemaPath, "utf8")) };
4840
5689
  } catch (e) {
4841
5690
  return { error: e instanceof Error ? e.message : String(e) };
4842
5691
  }
@@ -4927,209 +5776,12 @@ async function placedValue(prop, node, where, ctx, problems) {
4927
5776
  import { readFile as readFile17 } from "node:fs/promises";
4928
5777
  import { basename as basename6 } from "node:path";
4929
5778
 
4930
- // packages/dev-server/src/storefront/pages.ts
4931
- import { readdir as readdir8, readFile as readFile16, stat as stat5 } from "node:fs/promises";
4932
- import { join as join22, relative as relative11, sep as sep7 } from "node:path";
4933
-
4934
- // packages/dev-server/src/storefront/redirects.ts
4935
- function redirectsOf(f) {
4936
- const v = f.frontmatter.values["redirectFrom"];
4937
- return Array.isArray(v) ? v : typeof v === "string" ? [v] : [];
4938
- }
4939
- var RESERVED = [PD_ROUTE_PREFIX.preview, "/_pd/", PD_ROUTE_PREFIX.assets, "/api/", PD_ROUTE_PREFIX.source];
4940
- function redirectProblems(files, workbenchPath) {
4941
- const problems = [];
4942
- const claimed = /* @__PURE__ */ new Map();
4943
- for (const f of files.values()) {
4944
- const at = `${f.path}${f.frontmatter.lines["redirectFrom"] !== void 0 ? `:${f.frontmatter.lines["redirectFrom"]}` : ""}`;
4945
- for (const old of redirectsOf(f)) {
4946
- if (typeof old !== "string" || !old.startsWith("/") || !old.endsWith("/")) continue;
4947
- const page = files.get(old);
4948
- if (page) {
4949
- problems.push(`${at}: redirectFrom "${old}" is the page ${page.path}: a route can't both be a page and forward to one`);
4950
- continue;
4951
- }
4952
- const reserved = RESERVED.find((r) => old.startsWith(r));
4953
- if (reserved) {
4954
- problems.push(`${at}: redirectFrom "${old}" is under ${reserved}, which Pattern Dump serves itself`);
4955
- continue;
4956
- }
4957
- if (workbenchPath !== void 0 && old.startsWith(workbenchPath)) {
4958
- problems.push(`${at}: redirectFrom "${old}" is ${workbenchPlace(old, workbenchPath)}`);
4959
- continue;
4960
- }
4961
- const other = claimed.get(old);
4962
- if (other && other !== f) {
4963
- problems.push(`${at}: redirectFrom "${old}" is also claimed by ${other.path}: one old route forwards to one page`);
4964
- continue;
4965
- }
4966
- claimed.set(old, f);
4967
- }
4968
- }
4969
- return problems;
4970
- }
4971
- function workbenchPlace(route, workbenchPath) {
4972
- return `${route === workbenchPath ? "at" : "under"} ${workbenchPath}, where the workbench is published on this host (storefront.workbenchUrl), so the workbench would overwrite it there`;
4973
- }
4974
- function redirectStub(p) {
4975
- const target = escapeAttr(p.route);
4976
- const canonical2 = p.siteUrl !== void 0 ? `
4977
- <link rel="canonical" href="${escapeAttr(new URL(p.route.slice(1), p.siteUrl).href)}">` : "";
4978
- return `<!DOCTYPE html>
4979
- <html lang="${escapeAttr(p.lang)}">
4980
- <head>
4981
- <meta charset="UTF-8">
4982
- <title>Moved to ${escapeHtml(p.title)}</title>${canonical2}
4983
- <meta name="robots" content="noindex">
4984
- <meta http-equiv="refresh" content="0; url=${target}">
4985
- </head>
4986
- <body>
4987
- <p>This page has moved to <a href="${target}">${escapeHtml(p.title)}</a>.</p>
4988
- </body>
4989
- </html>
4990
- `;
4991
- }
4992
-
4993
- // packages/dev-server/src/storefront/pages.ts
4994
- var DEFAULT_LAYOUT = "page";
4995
- var NOT_FOUND_FILE = "404.md";
4996
- function routeFor(path) {
4997
- const parts = path.replace(/\.md$/, "").split("/");
4998
- if (parts.at(-1) === "index") parts.pop();
4999
- return parts.length ? `/${parts.join("/")}/` : "/";
5000
- }
5001
- var NAME = /^[a-z0-9-]+$/;
5002
- async function markdownFiles(dir, root, problems) {
5003
- const out = [];
5004
- const entries = (await readdir8(dir, { withFileTypes: true }).catch(() => [])).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
5005
- for (const e of entries) {
5006
- if (e.name.startsWith(".")) continue;
5007
- const p = join22(dir, e.name);
5008
- const rel = relative11(root, p).split(sep7).join("/");
5009
- if (e.isDirectory()) {
5010
- if (!NAME.test(e.name)) {
5011
- problems.push(`${rel}/: a folder name is part of the URL, so it is lowercase letters, digits and hyphens only`);
5012
- continue;
5013
- }
5014
- out.push(...await markdownFiles(p, root, problems));
5015
- } else if (!e.name.endsWith(".md")) {
5016
- 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)`);
5017
- } else if (!NAME.test(e.name.slice(0, -".md".length))) {
5018
- problems.push(`${rel}: a page's file name is part of the URL, so it is lowercase letters, digits and hyphens only, then .md`);
5019
- } else {
5020
- out.push(rel);
5021
- }
5022
- }
5023
- return out;
5024
- }
5025
- var scalar = (v) => typeof v === "string" ? v : void 0;
5026
- async function readPages(pagesDir, opts = {}) {
5027
- const problems = [];
5028
- const files = /* @__PURE__ */ new Map();
5029
- let notFound;
5030
- if (!await stat5(pagesDir).then((s) => s.isDirectory(), () => false)) {
5031
- return { pages: [], nav: [], files, problems: [`the pages folder ${pagesDir} does not exist`] };
5032
- }
5033
- for (const path of (await markdownFiles(pagesDir, pagesDir, problems)).sort()) {
5034
- const route = routeFor(path);
5035
- const other = files.get(route);
5036
- if (other) {
5037
- problems.push(`${other.path} and ${path} are both the page at ${route}`);
5038
- continue;
5039
- }
5040
- const file = join22(pagesDir, ...path.split("/"));
5041
- const text = await readFile16(file, "utf8").catch((e) => {
5042
- if (e.code === "ENOENT") return null;
5043
- throw e;
5044
- });
5045
- if (text === null) continue;
5046
- if (path === NOT_FOUND_FILE) {
5047
- notFound = { route: PD_ROUTES.notFound, file, path, frontmatter: readFrontmatterStrict(text) };
5048
- continue;
5049
- }
5050
- files.set(route, { route, file, path, frontmatter: readFrontmatterStrict(text) });
5051
- }
5052
- const pageOf = (f) => {
5053
- const redirectFrom = redirectsOf(f);
5054
- return {
5055
- route: f.route,
5056
- title: scalar(f.frontmatter.values["title"]) ?? f.route,
5057
- layout: scalar(f.frontmatter.values["layout"]) ?? DEFAULT_LAYOUT,
5058
- ...redirectFrom.length ? { redirectFrom } : {}
5059
- };
5060
- };
5061
- const entryOf = (f) => {
5062
- const description = scalar(f.frontmatter.values["description"]);
5063
- return { route: f.route, title: pageOf(f).title, ...description !== void 0 ? { description } : {} };
5064
- };
5065
- const weightOf = (f) => {
5066
- const o = parseOrder(f?.frontmatter.values["order"]);
5067
- return o && "weight" in o ? o.weight : void 0;
5068
- };
5069
- const listOf = (f) => {
5070
- const o = parseOrder(f?.frontmatter.values["order"]);
5071
- return o && "list" in o ? o.list : void 0;
5072
- };
5073
- const folders = /* @__PURE__ */ new Set();
5074
- for (const f of files.values()) {
5075
- const parts = f.path.split("/").slice(0, -1);
5076
- for (let i = 1; i <= parts.length; i++) folders.add(`/${parts.slice(0, i).join("/")}/`);
5077
- }
5078
- if (!files.has("/")) problems.push("there is no home page: the pages folder needs an index.md at its top, the page at /");
5079
- for (const folder of [...folders].sort()) {
5080
- 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`);
5081
- }
5082
- const childrenOf = (folder) => {
5083
- const depth = folder === "/" ? 0 : folder.split("/").length - 2;
5084
- const out = /* @__PURE__ */ new Map();
5085
- for (const route of [...files.keys(), ...folders]) {
5086
- if (route === folder || !route.startsWith(folder)) continue;
5087
- const segs = route.split("/").filter(Boolean);
5088
- if (segs.length !== depth + 1) continue;
5089
- out.set(route, segs[depth]);
5090
- }
5091
- return [...out].map(([route, segment]) => ({ route, segment }));
5092
- };
5093
- const ordered = (folder, extra = []) => {
5094
- const items = childrenOf(folder).map((c3) => {
5095
- const w = weightOf(files.get(c3.route));
5096
- return { route: c3.route, segment: c3.segment, ...w !== void 0 ? { weight: w } : {} };
5097
- });
5098
- return orderSiblings([...extra, ...items], listOf(files.get(folder)));
5099
- };
5100
- const home = files.get("/");
5101
- const homeWeight = weightOf(home);
5102
- const top = ordered("/", home ? [{ route: "/", segment: "index", ...homeWeight !== void 0 ? { weight: homeWeight } : {} }] : []);
5103
- const nav = [];
5104
- for (const item of top) {
5105
- const page = files.get(item.route);
5106
- if (!page) continue;
5107
- const children = folders.has(item.route) && item.route !== "/" ? ordered(item.route).flatMap((c3) => {
5108
- const p = files.get(c3.route);
5109
- return p ? [entryOf(p)] : [];
5110
- }) : [];
5111
- nav.push({ ...entryOf(page), ...children.length ? { children } : {} });
5112
- }
5113
- const inNav = nav.flatMap((n) => [n.route, ...(n.children ?? []).map((c3) => c3.route)]);
5114
- const rest = [...files.keys()].filter((r) => !inNav.includes(r)).sort();
5115
- const routes = [...inNav, ...rest];
5116
- const pages = routes.map((r) => pageOf(files.get(r)));
5117
- const { workbenchPath } = opts;
5118
- if (workbenchPath !== void 0) {
5119
- for (const f of files.values()) {
5120
- if (f.route.startsWith(workbenchPath)) problems.push(`${f.path}: the page ${f.route} is ${workbenchPlace(f.route, workbenchPath)}`);
5121
- }
5122
- }
5123
- problems.push(...redirectProblems(files, workbenchPath));
5124
- return { pages, nav, files: new Map(routes.map((r) => [r, files.get(r)])), problems, ...notFound ? { notFound } : {} };
5125
- }
5126
-
5127
5779
  // packages/dev-server/src/storefront/frontmatter-contract.ts
5128
- var RESERVED_KEYS = ["layout", "order", "redirectFrom", "index", "image"];
5780
+ var RESERVED_KEYS = ["layout", "order", "redirectFrom", "index", "image", "date", "tags", "draft", "sort"];
5129
5781
  var ASSEMBLY_KEYS = ["body", "nav", "site", "page"];
5130
5782
  var OLD_ROUTE = /^\/(?:(?!\.\.?\/)[^/?#\s]+\/)+$/;
5131
5783
  var ASSET_PATH = /^\/assets\/[^?#]+[^/?#]$/;
5132
- function checkBuildKeys(values, at) {
5784
+ function checkBuildKeys(values, path, at) {
5133
5785
  const out = {};
5134
5786
  const redirect = values["redirectFrom"];
5135
5787
  if (redirect !== void 0) {
@@ -5149,6 +5801,16 @@ function checkBuildKeys(values, at) {
5149
5801
  if (typeof image === "string" && ASSET_PATH.test(image)) out.image = image;
5150
5802
  else at("image", `image must be a path to a file in the assets, such as "/assets/images/share.png"`);
5151
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));
5152
5814
  return out;
5153
5815
  }
5154
5816
  function layoutNames(discoveryMap) {
@@ -5171,12 +5833,12 @@ async function checkPageFrontmatter(page, discoveryMap) {
5171
5833
  if (!Array.isArray(rawLayout)) {
5172
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)`);
5173
5835
  }
5174
- return { layoutId, values: {}, build: checkBuildKeys(values, at), problems };
5836
+ return { layoutId, values: {}, build: checkBuildKeys(values, page.path, at), problems };
5175
5837
  }
5176
5838
  const read = await readSchema(entry.schemaPath);
5177
5839
  if (read.error !== void 0) {
5178
5840
  problems.push({ file: page.path, message: `the schema of layout "${layout}" cannot be read: ${read.error}` });
5179
- return { layoutId, values: {}, build: checkBuildKeys(values, at), problems };
5841
+ return { layoutId, values: {}, build: checkBuildKeys(values, page.path, at), problems };
5180
5842
  }
5181
5843
  const schema = read.schema ?? {};
5182
5844
  const hasSchema = read.schema !== void 0;
@@ -5186,7 +5848,7 @@ async function checkPageFrontmatter(page, discoveryMap) {
5186
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` });
5187
5849
  }
5188
5850
  }
5189
- const build = checkBuildKeys(values, at);
5851
+ const build = checkBuildKeys(values, page.path, at);
5190
5852
  const out = {};
5191
5853
  for (const [key, value] of Object.entries(values)) {
5192
5854
  if (RESERVED_KEYS.includes(key)) continue;
@@ -5217,6 +5879,9 @@ async function checkPageFrontmatter(page, discoveryMap) {
5217
5879
 
5218
5880
  // packages/dev-server/src/storefront/derived.ts
5219
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
+ };
5220
5885
  var PAGE_SHAPE = {
5221
5886
  fields: {
5222
5887
  headings: { items: { fields: { level: "value", text: "value", anchor: "value" } } },
@@ -5225,10 +5890,13 @@ var PAGE_SHAPE = {
5225
5890
  prev: LINK,
5226
5891
  next: LINK,
5227
5892
  created: "value",
5228
- modified: "value"
5893
+ modified: "value",
5894
+ children: { items: ENTRY },
5895
+ date: "value",
5896
+ tags: "value"
5229
5897
  }
5230
5898
  };
5231
- var FILLED = ["headings", "section", "breadcrumbs", "prev", "next", "created", "modified"];
5899
+ var FILLED = ["headings", "section", "breadcrumbs", "prev", "next", "created", "modified", "children", "date", "tags"];
5232
5900
  function positionOf(route, site) {
5233
5901
  const titles = new Map(site.pages.map((p) => [p.route, p.title]));
5234
5902
  const link = (r) => titles.has(r) ? { route: r, title: titles.get(r) } : null;
@@ -5276,7 +5944,7 @@ var NAVIGATIONS_KEY = "navigations";
5276
5944
  var AT = "storefront.site.navigations";
5277
5945
  var KEYS = ["page", "href", "label", "children"];
5278
5946
  var isObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5279
- 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);
5280
5948
  var isAllowedHref = (href) => /^(https?:\/\/|mailto:)/i.test(href) || /^\/(?!\/)/.test(href);
5281
5949
  function listedNavigations(declared, owner) {
5282
5950
  if (!declared) return { listed: [], problems: [] };
@@ -5299,7 +5967,7 @@ function checkNavigations(value, declared, owner, pagePaths) {
5299
5967
  const problems = [...names.problems];
5300
5968
  const navigations = {};
5301
5969
  if (value !== void 0 && !isObject5(value)) {
5302
- 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)})`);
5303
5971
  return { navigations, listed: names.listed, problems };
5304
5972
  }
5305
5973
  const given = value ?? {};
@@ -5310,7 +5978,7 @@ function checkNavigations(value, declared, owner, pagePaths) {
5310
5978
  continue;
5311
5979
  }
5312
5980
  if (!Array.isArray(entries)) {
5313
- 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)})`);
5314
5982
  continue;
5315
5983
  }
5316
5984
  problems.push(...checkEntries(entries, `${AT}.${name}`, pagePaths));
@@ -5326,7 +5994,7 @@ function checkEntries(entries, at, pagePaths) {
5326
5994
  }
5327
5995
  function checkEntry(entry, at, pagePaths) {
5328
5996
  const FORMS = `{ page }, { href, label } or { label, children }`;
5329
- 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)})`];
5330
5998
  const problems = [];
5331
5999
  for (const key of Object.keys(entry)) {
5332
6000
  if (KEYS.includes(key)) continue;
@@ -5339,11 +6007,11 @@ function checkEntry(entry, at, pagePaths) {
5339
6007
  return problems;
5340
6008
  }
5341
6009
  const label = entry["label"];
5342
- 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)})`);
5343
6011
  const form = forms[0];
5344
6012
  if (form === "page") {
5345
6013
  const page = entry["page"];
5346
- 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)})`);
5347
6015
  else if (!pagePaths.has(page)) {
5348
6016
  const near = suggest(page, [...pagePaths]);
5349
6017
  problems.push(`${at}: "${page}" is not a page in the pages folder${near ? ` \u2014 did you mean "${near}"?` : ""}`);
@@ -5352,11 +6020,11 @@ function checkEntry(entry, at, pagePaths) {
5352
6020
  if (label === void 0) problems.push(`${at} needs a "label"`);
5353
6021
  if (form === "href") {
5354
6022
  const href = entry["href"];
5355
- if (typeof href !== "string") problems.push(`${at}.href must be text (got ${show(href)})`);
5356
- 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)`);
5357
6025
  } else {
5358
6026
  const children = entry["children"];
5359
- 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)})`);
5360
6028
  else problems.push(...checkEntries(children, `${at}.children`, pagePaths));
5361
6029
  }
5362
6030
  }
@@ -5377,10 +6045,86 @@ function resolveNavigations(navigations, listed, model, route) {
5377
6045
  return Object.fromEntries(listed.map((name) => [name, Object.hasOwn(navigations, name) ? resolve15(navigations[name]) : null]));
5378
6046
  }
5379
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
+
5380
6124
  // packages/dev-server/src/storefront/site.ts
5381
6125
  var SITE_SCHEMA_OWNER = "partials/base";
5382
- var none = (problems = []) => ({ values: {}, navigations: {}, listed: [], problems });
5383
- 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: [] }) {
5384
6128
  const problems = [];
5385
6129
  const entry = discoveryMap.get(SITE_SCHEMA_OWNER);
5386
6130
  if (!entry) {
@@ -5397,24 +6141,32 @@ async function checkSite(site, discoveryMap, pagePaths = /* @__PURE__ */ new Set
5397
6141
  }
5398
6142
  const navDeclared = declared.properties?.[NAVIGATIONS_KEY];
5399
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
+ );
5400
6151
  if (site === void 0) {
5401
6152
  if (schema.required?.includes("site")) problems.push(`storefront.site is required by ${SITE_SCHEMA_OWNER}`);
5402
- problems.push(...navs.problems);
5403
- 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 };
5404
6155
  }
5405
- 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)));
5406
6158
  const restDeclared = {
5407
6159
  ...declared,
5408
- properties: Object.fromEntries(Object.entries(declared.properties ?? {}).filter(([k]) => k !== NAVIGATIONS_KEY)),
5409
- ...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) } : {}
5410
6162
  };
5411
6163
  const checked = checkJsonKeys(restDeclared, rest, "storefront.site", SITE_SCHEMA_OWNER);
5412
- problems.push(...checked.problems, ...navs.problems);
6164
+ problems.push(...checked.problems, ...navs.problems, ...cols.problems);
5413
6165
  if (declared.required?.includes(NAVIGATIONS_KEY) && !Object.hasOwn(site, NAVIGATIONS_KEY)) {
5414
6166
  problems.push(`storefront.site.${NAVIGATIONS_KEY} is required by ${SITE_SCHEMA_OWNER}`);
5415
6167
  }
5416
- if (problems.length) return { ...none(problems), listed: navs.listed };
5417
- 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 };
5418
6170
  }
5419
6171
 
5420
6172
  // packages/dev-server/src/storefront/sample-data.ts
@@ -5432,7 +6184,7 @@ function includeTree(layoutId, composition) {
5432
6184
  }
5433
6185
  return [layoutId, ...[...seen].filter((id) => id !== layoutId).sort()];
5434
6186
  }
5435
- async function checkSampleData(layoutId, discoveryMap, composition, layoutDeclared, siteDeclared, pageDeclared, navigationNames = []) {
6187
+ async function checkSampleData(layoutId, discoveryMap, composition, layoutDeclared, siteDeclared, pageDeclared, navigationNames = [], collectionNames = []) {
5436
6188
  const problems = [];
5437
6189
  const allowed = new Set([...layoutDeclared, ...ASSEMBLY_KEYS].filter((k) => k !== "page" || pageDeclared !== void 0));
5438
6190
  const shapeFields = PAGE_SHAPE.fields;
@@ -5469,10 +6221,11 @@ async function checkSampleData(layoutId, discoveryMap, composition, layoutDeclar
5469
6221
  for (const [sub, v] of Object.entries(value)) {
5470
6222
  if (!siteDeclared.includes(sub)) {
5471
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}`);
5472
- } else if (sub === "navigations" && isPlainObject2(v)) {
6224
+ } else if ((sub === "navigations" || sub === "collections") && isPlainObject2(v)) {
6225
+ const names = sub === "navigations" ? navigationNames : collectionNames;
5473
6226
  for (const name of Object.keys(v)) {
5474
- if (!navigationNames.includes(name)) {
5475
- 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}`);
5476
6229
  }
5477
6230
  }
5478
6231
  }
@@ -5484,7 +6237,7 @@ async function checkSampleData(layoutId, discoveryMap, composition, layoutDeclar
5484
6237
  }
5485
6238
 
5486
6239
  // packages/dev-server/src/storefront/placed.ts
5487
- var contentMd = new MarkdownIt2({ html: false, linkify: false });
6240
+ var contentMd = new MarkdownIt3({ html: false, linkify: false });
5488
6241
  var kebab = (k) => k.replace(/[A-Z]/g, (c3) => `-${c3.toLowerCase()}`);
5489
6242
  async function placementComponents(id, deps) {
5490
6243
  const seen = /* @__PURE__ */ new Set();
@@ -5524,8 +6277,8 @@ async function unsafeOutputs(id, at, deps) {
5524
6277
  for (const componentId of [...templates].sort()) {
5525
6278
  const file = deps.discoveryMap.get(componentId)?.hbsPath;
5526
6279
  if (!file) continue;
5527
- const source = await readFile18(file, "utf8").catch(() => "");
5528
- for (const o of unescapedOutputs(source)) {
6280
+ const source2 = await readFile18(file, "utf8").catch(() => "");
6281
+ for (const o of unescapedOutputs(source2)) {
5529
6282
  if (!o.helper && allowed.has(o.name)) continue;
5530
6283
  const what = o.helper ? `a helper's result unescaped ({{{${o.name}}}})` : `"${o.name}" unescaped ({{{${o.name}}}})`;
5531
6284
  problems.push({ at, message: `${componentId} outputs ${what} at ${deps.shown(file)}:${o.line}:${o.column}, which only a text/markdown property may be` });
@@ -5533,7 +6286,7 @@ async function unsafeOutputs(id, at, deps) {
5533
6286
  }
5534
6287
  return problems;
5535
6288
  }
5536
- var isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6289
+ var isObject7 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5537
6290
  function undeclaredSampleKeys(sample, props, prefix) {
5538
6291
  const out = [];
5539
6292
  for (const [key, value] of Object.entries(sample)) {
@@ -5542,7 +6295,7 @@ function undeclaredSampleKeys(sample, props, prefix) {
5542
6295
  out.push(`${prefix}${key}`);
5543
6296
  continue;
5544
6297
  }
5545
- 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}.`));
5546
6299
  }
5547
6300
  return out;
5548
6301
  }
@@ -5561,7 +6314,7 @@ async function sampleDataProblems(id, at, deps) {
5561
6314
  } catch {
5562
6315
  continue;
5563
6316
  }
5564
- if (!isObject6(sample)) continue;
6317
+ if (!isObject7(sample)) continue;
5565
6318
  for (const key of undeclaredSampleKeys(sample, declared, "")) {
5566
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` });
5567
6320
  }
@@ -5581,6 +6334,11 @@ async function schemaProblems(id, at, deps) {
5581
6334
  })) {
5582
6335
  problems.push({ at, message: `${component} cannot be placed: its schema (${deps.shown(entry.schemaPath)}, ${p.path}): ${p.message}` });
5583
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
+ }
5584
6342
  }
5585
6343
  return problems;
5586
6344
  }
@@ -5636,23 +6394,23 @@ var Slugger = class {
5636
6394
  // packages/dev-server/src/markdown.ts
5637
6395
  var INLINE = { embed: "inline" };
5638
6396
  function createDocMarkdown(placing) {
5639
- const md = new MarkdownIt3({ html: false, linkify: false });
5640
- md.validateLink = (url) => {
6397
+ const md2 = new MarkdownIt4({ html: false, linkify: false });
6398
+ md2.validateLink = (url) => {
5641
6399
  const t = url.trim();
5642
6400
  return /^(https?|mailto|tel):/i.test(t) || /^[.#/]/.test(t) || t.startsWith("pattern:");
5643
6401
  };
5644
- md.renderer.rules["fence"] = (tokens, idx, _opts, env) => {
6402
+ md2.renderer.rules["fence"] = (tokens, idx, _opts, env) => {
5645
6403
  const map = env.resolvedFences;
5646
6404
  return map?.get(idx) ?? _mdPlainPre(tokens[idx]?.content ?? "");
5647
6405
  };
5648
- const image = md.renderer.rules["image"];
5649
- 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);
5650
6408
  if (placing) {
5651
- usePlacedTags(md);
5652
- 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>
5653
6411
  `;
5654
6412
  }
5655
- return md;
6413
+ return md2;
5656
6414
  }
5657
6415
  var _md = createDocMarkdown(false);
5658
6416
  var _mdPlaced = createDocMarkdown(true);
@@ -5706,10 +6464,10 @@ function _resolvePatternLinks(tokens, discoveryMap, embed) {
5706
6464
  }
5707
6465
  async function renderDoc(body, renderComponent, discoveryMap, embed = INLINE) {
5708
6466
  const placing = embed.embed === "frame" ? embed.placed : void 0;
5709
- const md = placing ? _mdPlaced : _md;
5710
- 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 });
5711
6469
  _resolvePatternLinks(tokens, discoveryMap, embed);
5712
- if (embed.embed === "frame" && embed.prose) proseImages(md, tokens, embed.prose);
6470
+ if (embed.embed === "frame" && embed.prose) proseImages(md2, tokens, embed.prose);
5713
6471
  if (embed.embed === "frame") {
5714
6472
  const headings = headingAnchors(tokens, embed.takenIds);
5715
6473
  embed.headings?.push(...headings);
@@ -5730,9 +6488,9 @@ async function renderDoc(body, renderComponent, discoveryMap, embed = INLINE) {
5730
6488
  `);
5731
6489
  }
5732
6490
  }
5733
- return md.renderer.render(tokens, md.options, { resolvedFences, resolvedPlaced });
6491
+ return md2.renderer.render(tokens, md2.options, { resolvedFences, resolvedPlaced });
5734
6492
  }
5735
- function proseImages(md, tokens, prose) {
6493
+ function proseImages(md2, tokens, prose) {
5736
6494
  let eager = prose.slot?.eager ?? 0;
5737
6495
  for (const block2 of tokens) {
5738
6496
  if (block2.type !== "inline" || !block2.children) continue;
@@ -5745,22 +6503,22 @@ function proseImages(md, tokens, prose) {
5745
6503
  if (child.type !== "image") continue;
5746
6504
  const src = child.attrGet("src") ?? "";
5747
6505
  const problem = (message) => prose.problems.push({ line, message: `the image ${src}${message}` });
5748
- const alt = md.renderer.renderInlineAsText(child.children ?? [], md.options, {});
6506
+ const alt = md2.renderer.renderInlineAsText(child.children ?? [], md2.options, {});
5749
6507
  if (alt.trim() === "") {
5750
6508
  problem(` has no alt text: write what it shows between the brackets; a decorative image goes in through a component, with alt=""`);
5751
6509
  continue;
5752
6510
  }
5753
6511
  if (!src.startsWith("/assets/")) continue;
5754
- const source = prose.lookup(src);
5755
- if (!source) {
6512
+ const source2 = prose.lookup(src);
6513
+ if (!source2) {
5756
6514
  problem(` is not an image in the assets`);
5757
6515
  continue;
5758
6516
  }
5759
- if (source.kind === "unavailable") {
5760
- problem(`: ${source.reason}`);
6517
+ if (source2.kind === "unavailable") {
6518
+ problem(`: ${source2.reason}`);
5761
6519
  continue;
5762
6520
  }
5763
- if (source.kind === "variants" && !prose.slot) {
6521
+ if (source2.kind === "variants" && !prose.slot) {
5764
6522
  problem(` needs its layout's slot for prose images: ${prose.layoutId} ` + (prose.slotProblem ? `has an invalid x-pd-prose-images (${prose.slotProblem})` : `declares no x-pd-prose-images`));
5765
6523
  continue;
5766
6524
  }
@@ -5768,7 +6526,7 @@ function proseImages(md, tokens, prose) {
5768
6526
  const atOnce = eager > 0;
5769
6527
  if (atOnce) eager--;
5770
6528
  const slot = prose.slot;
5771
- child.meta = { pdImage: imageTag(source, {
6529
+ child.meta = { pdImage: imageTag(source2, {
5772
6530
  widths: slot?.widths ?? [],
5773
6531
  sizes: slot ? atOnce ? slot.sizes : `auto, ${slot.sizes}` : "",
5774
6532
  alt,
@@ -5932,7 +6690,7 @@ import postcss4 from "postcss";
5932
6690
  import * as sass3 from "sass";
5933
6691
  import { SourceMapConsumer as SourceMapConsumer2 } from "source-map";
5934
6692
  import { parse as parseHtml2 } from "node-html-parser";
5935
- async function recomputeComponentCss(entryFile, compId, stylesDir, componentIds, inBase = () => false) {
6693
+ async function recomputeComponentCss(entryFile, compId, stylesDir, componentIds, inBase = () => false, out) {
5936
6694
  try {
5937
6695
  const compiled = sass3.compile(entryFile, { sourceMap: true, ...sassPackageOptions(entryFile) });
5938
6696
  const rawMap = compiled.sourceMap;
@@ -5940,16 +6698,22 @@ async function recomputeComponentCss(entryFile, compId, stylesDir, componentIds,
5940
6698
  const consumer = new SourceMapConsumer2(rawMap);
5941
6699
  const root = postcss4.parse(compiled.css);
5942
6700
  const bucket = /* @__PURE__ */ new Set();
6701
+ const base2 = /* @__PURE__ */ new Set();
5943
6702
  root.each((node) => {
5944
6703
  const pos = node.source?.start;
5945
6704
  if (!pos) return;
5946
6705
  const original = consumer.originalPositionFor({ line: pos.line, column: pos.column - 1 });
5947
6706
  if (!original.source) return;
5948
6707
  const originPath = original.source.startsWith("file://") ? fileURLToPath4(original.source) : resolve8(dirname8(entryFile), original.source);
5949
- if (inBase(originPath)) return;
6708
+ if (inBase(originPath)) {
6709
+ addLastOccurrence(base2, cssNodeText(node));
6710
+ return;
6711
+ }
5950
6712
  const originComp = resolveScssToComponent(originPath, stylesDir, componentIds);
5951
6713
  if (originComp === compId) addLastOccurrence(bucket, cssNodeText(node));
6714
+ else if (originComp === null) addLastOccurrence(base2, cssNodeText(node));
5952
6715
  });
6716
+ if (out) out.baseRules = joinCssChunks(base2);
5953
6717
  return joinCssChunks(bucket);
5954
6718
  } catch {
5955
6719
  return null;
@@ -6090,7 +6854,7 @@ async function buildComponentDetail(componentId, patternsDir, renderFn, tracker,
6090
6854
  } catch {
6091
6855
  }
6092
6856
  }
6093
- const source = {
6857
+ const source2 = {
6094
6858
  hbs,
6095
6859
  hbsHighlighted,
6096
6860
  ...jsonStr !== void 0 ? { json: jsonStr, jsonHighlighted } : {},
@@ -6131,7 +6895,7 @@ async function buildComponentDetail(componentId, patternsDir, renderFn, tracker,
6131
6895
  layout,
6132
6896
  ...deprecatedUse ? { deprecatedUse } : {},
6133
6897
  variants,
6134
- source,
6898
+ source: source2,
6135
6899
  doc,
6136
6900
  ...requireScripts ? { requireScripts } : {},
6137
6901
  ...requireAssets ? { requireAssets } : {},
@@ -6142,13 +6906,13 @@ async function buildComponentDetail(componentId, patternsDir, renderFn, tracker,
6142
6906
  }
6143
6907
 
6144
6908
  // packages/dev-server/src/overview.ts
6145
- import { join as join23 } from "node:path";
6909
+ import { join as join24 } from "node:path";
6146
6910
  async function buildFolderOverview(folderPath, patternsDir, renderFn, discoveryMap) {
6147
6911
  const category = folderPath.split("/")[0];
6148
6912
  let folderLayout = FULL_CATEGORIES.has(stripOrderPrefixes(category)) ? "full" : "centered";
6149
6913
  let doc;
6150
6914
  try {
6151
- const rawIndex = await readFolderFile(patternsDir, join23(folderPath, "index.md"));
6915
+ const rawIndex = await readFolderFile(patternsDir, join24(folderPath, "index.md"));
6152
6916
  if (rawIndex === null) throw new Error("no index.md");
6153
6917
  const m = rawIndex.match(/^preview:\s*(centered|full)$/m);
6154
6918
  if (m) folderLayout = m[1];
@@ -6185,9 +6949,9 @@ async function collectPublicLeaves(folderPath, patternsDir, discoveryMap) {
6185
6949
  }
6186
6950
 
6187
6951
  // packages/dev-server/src/content-hash.ts
6188
- import { createHash as createHash5 } from "node:crypto";
6952
+ import { createHash as createHash6 } from "node:crypto";
6189
6953
  function contentHash(bytes) {
6190
- return createHash5("sha256").update(bytes).digest("hex").slice(0, 10);
6954
+ return createHash6("sha256").update(bytes).digest("hex").slice(0, 10);
6191
6955
  }
6192
6956
 
6193
6957
  // packages/dev-server/src/scripts.ts
@@ -6357,7 +7121,11 @@ async function stampSrcset(value, hashOf) {
6357
7121
  }));
6358
7122
  return entries.join(",");
6359
7123
  }
6360
- 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) {
6361
7129
  const isPreview = name.toLowerCase() === "meta" && /\s(?:property="og:image"|name="twitter:image")/.test(attrs);
6362
7130
  return replaceAsync(attrs, ATTRIBUTE, async (m) => {
6363
7131
  const [whole, lead, attr2, eq, quoted] = m;
@@ -6370,22 +7138,22 @@ async function stampTag(name, attrs, hashOf) {
6370
7138
  else if (key === "srcset") stamped = await stampSrcset(value, hashOf);
6371
7139
  else if (key === "content" && isPreview) {
6372
7140
  const at = /^(https?:\/\/[^/"']+)(.*)$/.exec(value);
6373
- if (at) stamped = `${at[1]}${await stamp(at[2], hashOf)}`;
7141
+ if (at) stamped = `${at[1]}${await stampAbsolute(at[2], hashOf, base2)}`;
6374
7142
  }
6375
7143
  return `${lead}${attr2}${eq}${q}${stamped}${q}`;
6376
7144
  });
6377
7145
  }
6378
- async function stampHtml(html, hashOf) {
7146
+ async function stampHtml(html, hashOf, base2 = "/") {
6379
7147
  const parts = [];
6380
7148
  let last = 0;
6381
7149
  for (const tag of startTags(html)) {
6382
7150
  parts.push(html.slice(last, tag.start));
6383
7151
  const attrsAt = tag.start + 1 + tag.name.length;
6384
- 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)}`);
6385
7153
  last = tag.end;
6386
7154
  if (!tag.content) continue;
6387
7155
  const content = html.slice(tag.content.start, tag.content.end);
6388
- 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);
6389
7157
  last = tag.content.end;
6390
7158
  }
6391
7159
  parts.push(html.slice(last));
@@ -6402,7 +7170,7 @@ async function stampCss(css, hashOf) {
6402
7170
 
6403
7171
  // packages/dev-server/src/assets.ts
6404
7172
  import { existsSync as existsSync5 } from "node:fs";
6405
- import { createRequire as createRequire3 } from "node:module";
7173
+ import { createRequire as createRequire5 } from "node:module";
6406
7174
  import { fileURLToPath as fileURLToPath5 } from "node:url";
6407
7175
  function resolveDevUiDist(moduleUrl, exists = existsSync5) {
6408
7176
  const packaged = fileURLToPath5(new URL("./dev-ui/", moduleUrl));
@@ -6410,7 +7178,7 @@ function resolveDevUiDist(moduleUrl, exists = existsSync5) {
6410
7178
  return fileURLToPath5(new URL("../../../apps/dev-ui/dist/", moduleUrl));
6411
7179
  }
6412
7180
  function resolveAxeCore(moduleUrl) {
6413
- return createRequire3(moduleUrl).resolve("axe-core/axe.min.js");
7181
+ return createRequire5(moduleUrl).resolve("axe-core/axe.min.js");
6414
7182
  }
6415
7183
 
6416
7184
  // packages/dev-server/src/storefront/page-image.ts
@@ -6471,6 +7239,7 @@ function jsonLdScript(p) {
6471
7239
  name: p.title,
6472
7240
  ...p.description ? { description: p.description } : {},
6473
7241
  isPartOf: { "@id": site },
7242
+ ...p.published ? { datePublished: p.published } : {},
6474
7243
  ...p.modified ? { dateModified: p.modified } : {},
6475
7244
  ...p.breadcrumbs.length ? {
6476
7245
  breadcrumb: {
@@ -6502,9 +7271,9 @@ function writtenByPatternDump(tag, attrs) {
6502
7271
  if (tag === "script" && attr(attrs, "type") === "application/ld+json") return `<script type="application/ld+json">`;
6503
7272
  return void 0;
6504
7273
  }
6505
- function checkHeadMarkup(html) {
7274
+ function checkHeadMarkup(html, source2 = { label: HEAD_PARTIAL, subject: "a head partial" }) {
6506
7275
  const problems = [];
6507
- const say = (what) => problems.push(`${HEAD_PARTIAL}: ${what}`);
7276
+ const say = (what) => problems.push(`${source2.label}: ${what.replaceAll("a head partial", source2.subject)}`);
6508
7277
  let rest = html.replace(/<!--[\s\S]*?-->/g, "");
6509
7278
  rest = rest.replace(/<(script|style|noscript|title)\b([^>]*)>[\s\S]*?<\/\1\s*>/gi, (_m, rawTag, attrs) => {
6510
7279
  const tag = rawTag.toLowerCase();
@@ -6578,13 +7347,14 @@ var LIVE_NOTICE = `function(t){var n=document.querySelector("[data-pd-live-notic
6578
7347
  function exampleHeightCheck() {
6579
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();});})();`;
6580
7349
  }
6581
- function document(head, body, live, lang = "en") {
7350
+ function document2(head, body, live, lang = "en", base2) {
6582
7351
  return `<!DOCTYPE html>
6583
7352
  <html lang="${escapeAttr(lang)}">
6584
7353
  <head>
6585
7354
  <meta charset="UTF-8">
6586
7355
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6587
- ${head}
7356
+ ${base2 !== void 0 ? `<meta name="pd-base-path" content="${escapeAttr(base2)}">
7357
+ ` : ""}${head}
6588
7358
  </head>
6589
7359
  <body>
6590
7360
  ${body}
@@ -6641,7 +7411,7 @@ var BUILT_IN_NOT_FOUND_DOCUMENT = `<main>
6641
7411
  </main>`;
6642
7412
  function errorDocument(route, problems, live, siteWide = []) {
6643
7413
  const items = problems.map((p) => `<li${siteWide.includes(p) ? " data-pd-site" : ""}>${escapeHtml(p)}</li>`).join("\n");
6644
- return document(
7414
+ return document2(
6645
7415
  `<title>Problems \u2014 ${escapeHtml(route)}</title>`,
6646
7416
  `<main>
6647
7417
  <h1>The page ${escapeHtml(route)} cannot be built</h1>
@@ -6669,25 +7439,25 @@ async function assemblePage(route, model, a) {
6669
7439
  const notFound = () => ({ status: 404, html: errorDocument(route, [`there is no page at ${route}`], a.live), problems: [`there is no page at ${route}`], notes: [] });
6670
7440
  const isNotFound = route === PD_ROUTES.notFound;
6671
7441
  let page = isNotFound ? model.notFound : model.files.get(route);
6672
- let source;
7442
+ let source2;
6673
7443
  if (isNotFound && !page) {
6674
7444
  const layoutId = `layouts/${DEFAULT_LAYOUT}`;
6675
7445
  if (!a.discoveryMap.has(layoutId)) {
6676
7446
  return {
6677
7447
  status: 404,
6678
- 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 ?? "/"),
6679
7449
  problems: [],
6680
7450
  notes: [`there is no ${NOT_FOUND_FILE} and no ${layoutId}, so the 404 page is a plain one of Pattern Dump's`]
6681
7451
  };
6682
7452
  }
6683
7453
  const declared = (await readSchema(a.discoveryMap.get(layoutId)?.schemaPath)).schema?.properties ?? {};
6684
- source = builtInNotFound(Object.hasOwn(declared, "title"));
6685
- page = { route, file: "", path: `${NOT_FOUND_FILE} (built in)`, frontmatter: readFrontmatterStrict(source) };
7454
+ source2 = builtInNotFound(Object.hasOwn(declared, "title"));
7455
+ page = { route, file: "", path: `${NOT_FOUND_FILE} (built in)`, frontmatter: readFrontmatterStrict(source2) };
6686
7456
  }
6687
7457
  if (!page) return notFound();
6688
- if (source === void 0) {
7458
+ if (source2 === void 0) {
6689
7459
  try {
6690
- source = await readFile23(page.file, "utf8");
7460
+ source2 = await readFile23(page.file, "utf8");
6691
7461
  } catch (e) {
6692
7462
  if (e.code === "ENOENT") return notFound();
6693
7463
  throw e;
@@ -6700,7 +7470,10 @@ async function assemblePage(route, model, a) {
6700
7470
  const line = page.frontmatter.lines["redirectFrom"];
6701
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`);
6702
7472
  }
6703
- 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
+ });
6704
7477
  siteWide = site.problems;
6705
7478
  problems.push(...site.problems);
6706
7479
  const imageLine = page.frontmatter.lines["image"];
@@ -6716,20 +7489,22 @@ async function assemblePage(route, model, a) {
6716
7489
  const layoutSchema = (await readSchema(a.discoveryMap.get(checked.layoutId)?.schemaPath)).schema;
6717
7490
  const proseSlot = readProseSlot(layoutSchema);
6718
7491
  const imageLookup = a.images ? await a.images() : void 0;
7492
+ const placedIds = /* @__PURE__ */ new Set();
6719
7493
  const renderBody = async (takenIds = []) => {
6720
7494
  const prose = imageLookup ? {
6721
7495
  layoutId: checked.layoutId,
6722
7496
  lookup: imageLookup,
6723
- lineOffset: frontmatterBodyOffset(source),
7497
+ lineOffset: frontmatterBodyOffset(source2),
6724
7498
  problems: [],
6725
7499
  ...proseSlot.slot ? { slot: proseSlot.slot } : {},
6726
7500
  ...proseSlot.problem ? { slotProblem: proseSlot.problem } : {}
6727
7501
  } : void 0;
6728
7502
  const docProblems = [];
7503
+ placedIds.clear();
6729
7504
  const placedProblems = [];
6730
7505
  const headings = [];
6731
7506
  const body = await renderDoc(
6732
- frontmatterBody(source),
7507
+ frontmatterBody(source2),
6733
7508
  (id, variant) => a.render({ component: id, variant }).then((r) => r.html),
6734
7509
  a.discoveryMap,
6735
7510
  {
@@ -6741,13 +7516,17 @@ async function assemblePage(route, model, a) {
6741
7516
  ...prose ? { prose } : {},
6742
7517
  // Components placed with values (pd- tags): checked against their schemas, rendered into the page.
6743
7518
  placed: {
6744
- lineOffset: frontmatterBodyOffset(source),
7519
+ lineOffset: frontmatterBodyOffset(source2),
6745
7520
  problems: placedProblems,
6746
7521
  deps: {
6747
7522
  discoveryMap: a.discoveryMap,
6748
7523
  handleMap: a.handleMap,
6749
7524
  composition: a.composition,
6750
- 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
+ },
6751
7530
  schemaOf: async (id) => (await readSchema(a.discoveryMap.get(id)?.schemaPath)).schema,
6752
7531
  shown: a.shown
6753
7532
  }
@@ -6768,37 +7547,65 @@ async function assemblePage(route, model, a) {
6768
7547
  const notes = [];
6769
7548
  let dates = {};
6770
7549
  let modified = null;
6771
- 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");
6772
7554
  if (a.dates && !isNotFound && (showsDates || a.siteUrl !== void 0)) {
6773
- const dated = await a.dates.of(page.path, a.live ? "dev" : "build");
7555
+ const dated = await a.dates.of(page.path, mode);
6774
7556
  if (showsDates) {
6775
7557
  dates = dated.dates;
6776
7558
  notes.push(...dated.notes);
6777
7559
  problems.push(...dated.problems);
6778
7560
  }
6779
7561
  if (dated.shallow) notes.push(SHALLOW_SEO_NOTE);
6780
- else modified = dated.dates.modified;
6781
- }
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 ?? []);
6782
7587
  if (a.discoveryMap.has(checked.layoutId)) {
6783
- 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));
6784
7589
  }
6785
7590
  const hasHead = a.discoveryMap.has(HEAD_PARTIAL);
6786
7591
  if (hasHead) {
6787
- 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);
6788
7593
  siteWide = [...siteWide, ...headSample];
6789
7594
  problems.push(...headSample);
6790
7595
  }
6791
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`));
6792
7598
  const siteData = {
6793
7599
  ...withDeclaredAbsentAsNull(site.values, siteDeclared),
6794
- ...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 } : {}
6795
7602
  };
6796
7603
  const dataFor = (body, headings) => ({
6797
7604
  ...withDeclaredAbsentAsNull(checked.values, layoutDeclared, ["body", "nav", "site", "page"]),
6798
7605
  body,
6799
7606
  nav: markCurrent(model.nav, route),
6800
7607
  site: siteData,
6801
- ...pageDeclaration.declared ? { page: pageValue(pageDeclaration.declared, { headings, ...positionOf(route, model), ...dates }) } : {}
7608
+ ...pageDeclaration.declared ? { page: pageValue(pageDeclaration.declared, { headings, ...positionOf(route, model), ...dates, ...listing }) } : {}
6802
7609
  });
6803
7610
  const renderLayout = async (body, headings) => {
6804
7611
  try {
@@ -6848,6 +7655,30 @@ async function assemblePage(route, model, a) {
6848
7655
  return fail(markup);
6849
7656
  }
6850
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
+ }
6851
7682
  const title = typeof checked.values["title"] === "string" ? checked.values["title"] : isNotFound ? "Page not found" : route;
6852
7683
  const siteTitle = typeof site.values["title"] === "string" ? site.values["title"] : void 0;
6853
7684
  const description = typeof checked.values["description"] === "string" ? checked.values["description"] : void 0;
@@ -6864,9 +7695,10 @@ async function assemblePage(route, model, a) {
6864
7695
  image: imageCheck?.ok ? imageCheck.image : void 0,
6865
7696
  index: !isNotFound && a.siteIndex !== false && checked.build.index !== false
6866
7697
  }),
6867
- ...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 })] : [],
6868
7699
  ...headExtra ? [headExtra] : [],
6869
- await a.head(checked.layoutId, html)
7700
+ ...siteHead ? [siteHead] : [],
7701
+ await a.head(checked.layoutId, html, [...placedIds])
6870
7702
  ].join("\n");
6871
7703
  if (isNotFound) {
6872
7704
  const relative19 = relativeUrls(`${head}
@@ -6903,20 +7735,33 @@ ${html}`);
6903
7735
  const self = isNotFound ? PD_ROUTES.notFound : route;
6904
7736
  const routes = /* @__PURE__ */ new Set([...model.files.keys(), ...[...model.files.values()].flatMap(redirectsOf), PD_ROUTES.notFound]);
6905
7737
  routes.add(self);
6906
- 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
+ });
6907
7745
  notes.push(...links.problems.map((p) => `warning: ${page.path}${p.slice(self.length)}`));
7746
+ notes.push(...checkPageBasics(html).map((p) => `warning: ${page.path}: ${p}`));
6908
7747
  }
6909
- return { status: isNotFound ? 404 : 200, html: document(head, `${html}
6910
- ${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 };
6911
7750
  }
6912
7751
 
6913
7752
  // packages/dev-server/src/storefront/page-head.ts
6914
- async function pageStylesheets(cfg, state, discovery, layoutId, html) {
6915
- 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
+ }
6916
7761
  const assets = await resolveDocumentAssets(cfg, state, {
6917
7762
  componentId: layoutId,
6918
7763
  html,
6919
- requireScripts: void 0,
7764
+ requireScripts,
6920
7765
  requireAssets: void 0,
6921
7766
  compositionRegistry: await compositionFor(state, discovery, cfg.patternDirs),
6922
7767
  discoveryMap
@@ -6929,7 +7774,8 @@ async function pageStylesheets(cfg, state, discovery, layoutId, html) {
6929
7774
  themeSwitch: cfg.themeSwitch,
6930
7775
  baseCssVersion: state.baseCssVersion
6931
7776
  });
6932
- 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");
6933
7779
  }
6934
7780
 
6935
7781
  // packages/dev-server/src/routes.ts
@@ -6943,9 +7789,31 @@ async function handleRequest(req, res, ctx) {
6943
7789
  return;
6944
7790
  }
6945
7791
  const host = (req.headers.host ?? "").replace(/:\d+$/, "").toLowerCase();
6946
- 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
+ }
6947
7796
  return routeRequest(req, res, ctx);
6948
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
+ }
6949
7817
  function holdResponse() {
6950
7818
  let status = 200;
6951
7819
  const headers = {};
@@ -6980,12 +7848,12 @@ function holdResponse() {
6980
7848
  var stampCaches = /* @__PURE__ */ new WeakMap();
6981
7849
  function hashOfFor(req, ctx) {
6982
7850
  const generation = ctx.generation?.() ?? "";
6983
- let cache = stampCaches.get(ctx);
6984
- if (!cache || cache.generation !== generation) {
6985
- cache = { generation, hashes: /* @__PURE__ */ new Map() };
6986
- 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);
6987
7855
  }
6988
- const { hashes } = cache;
7856
+ const { hashes } = cache2;
6989
7857
  return (path) => {
6990
7858
  let hash = hashes.get(path);
6991
7859
  if (!hash) {
@@ -7011,8 +7879,13 @@ async function stampedResponse(req, res, ctx) {
7011
7879
  await routeRequest(req, held.res, ctx);
7012
7880
  const type = String(held.headers["content-type"] ?? "");
7013
7881
  let body = held.body();
7014
- if (/^text\/html\b/.test(type)) body = await stampHtml(body.toString("utf8"), hashOfFor(req, ctx));
7015
- 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);
7016
7889
  res.writeHead(held.status(), held.headers);
7017
7890
  res.end(body);
7018
7891
  }
@@ -7061,10 +7934,10 @@ async function routeRequest(req, res, ctx) {
7061
7934
  return;
7062
7935
  }
7063
7936
  if (pathname === PD_ROUTES.mainJs) {
7064
- return await serveFile(res, join24(devUiDist2, "main.js"), "application/javascript");
7937
+ return await serveFile(res, join25(devUiDist2, "main.js"), "application/javascript");
7065
7938
  }
7066
7939
  if (pathname === PD_ROUTES.mainCss) {
7067
- return await serveFile(res, join24(devUiDist2, "main.css"), "text/css");
7940
+ return await serveFile(res, join25(devUiDist2, "main.css"), "text/css");
7068
7941
  }
7069
7942
  if (pathname === PD_ROUTES.axeCore) {
7070
7943
  return await serveFile(res, resolveAxeCore(import.meta.url), "application/javascript");
@@ -7382,7 +8255,7 @@ async function routeRequest(req, res, ctx) {
7382
8255
  if (cfg.storefront && host === cfg.storefront.host) {
7383
8256
  const discovery = await currentDiscovery(state, patternRoots);
7384
8257
  const discoveryMap = discovery.discoveryMap;
7385
- 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" });
7386
8259
  if (pathname === PD_ROUTES.pages) {
7387
8260
  if (model.problems.length) {
7388
8261
  json(res, 500, { error: model.problems.join("\n") });
@@ -7391,8 +8264,8 @@ async function routeRequest(req, res, ctx) {
7391
8264
  json(res, 200, { pages: model.pages, nav: model.nav });
7392
8265
  return;
7393
8266
  }
7394
- const siteUrl = cfg.storefront.url === void 0 ? void 0 : ctx.live ? `http://${req.headers.host ?? cfg.storefront.host}/` : cfg.storefront.url;
7395
- 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 === "/") {
7396
8269
  res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
7397
8270
  res.end(storefrontRobotsTxt({ siteUrl, index: cfg.storefront.index, workbenchPath: sameHostWorkbenchPath(cfg.storefront) }));
7398
8271
  return;
@@ -7431,13 +8304,15 @@ async function routeRequest(req, res, ctx) {
7431
8304
  render,
7432
8305
  site: ctx.site.get(),
7433
8306
  workbenchUrl: ctx.storefrontWorkbenchUrl ?? `http://${PD_HOSTS.workbench}${port2 ? `:${port2}` : ""}/`,
7434
- head: (layoutId, html) => pageStylesheets(cfg, state, discovery, layoutId, html),
8307
+ head: (layoutId, html, placed) => pageStylesheets(cfg, state, discovery, layoutId, html, placed),
7435
8308
  ...ctx.dates ? { dates: ctx.dates } : {},
7436
8309
  ...ctx.live ? { live: { wsPort, generation: generation ?? "" } } : {},
7437
8310
  image: (path) => checkPageImage(path, assetRoots, assetRootNames),
7438
8311
  ...siteUrl !== void 0 ? { siteUrl } : {},
7439
8312
  lang: cfg.storefront.lang,
7440
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 } } : {},
7441
8316
  ...cfg.storefront.image !== void 0 ? { siteImage: cfg.storefront.image } : {},
7442
8317
  ...ctx.images ? { images: ctx.images } : {}
7443
8318
  });
@@ -7530,7 +8405,7 @@ function createRenderingHandle(build, registrars, globalData) {
7530
8405
 
7531
8406
  // packages/dev-server/src/watch-roots.ts
7532
8407
  import { statSync as statSync3 } from "node:fs";
7533
- 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";
7534
8409
  var MODULE = /\.[mc]?js$/;
7535
8410
  function helperScope(path) {
7536
8411
  let isDir = false;
@@ -7549,23 +8424,23 @@ function watchedRoots(cfg) {
7549
8424
  styles: [...projectStyles, ...packageStyles].map((r) => ({ dir: r.dir, inBase: documentBaseMatcher(r.dir, r.base) })),
7550
8425
  themes: [
7551
8426
  ...cfg.config.themes?.dir && cfg.themesDir ? [cfg.themesDir] : [],
7552
- ...[...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))
7553
8428
  ],
7554
8429
  scripts: [
7555
8430
  ...cfg.config.scriptsDir && cfg.scriptsDirs[0] ? [cfg.scriptsDirs[0]] : [],
7556
- ...[...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))
7557
8432
  ],
7558
8433
  data: [
7559
8434
  ...cfg.config.dataDir ? [resolve9(cfg.cwd, cfg.config.dataDir)] : [],
7560
- ...[...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))
7561
8436
  ],
7562
8437
  assets: [
7563
8438
  ...[cfg.config.assets?.dir ?? []].flat().map((d) => resolve9(cfg.cwd, d)),
7564
- ...[...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)))
7565
8440
  ],
7566
8441
  helpers: [
7567
8442
  ...cfg.config.helpers ? [resolve9(cfg.cwd, cfg.config.helpers)] : [],
7568
- ...[...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))
7569
8444
  ].map(helperScope)
7570
8445
  };
7571
8446
  }
@@ -7596,10 +8471,10 @@ function rootMatchers(watched) {
7596
8471
  }
7597
8472
 
7598
8473
  // packages/dev-server/src/watch.ts
7599
- import { createHash as createHash6 } from "node:crypto";
7600
- import { existsSync as existsSync6, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs";
8474
+ import { createHash as createHash7 } from "node:crypto";
8475
+ import { existsSync as existsSync6, readFileSync as readFileSync4, statSync as statSync4 } from "node:fs";
7601
8476
  import { appendFile, readdir as readdir9 } from "node:fs/promises";
7602
- 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";
7603
8478
  import chokidar from "chokidar";
7604
8479
 
7605
8480
  // packages/dev-server/src/config-change.ts
@@ -7658,7 +8533,7 @@ function seenNow(path) {
7658
8533
  const s = statSync4(path);
7659
8534
  const stamp2 = `${s.ino}:${s.ctimeMs}:${s.mtimeMs}:${s.size}`;
7660
8535
  const forced = globalThis.__testOnly_watchStamp;
7661
- return { stamp: forced ? forced(path, stamp2) : stamp2, real: stamp2, hash: createHash6("sha1").update(readFileSync3(path)).digest("hex") };
8536
+ return { stamp: forced ? forced(path, stamp2) : stamp2, real: stamp2, hash: createHash7("sha1").update(readFileSync4(path)).digest("hex") };
7662
8537
  } catch {
7663
8538
  return null;
7664
8539
  }
@@ -7689,7 +8564,7 @@ async function startWatching(ctx) {
7689
8564
  const barrierWaiters = /* @__PURE__ */ new Set();
7690
8565
  const SETTLE_MS = 1e4;
7691
8566
  async function settle() {
7692
- 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));
7693
8568
  const waiting = new Set(files);
7694
8569
  const handled = new Promise((resolve15, reject) => {
7695
8570
  if (!waiting.size) {
@@ -7947,7 +8822,7 @@ async function startWatching(ctx) {
7947
8822
  }
7948
8823
  for (const entry of entries) {
7949
8824
  if (closed) return true;
7950
- const path = join26(dir, entry.name);
8825
+ const path = join27(dir, entry.name);
7951
8826
  if (entry.isDirectory()) {
7952
8827
  if (firstReport(path)) dispatchers.addDir?.(path);
7953
8828
  await scanFolder(path);
@@ -8057,7 +8932,11 @@ async function startWatching(ctx) {
8057
8932
  const compId = componentOfScss(resolve10(recompiledFile), allIds);
8058
8933
  const root = styleRootOf(resolve10(recompiledFile));
8059
8934
  if (compId && root) {
8060
- 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
+ }
8061
8940
  if (newCss !== null) {
8062
8941
  const wasEmpty = (state.perComponentCss.get(compId) ?? "") === "";
8063
8942
  state.perComponentCss.set(compId, newCss);
@@ -8259,7 +9138,7 @@ async function startWatching(ctx) {
8259
9138
  async function pagesFingerprint() {
8260
9139
  if (!pagesDir) return null;
8261
9140
  const model = await readPages(pagesDir, { workbenchPath });
8262
- 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] });
8263
9142
  }
8264
9143
  async function handlePages(filePath, event) {
8265
9144
  if (!inPages(filePath)) return false;
@@ -8564,7 +9443,7 @@ function createGitDates(pagesDir, listPages, git = runGit) {
8564
9443
 
8565
9444
  // packages/dev-server/src/export-io.ts
8566
9445
  import { readFile as readFile25, writeFile as writeFile6, mkdir as mkdir6 } from "node:fs/promises";
8567
- 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";
8568
9447
 
8569
9448
  // packages/dev-server/src/export.ts
8570
9449
  var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -8757,20 +9636,20 @@ async function writeExport(families, sink) {
8757
9636
  const files = [];
8758
9637
  for (const family of families) {
8759
9638
  const baseRel = `${family.baseId}.hbs`;
8760
- const basePath = join27(sink, baseRel);
9639
+ const basePath = join28(sink, baseRel);
8761
9640
  await mkdir6(dirname10(basePath), { recursive: true });
8762
9641
  await writeFile6(basePath, family.baseHbs, "utf-8");
8763
9642
  files.push(baseRel);
8764
9643
  if (family.baseData) {
8765
9644
  const dataRel = `${family.baseId}.json`;
8766
- 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");
8767
9646
  files.push(dataRel);
8768
9647
  }
8769
9648
  for (const v of family.variants) {
8770
9649
  if (v.token === "") continue;
8771
9650
  const overlayRel = `${family.baseId}~${v.token}.json`;
8772
9651
  const overlay = { modifier: v.modifier, ...v.data ?? {} };
8773
- 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");
8774
9653
  files.push(overlayRel);
8775
9654
  }
8776
9655
  }
@@ -8802,7 +9681,7 @@ async function writeThreadedIncluders(threaded, patternsDir, sink) {
8802
9681
  if (res.rewrites === 0) continue;
8803
9682
  rewrites += res.rewrites;
8804
9683
  const rel = relative15(patternsDir, hbsPath);
8805
- const outPath = join27(sink, rel);
9684
+ const outPath = join28(sink, rel);
8806
9685
  await mkdir6(dirname10(outPath), { recursive: true });
8807
9686
  await writeFile6(outPath, res.hbs, "utf-8");
8808
9687
  files.push(rel);
@@ -8958,6 +9837,16 @@ async function startDevServer(config, cwd, opts = {}) {
8958
9837
  build: opts.live === false,
8959
9838
  warn: (message) => console.warn(`[dev-server] ${message}`)
8960
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
+ };
8961
9850
  const buildRenderer = (registrars, data) => {
8962
9851
  const renderWith = createRenderer({
8963
9852
  patternsDir,
@@ -8971,7 +9860,9 @@ async function startDevServer(config, cwd, opts = {}) {
8971
9860
  });
8972
9861
  return async (input) => {
8973
9862
  await images.ready(generationNow());
8974
- return renderWith(input);
9863
+ const result = await renderWith(input);
9864
+ reportHelperNames(result.meta?.diagnostics);
9865
+ return result;
8975
9866
  };
8976
9867
  };
8977
9868
  const rendering = createRenderingHandle(buildRenderer, helperRegistrars, globalData);
@@ -8995,7 +9886,7 @@ async function startDevServer(config, cwd, opts = {}) {
8995
9886
  if (cfg.themeDirs.length) {
8996
9887
  for (const { name, dir } of await discoverThemeFiles(cfg.themeDirs)) {
8997
9888
  try {
8998
- const css = await compileSassFile(join28(dir, `${name}.theme.scss`));
9889
+ const css = await compileSassFile(join29(dir, `${name}.theme.scss`));
8999
9890
  state.themeCss.set(name, css);
9000
9891
  const slots = config.themes?.dir && dir === cfg.themeDirs[0] ? swatchSlots : cfg.packageSwatchSlots;
9001
9892
  state.themeTokens.set(name, await extractThemeTokens(name, css, slots, dir));
@@ -9071,7 +9962,7 @@ async function startDevServer(config, cwd, opts = {}) {
9071
9962
  httpPort = actualPort;
9072
9963
  console.log(`[dev-server] Listening at http://127.0.0.1:${actualPort}`);
9073
9964
  if (live) {
9074
- 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}`);
9075
9966
  console.log(`[dev-server] Workbench: http://${cfg.storefront ? PD_HOSTS.workbench : "localhost"}:${actualPort}/`);
9076
9967
  }
9077
9968
  const startupDiscovery = await cachedDiscovery(state);
@@ -9116,10 +10007,10 @@ async function startDevServer(config, cwd, opts = {}) {
9116
10007
  import { build as esbuild } from "esbuild";
9117
10008
  import { existsSync as existsSync7 } from "node:fs";
9118
10009
  import { readFile as readFile26, writeFile as writeFile7, unlink, mkdir as mkdir7 } from "node:fs/promises";
9119
- 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";
9120
10011
  import { tmpdir as tmpdir2 } from "node:os";
9121
- import { createHash as createHash7, randomUUID as randomUUID3 } from "node:crypto";
9122
- 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";
9123
10014
  async function runDev(root, opts, configPath) {
9124
10015
  const config = await loadConfig(root, configPath);
9125
10016
  const file = configFileOf(root, configPath);
@@ -9132,6 +10023,7 @@ async function runBuild(root, configPath, options) {
9132
10023
  outDir,
9133
10024
  ...options.base !== void 0 ? { basePath: options.base } : {},
9134
10025
  ...options.audit !== void 0 ? { audit: options.audit } : {},
10026
+ ...options.browser ? { browser: true } : {},
9135
10027
  pdVersion: options.pdVersion,
9136
10028
  log: (line) => console.log(`[pd build] ${line}`)
9137
10029
  });
@@ -9139,11 +10031,24 @@ async function runBuild(root, configPath, options) {
9139
10031
  console.log(`[pd build] ${report.components} components \u2192 ${report.files} files, ${mb} MB in ${report.ms} ms`);
9140
10032
  console.log(`[pd build] ${report.outDir} (served from ${report.basePath})`);
9141
10033
  if (report.storefront) {
9142
- 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})`);
9143
10035
  }
9144
10036
  if (report.links) {
9145
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)` : ""));
9146
10038
  }
10039
+ if (report.accessibility) {
10040
+ const n = report.accessibility.warnings.length;
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:` : ""));
10042
+ for (const w of report.accessibility.warnings) console.log(`[pd build] warning: ${w}`);
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
+ }
9147
10052
  for (const w of report.warnings) console.log(`[pd build] not built: ${w}`);
9148
10053
  for (const n of report.notes) console.log(`[pd build] note: ${n}`);
9149
10054
  }
@@ -9152,7 +10057,7 @@ async function runAudit(root, configPath, format = "md") {
9152
10057
  const { graph, reaudited, reused, artifactPath, imageWarning } = await runDiagnosticsAudit(config, root);
9153
10058
  const report = formatAudit(graph, format);
9154
10059
  const ext = format === "json" ? "json" : format === "text" ? "txt" : "md";
9155
- const reportPath = join29(dirname11(artifactPath), `diagnostics-report.${ext}`);
10060
+ const reportPath = join30(dirname11(artifactPath), `diagnostics-report.${ext}`);
9156
10061
  await writeFile7(reportPath, report, "utf-8");
9157
10062
  console.log(report);
9158
10063
  const meta = format === "json" ? console.error : console.log;
@@ -9170,8 +10075,8 @@ function auditSummaryLines(r) {
9170
10075
  ];
9171
10076
  }
9172
10077
  function defaultExportDir(root) {
9173
- const key = createHash7("sha1").update(resolve12(root)).digest("hex").slice(0, 16);
9174
- 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");
9175
10080
  }
9176
10081
  function assertOutsideRoot(outDir, root) {
9177
10082
  const rel = relative16(resolve12(root), resolve12(outDir));
@@ -9248,18 +10153,30 @@ async function loadConfig(root, configPath) {
9248
10153
  if (!existsSync7(configPath)) {
9249
10154
  throw new ConfigError(`--config file not found: ${configPath}`);
9250
10155
  }
9251
- return validateConfig(await readConfigFile(configPath), configPath);
10156
+ return validateConfig(await withRootManifest(root, await readConfigFile(configPath), configPath), configPath);
9252
10157
  }
9253
10158
  const path = configFileOf(root);
9254
- if (path) return validateConfig(await readConfigFile(path), basename11(path));
10159
+ if (path) return validateConfig(await withRootManifest(root, await readConfigFile(path), basename11(path)), basename11(path));
9255
10160
  throw new ConfigError(
9256
10161
  `No pattern-dump.config.ts or pattern-dump.config.json found in ${root} (and no --config given)`
9257
10162
  );
9258
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
+ }
9259
10176
  function configFileOf(root, configPath) {
9260
10177
  if (configPath) return configPath;
9261
10178
  for (const name of CONFIG_NAMES) {
9262
- const path = join29(root, name);
10179
+ const path = join30(root, name);
9263
10180
  if (existsSync7(path)) return path;
9264
10181
  }
9265
10182
  return void 0;
@@ -9269,7 +10186,7 @@ async function readConfigFile(path) {
9269
10186
  return path.endsWith(".json") ? JSON.parse(await readFile26(path, "utf-8")) : loadTsConfig(path);
9270
10187
  }
9271
10188
  async function loadTsConfig(tsPath) {
9272
- const tmp = join29(tmpdir2(), `pd-config-${process.pid}-${randomUUID3()}.mjs`);
10189
+ const tmp = join30(tmpdir2(), `pd-config-${process.pid}-${randomUUID3()}.mjs`);
9273
10190
  try {
9274
10191
  await esbuild({
9275
10192
  entryPoints: [tsPath],
@@ -9279,7 +10196,7 @@ async function loadTsConfig(tsPath) {
9279
10196
  // No packages:"external" — the temp file runs from /tmp so we must inline deps.
9280
10197
  outfile: tmp
9281
10198
  });
9282
- const { default: cfg } = await import(pathToFileURL2(tmp).href);
10199
+ const { default: cfg } = await import(pathToFileURL3(tmp).href);
9283
10200
  return cfg;
9284
10201
  } finally {
9285
10202
  await unlink(tmp).catch(() => {
@@ -9359,7 +10276,8 @@ function parseCommand(argv, cwd) {
9359
10276
  out: { type: "string" },
9360
10277
  base: { type: "string" },
9361
10278
  audit: { type: "boolean" },
9362
- "no-audit": { type: "boolean" }
10279
+ "no-audit": { type: "boolean" },
10280
+ browser: { type: "boolean" }
9363
10281
  }
9364
10282
  });
9365
10283
  if (values2.help) return { kind: "help", topic: cmd };
@@ -9370,7 +10288,8 @@ function parseCommand(argv, cwd) {
9370
10288
  ...base(values2, cwd),
9371
10289
  ...values2.out !== void 0 ? { out: values2.out } : {},
9372
10290
  ...values2.base !== void 0 ? { base: values2.base } : {},
9373
- ...audit !== void 0 ? { audit } : {}
10291
+ ...audit !== void 0 ? { audit } : {},
10292
+ ...values2.browser ? { browser: true } : {}
9374
10293
  };
9375
10294
  }
9376
10295
  const { values } = parseArgs({
@@ -9467,6 +10386,9 @@ var USAGE = `Usage: pd <command> [options]
9467
10386
  --config <path> Use a config file outside the root
9468
10387
  --out <dir> Output folder, relative to the root (default: build.outDir, outDir, or dist)
9469
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)
9470
10392
  --no-audit Leave the corpus audit out: it is not run, and the site has no Audit tab
9471
10393
  (default: build.audit in the config; --audit forces it back in)
9472
10394
 
@@ -9480,12 +10402,12 @@ var USAGE = `Usage: pd <command> [options]
9480
10402
  // packages/cli/src/init/run.ts
9481
10403
  import { existsSync as existsSync8 } from "node:fs";
9482
10404
  import { readFile as readFile28 } from "node:fs/promises";
9483
- 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";
9484
10406
 
9485
10407
  // packages/cli/package.json
9486
10408
  var package_default = {
9487
10409
  name: "@pattern-dump/cli",
9488
- version: "0.2.0-alpha.0",
10410
+ version: "0.4.0",
9489
10411
  private: true,
9490
10412
  description: "The pd command and the published pattern-dump package.",
9491
10413
  type: "module",
@@ -9521,12 +10443,12 @@ var PD_VERSION = package_default.version;
9521
10443
 
9522
10444
  // packages/cli/src/init/files.ts
9523
10445
  import { mkdir as mkdir8, readdir as readdir10, readFile as readFile27, writeFile as writeFile8 } from "node:fs/promises";
9524
- 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";
9525
10447
  async function readTemplateDir(dir) {
9526
10448
  const out = [];
9527
10449
  async function walk(current) {
9528
10450
  for (const entry of await readdir10(current, { withFileTypes: true })) {
9529
- const full = join30(current, entry.name);
10451
+ const full = join31(current, entry.name);
9530
10452
  if (entry.isDirectory()) await walk(full);
9531
10453
  else out.push({ path: relative17(dir, full).split(sep12).join("/"), content: await readFile27(full, "utf8") });
9532
10454
  }
@@ -9535,11 +10457,11 @@ async function readTemplateDir(dir) {
9535
10457
  return out.sort((a, b) => a.path.localeCompare(b.path));
9536
10458
  }
9537
10459
  async function readTemplates(templatesDir) {
9538
- const fill = (name) => readFile27(join30(templatesDir, "storefront-fills", name), "utf8");
10460
+ const fill = (name) => readFile27(join31(templatesDir, "storefront-fills", name), "utf8");
9539
10461
  const [base2, examples, storefront, config, readme, gitignore] = await Promise.all([
9540
- readTemplateDir(join30(templatesDir, "base")),
9541
- readTemplateDir(join30(templatesDir, "examples")),
9542
- readTemplateDir(join30(templatesDir, "storefront")),
10462
+ readTemplateDir(join31(templatesDir, "base")),
10463
+ readTemplateDir(join31(templatesDir, "examples")),
10464
+ readTemplateDir(join31(templatesDir, "storefront")),
9543
10465
  fill("config.txt"),
9544
10466
  fill("readme.md"),
9545
10467
  fill("gitignore.txt")
@@ -9549,12 +10471,12 @@ async function readTemplates(templatesDir) {
9549
10471
  async function applyPlan(target, plan) {
9550
10472
  await mkdir8(target, { recursive: true });
9551
10473
  for (const file of plan.writes) {
9552
- const abs = join30(target, file.path);
10474
+ const abs = join31(target, file.path);
9553
10475
  await mkdir8(dirname12(abs), { recursive: true });
9554
10476
  await writeFile8(abs, file.content, { flag: "wx" });
9555
10477
  }
9556
- await writeFile8(join30(target, "package.json"), plan.packageJson.content);
9557
- 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 });
9558
10480
  }
9559
10481
 
9560
10482
  // packages/cli/src/init/plan.ts
@@ -9676,7 +10598,7 @@ async function runInit(cmd, io) {
9676
10598
  } else {
9677
10599
  dependency = dependencySpec(void 0, io.cwd);
9678
10600
  }
9679
- const pkgPath = join31(target, "package.json");
10601
+ const pkgPath = join32(target, "package.json");
9680
10602
  let packageJson = null;
9681
10603
  if (existsSync8(pkgPath)) {
9682
10604
  try {
@@ -9697,7 +10619,7 @@ async function runInit(cmd, io) {
9697
10619
  const install = cmd.install ?? (prompt ? await io.confirm("Run npm install", true) : true);
9698
10620
  const answers = { name, brand, examples, storefront, dependency };
9699
10621
  const templates = await readTemplates(io.templatesDir);
9700
- 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);
9701
10623
  if (!result.ok) {
9702
10624
  io.log(`\u2717 ${result.reason}`);
9703
10625
  return 1;
@@ -9746,7 +10668,7 @@ async function runInit(cmd, io) {
9746
10668
 
9747
10669
  // packages/cli/src/init/local-build.ts
9748
10670
  import { existsSync as existsSync9, statSync as statSync5 } from "node:fs";
9749
- import { join as join32 } from "node:path";
10671
+ import { join as join33 } from "node:path";
9750
10672
  import { fileURLToPath as fileURLToPath7 } from "node:url";
9751
10673
  var BUILT = [
9752
10674
  "packages/cli/dist/index.js",
@@ -9764,10 +10686,10 @@ var mtime = (path) => {
9764
10686
  };
9765
10687
  function detectLocalBuild(moduleUrl, packageName, version) {
9766
10688
  const repo = fileURLToPath7(new URL("../../../", moduleUrl)).replace(/[\\/]$/, "");
9767
- if (!existsSync9(join32(repo, "packages/cli/src/distribution.ts"))) return null;
9768
- 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`);
9769
10691
  if (!existsSync9(tarball)) return { repo, tarball: null, stale: false };
9770
- const newestBuild = Math.max(...BUILT.map((p) => mtime(join32(repo, p))));
10692
+ const newestBuild = Math.max(...BUILT.map((p) => mtime(join33(repo, p))));
9771
10693
  return { repo, tarball, stale: newestBuild > mtime(tarball) };
9772
10694
  }
9773
10695
 
@@ -9834,6 +10756,7 @@ switch (command.kind) {
9834
10756
  ...command.out !== void 0 ? { out: command.out } : {},
9835
10757
  ...command.base !== void 0 ? { base: command.base } : {},
9836
10758
  ...command.audit !== void 0 ? { audit: command.audit } : {},
10759
+ ...command.browser ? { browser: true } : {},
9837
10760
  pdVersion: PD_VERSION
9838
10761
  }).then(() => process.stdout.write("", () => process.exit(0))).catch((e) => {
9839
10762
  console.error("[pd] Error:", e instanceof Error ? e.message : e);