pattern-dump 0.2.0-alpha.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ From this release Pattern Dump is published as plain 0.x versions under npm's `latest` tag: install it with
6
+ `npx pattern-dump init`, without `@alpha`. Being at 0.x, breaking changes are possible between minor versions, and
7
+ this changelog names them.
8
+
9
+ ### Added
10
+
11
+ - `sourceDir`, `patternsDir` and `styles` are optional. A site whose components, skin and styles all come from
12
+ pattern packages names none of them; they default to `./src`, `./src/patterns` and `./src/styles`, and the folders
13
+ don't have to exist.
14
+
15
+ ### Changed
16
+
17
+ - `pd build` now checks every page of a storefront with axe before it writes anything, and **stops on an
18
+ accessibility error**: a WCAG rule rated critical or serious (an image without alt text, a button with no name, a
19
+ page without a language), or a page without exactly one `<main>` and one `<h1>`. Each error is printed with its
20
+ page, the rule, the elements it was found on and a link to the rule's explanation; moderate and minor findings,
21
+ and axe's best practices, are listed as warnings and don't stop the build. To see them before a build, run
22
+ `pd build` locally: nothing is written while there are errors. In `pd dev`, a page without one `<main>` and one
23
+ `<h1>` is a warning in the terminal.
24
+
3
25
  ## 0.2.0-alpha.0
4
26
 
5
27
  The storefront: a website written as markdown pages, dressed by a skin made of ordinary components, served by
package/README.md CHANGED
@@ -4,19 +4,19 @@ A workbench for HTML and CSS design systems, and a successor to PatternLab. Writ
4
4
  Handlebars templates with SCSS, data and Markdown docs; Pattern Dump shows them live, checks them, maps
5
5
  how they relate, and publishes the result as a static site.
6
6
 
7
- > **Alpha (0.2.0-alpha.0).** Published under the `alpha` tag. Options and file conventions can still change
8
- > between releases.
7
+ > **0.3.0.** Pattern Dump is at 0.x, so breaking changes are possible between minor versions (0.3 to 0.4);
8
+ > the [changelog](https://github.com/n8design/pattern-dump/blob/main/CHANGELOG.md) names them.
9
9
 
10
10
  ## Requirements
11
11
 
12
- Node.js 20.10 or later, and npm.
12
+ Node.js 22.22 or later, and npm.
13
13
 
14
14
  ## Quick start
15
15
 
16
16
  Create a new project:
17
17
 
18
18
  ```bash
19
- npx pattern-dump@alpha init my-design-system
19
+ npx pattern-dump init my-design-system
20
20
  cd my-design-system
21
21
  npm run dev # → http://localhost:4000
22
22
  ```
@@ -24,7 +24,7 @@ npm run dev # → http://localhost:4000
24
24
  Or add Pattern Dump to an existing project:
25
25
 
26
26
  ```bash
27
- npm install --save-dev pattern-dump@alpha
27
+ npm install --save-dev pattern-dump
28
28
  npx pd init
29
29
  ```
30
30
 
@@ -35,7 +35,7 @@ overwrites an existing file.
35
35
  ### Installing `pd` globally
36
36
 
37
37
  ```bash
38
- npm install --global pattern-dump@alpha
38
+ npm install --global pattern-dump
39
39
  pd init my-design-system
40
40
  ```
41
41
 
@@ -59,7 +59,7 @@ var SHAPE = {
59
59
  required: ["pagesDir", "outDir"]
60
60
  }
61
61
  };
62
- var REQUIRED = ["sourceDir", "patternsDir", "outDir", "styles"];
62
+ var REQUIRED = ["outDir"];
63
63
  var WORKBENCH_PATH = /^\/(?:(?!\.{1,2}(?:\/|$))[a-z0-9._~-]+\/?)+$/i;
64
64
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
65
65
  var isStrings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
@@ -242,13 +242,24 @@ function configProblems(raw) {
242
242
  return problems;
243
243
  }
244
244
  var withOneTrailingSlash = (url) => `${url.replace(/\/+$/, "")}/`;
245
+ function withSourceDefaults(config) {
246
+ if (config.sourceDir !== void 0 && config.patternsDir !== void 0 && config.styles !== void 0) return config;
247
+ const sourceDir = config.sourceDir ?? "./src";
248
+ const base = sourceDir.replace(/\/+$/, "");
249
+ return {
250
+ ...config,
251
+ sourceDir,
252
+ patternsDir: config.patternsDir ?? `${base}/patterns`,
253
+ styles: config.styles ?? { strategy: "parallel", dir: `${base}/styles` }
254
+ };
255
+ }
245
256
  function validateConfig(raw, source) {
246
257
  const problems = configProblems(raw);
247
258
  if (problems.length > 0) {
248
259
  throw new ConfigError(`${source}: invalid config
249
260
  ${problems.map((p) => ` - ${p}`).join("\n")}`);
250
261
  }
251
- const config = raw;
262
+ const config = withSourceDefaults(raw);
252
263
  const sf = config.storefront;
253
264
  if (!sf) return config;
254
265
  const workbenchUrl = sf.workbenchUrl !== void 0 ? withOneTrailingSlash(sf.workbenchUrl) : void 0;
@@ -271,7 +282,7 @@ function sameHostWorkbenchPath(storefront) {
271
282
 
272
283
  // packages/config/src/index.ts
273
284
  function defineConfig(config) {
274
- return config;
285
+ return withSourceDefaults(config);
275
286
  }
276
287
 
277
288
  export {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  defineConfig
3
- } from "./chunks/chunk-ESMZAOP2.js";
3
+ } from "./chunks/chunk-5WFBNPXB.js";
4
4
  export {
5
5
  defineConfig
6
6
  };
package/dist/pd.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  sameHostWorkbenchPath,
5
5
  suggest,
6
6
  validateConfig
7
- } from "./chunks/chunk-ESMZAOP2.js";
7
+ } from "./chunks/chunk-5WFBNPXB.js";
8
8
 
9
9
  // packages/contracts/src/dev-protocol.ts
10
10
  var AUDIT_SCOPE = {
@@ -335,9 +335,9 @@ async function buildDiscoveryMap(roots) {
335
335
  const dir = dirname(absPath);
336
336
  const stem = basename(absPath, ".hbs");
337
337
  const fileSet = dirFileSets.get(dir) ?? /* @__PURE__ */ new Set();
338
- const source = rootOf.get(absPath)?.root.source;
338
+ const source2 = rootOf.get(absPath)?.root.source;
339
339
  return {
340
- ...source ? { source } : {},
340
+ ...source2 ? { source: source2 } : {},
341
341
  hbsPath: absPath,
342
342
  jsonPath: fileSet.has(`${stem}.json`) ? join(dir, `${stem}.json`) : void 0,
343
343
  mdPath: fileSet.has(`${stem}.md`) ? join(dir, `${stem}.md`) : void 0,
@@ -395,8 +395,8 @@ async function buildDiscoveryMap(roots) {
395
395
  const baseName = basename(varPaths[0], ".hbs").split("~")[0];
396
396
  const fileSet = dirFileSets.get(varDir) ?? /* @__PURE__ */ new Set();
397
397
  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 });
398
+ const source2 = rootOf.get(varPaths[0])?.root.source;
399
+ discoveryMap.set(baseId, { ...source2 ? { source: source2 } : {}, hbsPath: "", ownMarkup: false, variants: variantTokens, mdPath });
400
400
  handleMap.set(handle, baseId);
401
401
  }
402
402
  }
@@ -426,8 +426,8 @@ async function buildDiscoveryMap(roots) {
426
426
  if (!baseEntry?.hbsPath) continue;
427
427
  const variantHandle = `${deriveHandle(baseId)}~${variantToken}`;
428
428
  if (!handleMap.has(variantHandle)) {
429
- const source = rootOf.get(jsonPath)?.root.source;
430
- discoveryMap.set(variantId, { ...source ? { source } : {}, hbsPath: baseEntry.hbsPath, jsonPath, ownMarkup: false });
429
+ const source2 = rootOf.get(jsonPath)?.root.source;
430
+ discoveryMap.set(variantId, { ...source2 ? { source: source2 } : {}, hbsPath: baseEntry.hbsPath, jsonPath, ownMarkup: false });
431
431
  handleMap.set(variantHandle, variantId);
432
432
  }
433
433
  }
@@ -911,9 +911,9 @@ var htmlValidator = new HtmlValidate({
911
911
  }
912
912
  });
913
913
  function deepMerge(target, ...sources) {
914
- for (const source of sources) {
915
- if (!source) continue;
916
- for (const [key, value] of Object.entries(source)) {
914
+ for (const source2 of sources) {
915
+ if (!source2) continue;
916
+ for (const [key, value] of Object.entries(source2)) {
917
917
  const targetVal = target[key];
918
918
  if (value !== null && typeof value === "object" && !Array.isArray(value) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
919
919
  deepMerge(targetVal, value);
@@ -955,16 +955,16 @@ async function readDataFile(path, display) {
955
955
  };
956
956
  }
957
957
  }
958
- function imageTag(source, o) {
958
+ function imageTag(source2, o) {
959
959
  const esc = Handlebars3.escapeExpression;
960
960
  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));
961
+ if (source2.kind === "single") return `<img src="${esc(source2.url)}" width="${source2.width}" height="${source2.height}" ${rest}>`;
962
+ const widths = o.widths.filter((w) => w <= source2.width);
963
+ if (widths.length === 0) widths.push(source2.width);
964
+ const urls = widths.map((w) => source2.variantUrl(w, o.settings));
965
965
  const top = widths[widths.length - 1];
966
966
  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}>`;
967
+ 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
968
  }
969
969
  function registerImageHelper(env, images) {
970
970
  env.registerHelper("pd-image", function(value, options) {
@@ -979,11 +979,11 @@ function registerImageHelper(env, images) {
979
979
  }
980
980
  if (typeof value !== "string" || value === "") throw new Error(`{{pd-image}}: the image field is empty`);
981
981
  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);
982
+ const source2 = images(value);
983
+ if (!source2) throw new Error(`{{pd-image}}: "${value}" is not an image in the assets`);
984
+ if (source2.kind === "unavailable") throw new Error(source2.reason);
985
985
  const settings = options.data?.pdImages;
986
- return new env.SafeString(imageTag(source, {
986
+ return new env.SafeString(imageTag(source2, {
987
987
  widths: slot.widths,
988
988
  sizes: slot.sizes,
989
989
  alt: String(alt),
@@ -1373,21 +1373,21 @@ function loadSharp(importer) {
1373
1373
  if (!importer) loaded = attempt;
1374
1374
  return attempt;
1375
1375
  }
1376
- function imageVariantKey(source, width, encoding) {
1377
- return imageVariantKeyFromSourceHash(imageSourceHash(source), width, encoding);
1376
+ function imageVariantKey(source2, width, encoding) {
1377
+ return imageVariantKeyFromSourceHash(imageSourceHash(source2), width, encoding);
1378
1378
  }
1379
- function imageSourceHash(source) {
1380
- return createHash("sha256").update(source).digest("hex");
1379
+ function imageSourceHash(source2) {
1380
+ return createHash("sha256").update(source2).digest("hex");
1381
1381
  }
1382
1382
  function imageVariantKeyFromSourceHash(sourceHash, width, encoding) {
1383
1383
  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
1384
  }
1385
- async function imageSize(sharp, source) {
1386
- const meta = await sharp(source).metadata();
1385
+ async function imageSize(sharp, source2) {
1386
+ const meta = await sharp(source2).metadata();
1387
1387
  return { width: meta.autoOrient?.width ?? meta.width ?? 0, height: meta.autoOrient?.height ?? meta.height ?? 0 };
1388
1388
  }
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();
1389
+ function encodeWebp(sharp, source2, width, encoding) {
1390
+ return sharp(source2, { autoOrient: true }).resize({ width, withoutEnlargement: true }).webp({ quality: encoding.quality, effort: encoding.effort }).toBuffer();
1391
1391
  }
1392
1392
  function createImageCache(dir) {
1393
1393
  const file = (key) => join2(dir, `${key}.webp`);
@@ -2960,6 +2960,163 @@ async function runA11yProducer(html) {
2960
2960
  return diagnostics;
2961
2961
  }
2962
2962
 
2963
+ // packages/a11y/src/page.ts
2964
+ import { readFileSync as readFileSync2 } from "node:fs";
2965
+ import { createRequire as createRequire3 } from "node:module";
2966
+ import { JSDOM as JSDOM2 } from "jsdom";
2967
+
2968
+ // packages/a11y/src/page-rule-tiers.generated.json
2969
+ var page_rule_tiers_generated_default = {
2970
+ _note: "Generated by packages/a11y/src/page-classifier.ts \u2014 do not hand-edit. Re-run: npm run classify-pages --workspace @pattern-dump/a11y",
2971
+ axeVersion: "4.11.4",
2972
+ tags: [
2973
+ "wcag2a",
2974
+ "wcag2aa",
2975
+ "wcag21a",
2976
+ "wcag21aa",
2977
+ "wcag22aa",
2978
+ "best-practice"
2979
+ ],
2980
+ jsdom: [
2981
+ "accesskeys",
2982
+ "area-alt",
2983
+ "aria-allowed-attr",
2984
+ "aria-allowed-role",
2985
+ "aria-command-name",
2986
+ "aria-conditional-attr",
2987
+ "aria-deprecated-role",
2988
+ "aria-dialog-name",
2989
+ "aria-hidden-body",
2990
+ "aria-input-field-name",
2991
+ "aria-meter-name",
2992
+ "aria-progressbar-name",
2993
+ "aria-required-attr",
2994
+ "aria-required-children",
2995
+ "aria-required-parent",
2996
+ "aria-roledescription",
2997
+ "aria-roles",
2998
+ "aria-text",
2999
+ "aria-toggle-field-name",
3000
+ "aria-tooltip-name",
3001
+ "aria-treeitem-name",
3002
+ "aria-valid-attr",
3003
+ "aria-valid-attr-value",
3004
+ "autocomplete-valid",
3005
+ "blink",
3006
+ "button-name",
3007
+ "definition-list",
3008
+ "dlitem",
3009
+ "document-title",
3010
+ "empty-heading",
3011
+ "empty-table-header",
3012
+ "frame-title",
3013
+ "heading-order",
3014
+ "html-has-lang",
3015
+ "html-lang-valid",
3016
+ "html-xml-lang-mismatch",
3017
+ "image-alt",
3018
+ "image-redundant-alt",
3019
+ "input-button-name",
3020
+ "input-image-alt",
3021
+ "label",
3022
+ "label-title-only",
3023
+ "landmark-banner-is-top-level",
3024
+ "landmark-contentinfo-is-top-level",
3025
+ "landmark-main-is-top-level",
3026
+ "landmark-no-duplicate-banner",
3027
+ "landmark-no-duplicate-contentinfo",
3028
+ "landmark-no-duplicate-main",
3029
+ "landmark-unique",
3030
+ "link-name",
3031
+ "list",
3032
+ "listitem",
3033
+ "meta-refresh",
3034
+ "meta-viewport",
3035
+ "nested-interactive",
3036
+ "presentation-role-conflict",
3037
+ "region",
3038
+ "role-img-alt",
3039
+ "scope-attr-valid",
3040
+ "select-name",
3041
+ "summary-name",
3042
+ "svg-img-alt",
3043
+ "tabindex",
3044
+ "table-duplicate-name",
3045
+ "td-headers-attr",
3046
+ "valid-lang"
3047
+ ],
3048
+ browser: [
3049
+ "aria-braille-equivalent",
3050
+ "aria-hidden-focus",
3051
+ "aria-prohibited-attr",
3052
+ "audio-caption",
3053
+ "avoid-inline-spacing",
3054
+ "bypass",
3055
+ "color-contrast",
3056
+ "css-orientation-lock",
3057
+ "duplicate-id-aria",
3058
+ "focus-order-semantics",
3059
+ "form-field-multiple-labels",
3060
+ "frame-focusable-content",
3061
+ "frame-tested",
3062
+ "frame-title-unique",
3063
+ "hidden-content",
3064
+ "label-content-name-mismatch",
3065
+ "landmark-complementary-is-top-level",
3066
+ "landmark-one-main",
3067
+ "link-in-text-block",
3068
+ "marquee",
3069
+ "meta-viewport-large",
3070
+ "no-autoplay-audio",
3071
+ "object-alt",
3072
+ "p-as-heading",
3073
+ "page-has-heading-one",
3074
+ "scrollable-region-focusable",
3075
+ "server-side-image-map",
3076
+ "skip-link",
3077
+ "table-fake-caption",
3078
+ "target-size",
3079
+ "td-has-header",
3080
+ "th-has-data-cells",
3081
+ "video-caption"
3082
+ ]
3083
+ };
3084
+
3085
+ // packages/a11y/src/page.ts
3086
+ var require4 = createRequire3(import.meta.url);
3087
+ var axeSource;
3088
+ var source = () => axeSource ??= readFileSync2(require4.resolve("axe-core/axe.min.js"), "utf8");
3089
+ var PAGE_TAGS = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa", "best-practice"];
3090
+ async function runAxeOnPage(html, rules) {
3091
+ const dom = new JSDOM2(html, { url: "http://site.invalid/", runScripts: "outside-only", pretendToBeVisual: true });
3092
+ dom.virtualConsole.removeAllListeners();
3093
+ try {
3094
+ dom.window.eval(source());
3095
+ const axe2 = dom.window.axe;
3096
+ const ruleIds = axe2.getRules(PAGE_TAGS).map((r2) => r2.ruleId);
3097
+ const options = rules.length ? { runOnly: { type: "rule", values: rules } } : { runOnly: { type: "tag", values: PAGE_TAGS } };
3098
+ const r = await axe2.run(dom.window.document, { ...options, resultTypes: ["violations", "incomplete"] });
3099
+ return { violations: r.violations, incomplete: r.incomplete, axeVersion: axe2.version, ruleIds };
3100
+ } finally {
3101
+ dom.window.close();
3102
+ }
3103
+ }
3104
+ function severityOf(impact, tags) {
3105
+ const wcag = tags.some((t) => /^wcag\d/.test(t));
3106
+ return wcag && (impact === "critical" || impact === "serious") ? "error" : "warning";
3107
+ }
3108
+ async function runPageA11y(html) {
3109
+ const { violations } = await runAxeOnPage(html, page_rule_tiers_generated_default.jsdom);
3110
+ return violations.map((v) => ({
3111
+ ruleId: v.id,
3112
+ severity: severityOf(v.impact, v.tags),
3113
+ impact: v.impact ?? "minor",
3114
+ help: v.help,
3115
+ helpUrl: v.helpUrl,
3116
+ nodes: v.nodes.map((n) => ({ target: n.target.map(String).join(" "), html: n.html }))
3117
+ }));
3118
+ }
3119
+
2963
3120
  // packages/dev-server/src/diagnostics.ts
2964
3121
  async function checkComponent(componentId, variant, render, quarantineEntry) {
2965
3122
  if (quarantineEntry) {
@@ -3119,7 +3276,7 @@ async function persistSlices(root, slices) {
3119
3276
  }
3120
3277
 
3121
3278
  // 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";
3279
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
3123
3280
  import { dirname as dirname5, join as join13, sep as sep3 } from "node:path";
3124
3281
  function findPackageDir(name, root) {
3125
3282
  let dir = root;
@@ -3164,7 +3321,7 @@ function loadPatternPackages(references, root) {
3164
3321
  throw new Error(`pattern package "${name}" is not installed \u2014 run: npm install --save-dev ${name}`);
3165
3322
  }
3166
3323
  const dir = realpathSync2(via);
3167
- const pkg = JSON.parse(readFileSync2(join13(dir, "package.json"), "utf8"));
3324
+ const pkg = JSON.parse(readFileSync3(join13(dir, "package.json"), "utf8"));
3168
3325
  if (!pkg.patternDump) {
3169
3326
  if (!direct) return;
3170
3327
  throw new Error(
@@ -3441,8 +3598,8 @@ async function serveImageVariant(pathname, host, res, cfg) {
3441
3598
  const variant = parseImageUrl(pathname);
3442
3599
  if (!variant) return notFound();
3443
3600
  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();
3601
+ const source2 = await variantSource(variant.name, variant.hash, variant.width, encoding, cfg);
3602
+ if (!source2) return notFound();
3446
3603
  let sharp;
3447
3604
  try {
3448
3605
  sharp = await loadSharp();
@@ -3453,11 +3610,11 @@ async function serveImageVariant(pathname, host, res, cfg) {
3453
3610
  res.end(e.message);
3454
3611
  return;
3455
3612
  }
3456
- if (variant.width > (await imageSize(sharp, source)).width) return notFound();
3613
+ if (variant.width > (await imageSize(sharp, source2)).width) return notFound();
3457
3614
  const cache = cacheFor(cfg);
3458
3615
  let bytes = await cache.read(variant.hash);
3459
3616
  if (!bytes) {
3460
- bytes = await encodeWebp(sharp, source, variant.width, encoding);
3617
+ bytes = await encodeWebp(sharp, source2, variant.width, encoding);
3461
3618
  await cache.write(variant.hash, bytes);
3462
3619
  }
3463
3620
  res.writeHead(200, { "Content-Type": "image/webp", "Cache-Control": "public, max-age=31536000, immutable" });
@@ -3755,6 +3912,22 @@ function checkLinks(site) {
3755
3912
  return { problems, external, references };
3756
3913
  }
3757
3914
 
3915
+ // packages/dev-server/src/storefront/page-basics.ts
3916
+ function checkPageBasics(html) {
3917
+ let main = 0, h1 = 0;
3918
+ for (const tag of startTags(html)) {
3919
+ const name = tag.name.toLowerCase();
3920
+ if (name === "main" || attributesOf(tag).some((a) => a.name === "role" && a.value?.trim() === "main")) main++;
3921
+ if (name === "h1") h1++;
3922
+ }
3923
+ const problems = [];
3924
+ 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");
3925
+ 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`);
3926
+ if (h1 === 0) problems.push("the page has no <h1>: one heading names the page");
3927
+ else if (h1 > 1) problems.push(`the page has ${h1} <h1> headings: one names the page, the others are <h2> or lower`);
3928
+ return problems;
3929
+ }
3930
+
3758
3931
  // packages/dev-server/src/storefront/site-files.ts
3759
3932
  var escapeXml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
3760
3933
  function sitemapXml(entries) {
@@ -3798,18 +3971,18 @@ async function buildStaticSite(config, root, opts) {
3798
3971
  };
3799
3972
  const project = resolve7(root);
3800
3973
  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`);
3974
+ for (const source2 of [cfg.patternsDir, cfg.stylesDir, ...cfg.themeDirs, ...cfg.assetRoots]) {
3975
+ if (inside2(outDir, source2) || inside2(source2, outDir)) {
3976
+ throw new Error(`pd build: --out ${outDir} overlaps ${source2}, which Pattern Dump reads \u2014 choose another folder`);
3804
3977
  }
3805
3978
  }
3806
3979
  const storefront = cfg.storefront;
3807
3980
  if (storefront) {
3808
3981
  const sfOut = storefront.outDir;
3809
3982
  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`);
3983
+ for (const source2 of [cfg.patternsDir, cfg.stylesDir, storefront.pagesDir, ...cfg.themeDirs, ...cfg.assetRoots]) {
3984
+ if (inside2(sfOut, source2) || inside2(source2, sfOut)) {
3985
+ throw new Error(`pd build: storefront.outDir ${sfOut} overlaps ${source2}, which Pattern Dump reads \u2014 choose another folder`);
3813
3986
  }
3814
3987
  }
3815
3988
  if (inside2(sfOut, outDir) || inside2(outDir, sfOut)) {
@@ -3847,6 +4020,7 @@ ${sfCrawl.problems.map((p) => ` - ${p}`).join("\n")}`);
3847
4020
  if (sfCrawl) log(`\u2026 storefront checked: ${sfCrawl.urls.length} files`);
3848
4021
  const origin = `http://127.0.0.1:${server.port}`;
3849
4022
  let linkSummary;
4023
+ let a11ySummary;
3850
4024
  const externalNotes = [];
3851
4025
  if (storefront && sfGet && sfCrawl) {
3852
4026
  const { pages: list } = await (await sfGet(PD_ROUTES.pages)).json();
@@ -3884,6 +4058,34 @@ ${links.problems.map((p) => ` - ${p}`).join("\n")}`);
3884
4058
  for (const [url, on] of links.external) externalNotes.push(`an external link, not checked: ${url} (on ${on.join(", ")})`);
3885
4059
  linkSummary = { pages: pages.length, references: links.references, external: links.external.size };
3886
4060
  log(`\u2026 links checked: ${links.references} references on ${pages.length} pages`);
4061
+ const errors = [];
4062
+ const warned = /* @__PURE__ */ new Map();
4063
+ for (const page of pages) {
4064
+ const html = sfCrawl.documents.get(page);
4065
+ if (html === void 0) continue;
4066
+ for (const p of checkPageBasics(html)) errors.push(`${page}: ${p}`);
4067
+ for (const f of await runPageA11y(html)) {
4068
+ const where = f.nodes.map((n) => n.target).join(", ");
4069
+ if (f.severity === "error") {
4070
+ errors.push(`${page}: ${f.ruleId} (${f.impact}): ${f.help}, on ${where} (${f.helpUrl})`);
4071
+ continue;
4072
+ }
4073
+ const w = warned.get(f.ruleId) ?? { finding: f, pages: [], elements: 0 };
4074
+ w.pages.push(page);
4075
+ w.elements += f.nodes.length;
4076
+ warned.set(f.ruleId, w);
4077
+ }
4078
+ }
4079
+ if (errors.length) {
4080
+ await server.close();
4081
+ throw new Error(`pd build: the storefront has accessibility problems, so nothing was written:
4082
+ ${errors.map((e) => ` - ${e}`).join("\n")}`);
4083
+ }
4084
+ a11ySummary = {
4085
+ pages: pages.length,
4086
+ warnings: [...warned.values()].map((w) => `${w.finding.ruleId} (${w.finding.impact}): ${w.finding.help}, ${w.elements} element${w.elements === 1 ? "" : "s"} on ${w.pages.join(", ")} (${w.finding.helpUrl})`)
4087
+ };
4088
+ log(`\u2026 accessibility checked on ${pages.length} pages`);
3887
4089
  }
3888
4090
  await rm3(outDir, { recursive: true, force: true });
3889
4091
  await mkdir5(outDir, { recursive: true });
@@ -4000,6 +4202,7 @@ ${sf.failures.map((f) => ` - ${f}`).join("\n")}`);
4000
4202
  }
4001
4203
  report.notes.push(...storefrontNotes, ...externalNotes);
4002
4204
  if (linkSummary) report.links = linkSummary;
4205
+ if (a11ySummary) report.accessibility = a11ySummary;
4003
4206
  log(`\u2026 storefront: ${pages} pages, ${counts.files} files`);
4004
4207
  }
4005
4208
  const used = /* @__PURE__ */ new Set();
@@ -5524,8 +5727,8 @@ async function unsafeOutputs(id, at, deps) {
5524
5727
  for (const componentId of [...templates].sort()) {
5525
5728
  const file = deps.discoveryMap.get(componentId)?.hbsPath;
5526
5729
  if (!file) continue;
5527
- const source = await readFile18(file, "utf8").catch(() => "");
5528
- for (const o of unescapedOutputs(source)) {
5730
+ const source2 = await readFile18(file, "utf8").catch(() => "");
5731
+ for (const o of unescapedOutputs(source2)) {
5529
5732
  if (!o.helper && allowed.has(o.name)) continue;
5530
5733
  const what = o.helper ? `a helper's result unescaped ({{{${o.name}}}})` : `"${o.name}" unescaped ({{{${o.name}}}})`;
5531
5734
  problems.push({ at, message: `${componentId} outputs ${what} at ${deps.shown(file)}:${o.line}:${o.column}, which only a text/markdown property may be` });
@@ -5751,16 +5954,16 @@ function proseImages(md, tokens, prose) {
5751
5954
  continue;
5752
5955
  }
5753
5956
  if (!src.startsWith("/assets/")) continue;
5754
- const source = prose.lookup(src);
5755
- if (!source) {
5957
+ const source2 = prose.lookup(src);
5958
+ if (!source2) {
5756
5959
  problem(` is not an image in the assets`);
5757
5960
  continue;
5758
5961
  }
5759
- if (source.kind === "unavailable") {
5760
- problem(`: ${source.reason}`);
5962
+ if (source2.kind === "unavailable") {
5963
+ problem(`: ${source2.reason}`);
5761
5964
  continue;
5762
5965
  }
5763
- if (source.kind === "variants" && !prose.slot) {
5966
+ if (source2.kind === "variants" && !prose.slot) {
5764
5967
  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
5968
  continue;
5766
5969
  }
@@ -5768,7 +5971,7 @@ function proseImages(md, tokens, prose) {
5768
5971
  const atOnce = eager > 0;
5769
5972
  if (atOnce) eager--;
5770
5973
  const slot = prose.slot;
5771
- child.meta = { pdImage: imageTag(source, {
5974
+ child.meta = { pdImage: imageTag(source2, {
5772
5975
  widths: slot?.widths ?? [],
5773
5976
  sizes: slot ? atOnce ? slot.sizes : `auto, ${slot.sizes}` : "",
5774
5977
  alt,
@@ -6090,7 +6293,7 @@ async function buildComponentDetail(componentId, patternsDir, renderFn, tracker,
6090
6293
  } catch {
6091
6294
  }
6092
6295
  }
6093
- const source = {
6296
+ const source2 = {
6094
6297
  hbs,
6095
6298
  hbsHighlighted,
6096
6299
  ...jsonStr !== void 0 ? { json: jsonStr, jsonHighlighted } : {},
@@ -6131,7 +6334,7 @@ async function buildComponentDetail(componentId, patternsDir, renderFn, tracker,
6131
6334
  layout,
6132
6335
  ...deprecatedUse ? { deprecatedUse } : {},
6133
6336
  variants,
6134
- source,
6337
+ source: source2,
6135
6338
  doc,
6136
6339
  ...requireScripts ? { requireScripts } : {},
6137
6340
  ...requireAssets ? { requireAssets } : {},
@@ -6402,7 +6605,7 @@ async function stampCss(css, hashOf) {
6402
6605
 
6403
6606
  // packages/dev-server/src/assets.ts
6404
6607
  import { existsSync as existsSync5 } from "node:fs";
6405
- import { createRequire as createRequire3 } from "node:module";
6608
+ import { createRequire as createRequire4 } from "node:module";
6406
6609
  import { fileURLToPath as fileURLToPath5 } from "node:url";
6407
6610
  function resolveDevUiDist(moduleUrl, exists = existsSync5) {
6408
6611
  const packaged = fileURLToPath5(new URL("./dev-ui/", moduleUrl));
@@ -6410,7 +6613,7 @@ function resolveDevUiDist(moduleUrl, exists = existsSync5) {
6410
6613
  return fileURLToPath5(new URL("../../../apps/dev-ui/dist/", moduleUrl));
6411
6614
  }
6412
6615
  function resolveAxeCore(moduleUrl) {
6413
- return createRequire3(moduleUrl).resolve("axe-core/axe.min.js");
6616
+ return createRequire4(moduleUrl).resolve("axe-core/axe.min.js");
6414
6617
  }
6415
6618
 
6416
6619
  // packages/dev-server/src/storefront/page-image.ts
@@ -6669,7 +6872,7 @@ async function assemblePage(route, model, a) {
6669
6872
  const notFound = () => ({ status: 404, html: errorDocument(route, [`there is no page at ${route}`], a.live), problems: [`there is no page at ${route}`], notes: [] });
6670
6873
  const isNotFound = route === PD_ROUTES.notFound;
6671
6874
  let page = isNotFound ? model.notFound : model.files.get(route);
6672
- let source;
6875
+ let source2;
6673
6876
  if (isNotFound && !page) {
6674
6877
  const layoutId = `layouts/${DEFAULT_LAYOUT}`;
6675
6878
  if (!a.discoveryMap.has(layoutId)) {
@@ -6681,13 +6884,13 @@ async function assemblePage(route, model, a) {
6681
6884
  };
6682
6885
  }
6683
6886
  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) };
6887
+ source2 = builtInNotFound(Object.hasOwn(declared, "title"));
6888
+ page = { route, file: "", path: `${NOT_FOUND_FILE} (built in)`, frontmatter: readFrontmatterStrict(source2) };
6686
6889
  }
6687
6890
  if (!page) return notFound();
6688
- if (source === void 0) {
6891
+ if (source2 === void 0) {
6689
6892
  try {
6690
- source = await readFile23(page.file, "utf8");
6893
+ source2 = await readFile23(page.file, "utf8");
6691
6894
  } catch (e) {
6692
6895
  if (e.code === "ENOENT") return notFound();
6693
6896
  throw e;
@@ -6720,7 +6923,7 @@ async function assemblePage(route, model, a) {
6720
6923
  const prose = imageLookup ? {
6721
6924
  layoutId: checked.layoutId,
6722
6925
  lookup: imageLookup,
6723
- lineOffset: frontmatterBodyOffset(source),
6926
+ lineOffset: frontmatterBodyOffset(source2),
6724
6927
  problems: [],
6725
6928
  ...proseSlot.slot ? { slot: proseSlot.slot } : {},
6726
6929
  ...proseSlot.problem ? { slotProblem: proseSlot.problem } : {}
@@ -6729,7 +6932,7 @@ async function assemblePage(route, model, a) {
6729
6932
  const placedProblems = [];
6730
6933
  const headings = [];
6731
6934
  const body = await renderDoc(
6732
- frontmatterBody(source),
6935
+ frontmatterBody(source2),
6733
6936
  (id, variant) => a.render({ component: id, variant }).then((r) => r.html),
6734
6937
  a.discoveryMap,
6735
6938
  {
@@ -6741,7 +6944,7 @@ async function assemblePage(route, model, a) {
6741
6944
  ...prose ? { prose } : {},
6742
6945
  // Components placed with values (pd- tags): checked against their schemas, rendered into the page.
6743
6946
  placed: {
6744
- lineOffset: frontmatterBodyOffset(source),
6947
+ lineOffset: frontmatterBodyOffset(source2),
6745
6948
  problems: placedProblems,
6746
6949
  deps: {
6747
6950
  discoveryMap: a.discoveryMap,
@@ -6905,6 +7108,7 @@ ${html}`);
6905
7108
  routes.add(self);
6906
7109
  const links = checkLinks({ built: routes, documents: /* @__PURE__ */ new Map([[self, html]]), pages: [self], pagesOnly: true });
6907
7110
  notes.push(...links.problems.map((p) => `warning: ${page.path}${p.slice(self.length)}`));
7111
+ notes.push(...checkPageBasics(html).map((p) => `warning: ${page.path}: ${p}`));
6908
7112
  }
6909
7113
  return { status: isNotFound ? 404 : 200, html: document(head, `${html}
6910
7114
  ${fitterScript()}`, a.live, a.lang), problems: [], notes };
@@ -7597,7 +7801,7 @@ function rootMatchers(watched) {
7597
7801
 
7598
7802
  // packages/dev-server/src/watch.ts
7599
7803
  import { createHash as createHash6 } from "node:crypto";
7600
- import { existsSync as existsSync6, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs";
7804
+ import { existsSync as existsSync6, readFileSync as readFileSync4, statSync as statSync4 } from "node:fs";
7601
7805
  import { appendFile, readdir as readdir9 } from "node:fs/promises";
7602
7806
  import { basename as basename10, extname as extname4, join as join26, relative as relative13, resolve as resolve10, sep as sep9 } from "node:path";
7603
7807
  import chokidar from "chokidar";
@@ -7658,7 +7862,7 @@ function seenNow(path) {
7658
7862
  const s = statSync4(path);
7659
7863
  const stamp2 = `${s.ino}:${s.ctimeMs}:${s.mtimeMs}:${s.size}`;
7660
7864
  const forced = globalThis.__testOnly_watchStamp;
7661
- return { stamp: forced ? forced(path, stamp2) : stamp2, real: stamp2, hash: createHash6("sha1").update(readFileSync3(path)).digest("hex") };
7865
+ return { stamp: forced ? forced(path, stamp2) : stamp2, real: stamp2, hash: createHash6("sha1").update(readFileSync4(path)).digest("hex") };
7662
7866
  } catch {
7663
7867
  return null;
7664
7868
  }
@@ -9144,6 +9348,11 @@ async function runBuild(root, configPath, options) {
9144
9348
  if (report.links) {
9145
9349
  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
9350
  }
9351
+ if (report.accessibility) {
9352
+ const n = report.accessibility.warnings.length;
9353
+ console.log(`[pd build] accessibility: ${report.accessibility.pages} pages, no errors` + (n ? `; ${n} rule${n === 1 ? "" : "s"} with warnings:` : ""));
9354
+ for (const w of report.accessibility.warnings) console.log(`[pd build] warning: ${w}`);
9355
+ }
9147
9356
  for (const w of report.warnings) console.log(`[pd build] not built: ${w}`);
9148
9357
  for (const n of report.notes) console.log(`[pd build] note: ${n}`);
9149
9358
  }
@@ -9485,7 +9694,7 @@ import { basename as basename12, dirname as dirname13, join as join31, relative
9485
9694
  // packages/cli/package.json
9486
9695
  var package_default = {
9487
9696
  name: "@pattern-dump/cli",
9488
- version: "0.2.0-alpha.0",
9697
+ version: "0.3.0",
9489
9698
  private: true,
9490
9699
  description: "The pd command and the published pattern-dump package.",
9491
9700
  type: "module",
@@ -1,6 +1,6 @@
1
1
  import type { ShellBrandConfig, SwatchSlots, ThemeSwitch } from "../contracts/index.js";
2
2
  export type { ShellBrandConfig, SwatchSlots, ThemeSwitch };
3
- export { ConfigError, configProblems, sameHostWorkbenchPath, suggest, validateConfig } from "./validate.js";
3
+ export { ConfigError, configProblems, sameHostWorkbenchPath, suggest, validateConfig, withSourceDefaults } from "./validate.js";
4
4
  export interface AssetsConfig {
5
5
  /** Where assets live. Only 'central' (the default: one assets dir, served at /assets/) is supported
6
6
  * so far; 'component-relative' is reserved, and the config check rejects it as not supported yet. */
@@ -242,4 +242,10 @@ export type StorefrontNavEntry = {
242
242
  label: string;
243
243
  children: StorefrontNavEntry[];
244
244
  };
245
- export declare function defineConfig(config: PatternDumpConfig): PatternDumpConfig;
245
+ /**
246
+ * What a project writes: the config, where `sourceDir`, `patternsDir` and `styles` may be left out (a site whose
247
+ * patterns and styles all come from pattern packages has none of its own). `defineConfig` and `validateConfig` fill
248
+ * them in (`withSourceDefaults`), so everything that reads a config gets all three.
249
+ */
250
+ export type PatternDumpConfigInput = Omit<PatternDumpConfig, "sourceDir" | "patternsDir" | "styles"> & Partial<Pick<PatternDumpConfig, "sourceDir" | "patternsDir" | "styles">>;
251
+ export declare function defineConfig(config: PatternDumpConfigInput): PatternDumpConfig;
@@ -1,4 +1,4 @@
1
- import type { PatternDumpConfig } from "./index.js";
1
+ import type { PatternDumpConfig, PatternDumpConfigInput } from "./index.js";
2
2
  /** The closest of `known` to `key` when it is near enough to be a typo of it, else undefined. Used for
3
3
  * "did you mean" in config problems, and by other checks that name an unknown key. */
4
4
  export declare function suggest(key: string, known: string[]): string | undefined;
@@ -10,6 +10,12 @@ export declare class ConfigError extends Error {
10
10
  /** The problems with a loaded config, one line each; empty when it is valid. */
11
11
  export declare function configProblems(raw: unknown): string[];
12
12
  /** Check a loaded config and return it typed, or throw one error listing every problem. */
13
+ /**
14
+ * A config with its source folders filled in where it names none: `sourceDir` is "./src", and `patternsDir` and
15
+ * `styles.dir` are its `patterns` and `styles` folders. None of them has to exist: a site whose patterns and styles
16
+ * all come from pattern packages has none of its own. What the config names is kept as it is.
17
+ */
18
+ export declare function withSourceDefaults(config: PatternDumpConfigInput): PatternDumpConfig;
13
19
  export declare function validateConfig(raw: unknown, source: string): PatternDumpConfig;
14
20
  /**
15
21
  * The path of a workbench published on the storefront's own host, or undefined when it has a host of its own.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pattern-dump",
3
- "version": "0.2.0-alpha.0",
3
+ "version": "0.3.0",
4
4
  "description": "A workbench for HTML and CSS design systems: live component previews, docs, checks and static publishing.",
5
5
  "keywords": [
6
6
  "design-system",