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/CHANGELOG.md CHANGED
@@ -1,5 +1,88 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Added
6
+
7
+ - `pd build --browser` also checks a storefront's pages in Chromium, at a desktop and a phone width: colour contrast
8
+ and the other accessibility rules that need a rendered page (a critical or serious finding stops the build), and
9
+ each image's variant against the box it is laid out in (a warning when its slot's `sizes` doesn't match). It uses
10
+ the project's own Playwright: `npm install --save-dev playwright`, then `npx playwright install chromium`.
11
+ - Collections: a folder of storefront pages is a collection. A layout that declares `page.children` lists the
12
+ folder's pages on its `index.md` (route, title, description, date, image, tags), newest first with `sort: date`;
13
+ a page without a description is listed with the first paragraph of its body. `partials/base` can list folders
14
+ under `site.collections` to show their pages on every page, such as a home page's latest articles. Layouts can
15
+ show a page's own `page.date` and `page.tags`.
16
+ - A storefront can be published under a path, such as a GitHub project page: `storefront.url` takes the full
17
+ address (`https://example.github.io/project/`). Every page, stylesheet, script and image is then served under the
18
+ path, in `pd dev` too, with links still written from the site's root; canonical URLs, Open Graph, JSON-LD and the
19
+ sitemap carry it. Every page names its path in `<meta name="pd-base-path">` for a skin's own scripts, and
20
+ `pd build --browser` stops on a request outside the path. Under a path there is no `robots.txt` (the build says what
21
+ the host's own needs) and previews carry a robots `noindex` tag, as a workbench's do when it is built with `--base`.
22
+ - A project that is a pattern package itself (a style guide with its own workbench) describes its folders once: its
23
+ config takes each folder it doesn't name from its `package.json`'s `patternDump` field (patterns, data, helpers,
24
+ assets, styles with `entry` and `documentBase`, themes, scripts, `globalScripts`, `exclude`). A folder named in
25
+ both with different values is a config error naming both files.
26
+ - `storefront.head`: a site's own additions to every page's head (an analytics script, a verification tag), a
27
+ Handlebars file rendered with `site` after the skin's `partials/head` and checked like it. For a site whose skin
28
+ is a shared package.
29
+ - A `HELPER_SHADOW` warning on a component whose template has a bare `{{name}}` that calls a registered helper
30
+ while its data has a field of that name, at any depth: `{{id}}` inside `{{#each palettes}}` showed the `id`
31
+ helper's output instead of each palette's `id`, silently. Read the field as `{{this.id}}`, or rename it.
32
+ - `pd init --storefront` writes an `articles/` collection (a listing page and one dated, tagged article) with a
33
+ `listing` layout, shows a page's date and tags in its page layout, and its config shows a `storefront.url` for a
34
+ subfolder.
35
+ - `date`, `tags` and `draft` in a page's frontmatter. `date` (`YYYY-MM-DD`) is the page's date for listings and its
36
+ JSON-LD `datePublished`, which falls back to the page's first commit. `draft: true` shows a page in `pd dev` and
37
+ leaves it out of `pd build`, which names the drafts it left out.
38
+
39
+ ### Fixed
40
+
41
+ - A storefront page loads the behaviour scripts its components bring (`requireScript` of its layout's components and
42
+ of the components it places, and `globalScripts`), as their workbench previews do. Before, only the markup and CSS
43
+ reached the page.
44
+ - A code block on a storefront page, or in a component's `<pre>`, keeps its lines as written when its partial is
45
+ included on an indented line (as a skin's `{{> @partial-block }}` usually is). Handlebars indented every line after
46
+ the first by the include's indentation.
47
+ - A schema field named like a registered helper (`id`, `eq`, …) is no longer an error only the workbench's Diagnostics
48
+ tab shows: `pd dev` and `pd build` say it in the terminal, a storefront page that places the component fails (as
49
+ one whose layout has such a field already did), and `pd audit` re-audits a component when its schema changes
50
+ instead of reusing a verdict cached before.
51
+ - An image in a site page's markdown from a pattern package's assets, when that package's folder is the site's only
52
+ asset folder, gets its variants. Before, the page failed with "is not an image in the assets" for a URL that was
53
+ served.
54
+ - `pd dev` no longer keeps base-layer rules that a component's stylesheet stopped writing (through a mixin) until it is
55
+ restarted.
56
+
57
+ ### Changed
58
+
59
+ - **Breaking:** `date`, `tags`, `draft` and `sort` are now frontmatter keys of the build, like `layout` and
60
+ `image`. A layout whose schema declares one of them fails every page that uses it; rename the field in the
61
+ layout's schema (and show the page's date or tags through `page.date` and `page.tags`).
62
+ - A storefront page's JSON-LD carries `datePublished`.
63
+
64
+ ## 0.3.0
65
+
66
+ From this release Pattern Dump is published as plain 0.x versions under npm's `latest` tag: install it with
67
+ `npx pattern-dump init`, without `@alpha`. Being at 0.x, breaking changes are possible between minor versions, and
68
+ this changelog names them.
69
+
70
+ ### Added
71
+
72
+ - `sourceDir`, `patternsDir` and `styles` are optional. A site whose components, skin and styles all come from
73
+ pattern packages names none of them; they default to `./src`, `./src/patterns` and `./src/styles`, and the folders
74
+ don't have to exist.
75
+
76
+ ### Changed
77
+
78
+ - `pd build` now checks every page of a storefront with axe before it writes anything, and **stops on an
79
+ accessibility error**: a WCAG rule rated critical or serious (an image without alt text, a button with no name, a
80
+ page without a language), or a page without exactly one `<main>` and one `<h1>`. Each error is printed with its
81
+ page, the rule, the elements it was found on and a link to the rule's explanation; moderate and minor findings,
82
+ and axe's best practices, are listed as warnings and don't stop the build. To see them before a build, run
83
+ `pd build` locally: nothing is written while there are errors. In `pd dev`, a page without one `<main>` and one
84
+ `<h1>` is a warning in the terminal.
85
+
3
86
  ## 0.2.0-alpha.0
4
87
 
5
88
  The storefront: a website written as markdown pages, dressed by a skin made of ordinary components, served by
package/README.md CHANGED
@@ -1,22 +1,27 @@
1
1
  # Pattern Dump
2
2
 
3
- A workbench for HTML and CSS design systems, and a successor to PatternLab. Write components as
3
+ A workbench for HTML and CSS design systems, and a successor to Pattern Lab. Write components as
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
+ **Demo:** a style guide and the website built from it, at https://lab.n8d.studio/pattern-dump-demo/
8
+
9
+ > **0.4.0.** Pattern Dump is at 0.x, so breaking changes are possible between minor versions (0.3 to 0.4);
10
+ > the [changelog](https://github.com/n8design/pattern-dump/blob/main/CHANGELOG.md) names them.
11
+
12
+ **Coming from Pattern Lab?** Point `patternsDir` at your patterns folder: Handlebars templates, `.json` data and
13
+ `~` variants work as they are, and no build step of your own is needed.
9
14
 
10
15
  ## Requirements
11
16
 
12
- Node.js 20.10 or later, and npm.
17
+ Node.js 22.22 or later, and npm.
13
18
 
14
19
  ## Quick start
15
20
 
16
21
  Create a new project:
17
22
 
18
23
  ```bash
19
- npx pattern-dump@alpha init my-design-system
24
+ npx pattern-dump init my-design-system
20
25
  cd my-design-system
21
26
  npm run dev # → http://localhost:4000
22
27
  ```
@@ -24,7 +29,7 @@ npm run dev # → http://localhost:4000
24
29
  Or add Pattern Dump to an existing project:
25
30
 
26
31
  ```bash
27
- npm install --save-dev pattern-dump@alpha
32
+ npm install --save-dev pattern-dump
28
33
  npx pd init
29
34
  ```
30
35
 
@@ -35,7 +40,7 @@ overwrites an existing file.
35
40
  ### Installing `pd` globally
36
41
 
37
42
  ```bash
38
- npm install --global pattern-dump@alpha
43
+ npm install --global pattern-dump
39
44
  pd init my-design-system
40
45
  ```
41
46
 
@@ -54,12 +54,13 @@ var SHAPE = {
54
54
  lang: "lang",
55
55
  index: "boolean",
56
56
  image: "asset-path",
57
- images: IMAGES
57
+ images: IMAGES,
58
+ head: "hbs-file"
58
59
  },
59
60
  required: ["pagesDir", "outDir"]
60
61
  }
61
62
  };
62
- var REQUIRED = ["sourceDir", "patternsDir", "outDir", "styles"];
63
+ var REQUIRED = ["outDir"];
63
64
  var WORKBENCH_PATH = /^\/(?:(?!\.{1,2}(?:\/|$))[a-z0-9._~-]+\/?)+$/i;
64
65
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
65
66
  var isStrings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
@@ -90,25 +91,25 @@ function suggest(key, known) {
90
91
  const sameStart = key.slice(0, 3).toLowerCase() === best.slice(0, 3).toLowerCase();
91
92
  return bestD <= 2 || bestD <= 3 && sameStart ? best : void 0;
92
93
  }
93
- function checkObject(value, shape, required, at, problems) {
94
+ function checkObject(value, shape, required, at2, problems) {
94
95
  const known = Object.keys(shape);
95
96
  const unknown = Object.keys(value).filter((k) => !(k in shape));
96
97
  for (const key of required) {
97
98
  if (value[key] !== void 0) continue;
98
99
  const near = unknown.find((u) => suggest(u, [key]) === key);
99
- problems.push(near ? `missing required "${at}${key}" (found "${at}${near}" \u2014 did you mean "${at}${key}"?)` : `missing required "${at}${key}"`);
100
+ problems.push(near ? `missing required "${at2}${key}" (found "${at2}${near}" \u2014 did you mean "${at2}${key}"?)` : `missing required "${at2}${key}"`);
100
101
  }
101
102
  for (const key of unknown) {
102
103
  const near = suggest(key, known);
103
104
  if (near && required.includes(near) && value[near] === void 0) continue;
104
- problems.push(`unknown key "${at}${key}"${near ? ` \u2014 did you mean "${at}${near}"?` : ""}`);
105
+ problems.push(`unknown key "${at2}${key}"${near ? ` \u2014 did you mean "${at2}${near}"?` : ""}`);
105
106
  }
106
107
  for (const key of known) {
107
- if (value[key] !== void 0) checkValue(value[key], shape[key], `${at}${key}`, problems);
108
+ if (value[key] !== void 0) checkValue(value[key], shape[key], `${at2}${key}`, problems);
108
109
  }
109
110
  }
110
- function checkValue(value, rule, at, problems) {
111
- const fail = (expected) => problems.push(`"${at}" must be ${expected}`);
111
+ function checkValue(value, rule, at2, problems) {
112
+ const fail = (expected) => problems.push(`"${at2}" must be ${expected}`);
112
113
  if (rule === "string") {
113
114
  if (typeof value !== "string") fail("a string");
114
115
  return;
@@ -154,12 +155,12 @@ function checkValue(value, rule, at, problems) {
154
155
  if (typeof value === "string") {
155
156
  try {
156
157
  const url = new URL(value);
157
- ok = /^https?:$/.test(url.protocol) && url.pathname === "/" && url.search === "" && url.hash === "" && !/[?#]/.test(value);
158
+ ok = /^https?:$/.test(url.protocol) && url.search === "" && url.hash === "" && !/[?#]/.test(value) && !/\/\/./.test(url.pathname);
158
159
  } catch {
159
160
  ok = false;
160
161
  }
161
162
  }
162
- if (!ok) fail(`the site's absolute http(s) address, with no path, query or fragment, such as "https://example.com/"`);
163
+ if (!ok) fail(`the site's absolute http(s) address, with no query or fragment, such as "https://example.com/", or "https://example.github.io/project/" for a site published under a path`);
163
164
  return;
164
165
  }
165
166
  if (rule === "lang") {
@@ -168,6 +169,12 @@ function checkValue(value, rule, at, problems) {
168
169
  }
169
170
  return;
170
171
  }
172
+ if (rule === "hbs-file") {
173
+ if (typeof value !== "string" || !/\.hbs$/.test(value) || /^(?:\/|[a-zA-Z]:)/.test(value)) {
174
+ fail(`a Handlebars file relative to the project's root, such as "./head.hbs"`);
175
+ }
176
+ return;
177
+ }
171
178
  if (rule === "asset-path") {
172
179
  if (typeof value !== "string" || !/^\/assets\/[^?#]+[^/?#]$/.test(value)) {
173
180
  fail(`a path to a file in the assets, such as "/assets/images/share.png"`);
@@ -194,7 +201,7 @@ function checkValue(value, rule, at, problems) {
194
201
  }
195
202
  if ("values" in rule) {
196
203
  if (typeof value === "string" && rule.unsupported?.includes(value)) {
197
- problems.push(`"${at}": "${value}" is not supported yet \u2014 use ${rule.values.map((v) => `"${v}"`).join(" or ")}, or leave it out`);
204
+ problems.push(`"${at2}": "${value}" is not supported yet \u2014 use ${rule.values.map((v) => `"${v}"`).join(" or ")}, or leave it out`);
198
205
  } else if (typeof value !== "string" || !rule.values.includes(value)) {
199
206
  fail(rule.values.map((v) => `"${v}"`).join(" or "));
200
207
  }
@@ -204,7 +211,7 @@ function checkValue(value, rule, at, problems) {
204
211
  fail("an object");
205
212
  return;
206
213
  }
207
- checkObject(value, rule.keys, rule.required ?? [], `${at}.`, problems);
214
+ checkObject(value, rule.keys, rule.required ?? [], `${at2}.`, problems);
208
215
  }
209
216
  var ConfigError = class extends Error {
210
217
  name = "ConfigError";
@@ -218,8 +225,8 @@ function checkRelations(raw, problems) {
218
225
  ["storefront.outDir", sf.outDir]
219
226
  ];
220
227
  const root = resolve(sep, "pd-root");
221
- const inside = (child, parent) => {
222
- const rel = relative(parent, child);
228
+ const inside = (child2, parent) => {
229
+ const rel = relative(parent, child2);
223
230
  return rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep) && !/^[a-zA-Z]:/.test(rel);
224
231
  };
225
232
  for (let i = 0; i < folders.length; i++) {
@@ -242,17 +249,28 @@ function configProblems(raw) {
242
249
  return problems;
243
250
  }
244
251
  var withOneTrailingSlash = (url) => `${url.replace(/\/+$/, "")}/`;
252
+ function withSourceDefaults(config) {
253
+ if (config.sourceDir !== void 0 && config.patternsDir !== void 0 && config.styles !== void 0) return config;
254
+ const sourceDir = config.sourceDir ?? "./src";
255
+ const base = sourceDir.replace(/\/+$/, "");
256
+ return {
257
+ ...config,
258
+ sourceDir,
259
+ patternsDir: config.patternsDir ?? `${base}/patterns`,
260
+ styles: config.styles ?? { strategy: "parallel", dir: `${base}/styles` }
261
+ };
262
+ }
245
263
  function validateConfig(raw, source) {
246
264
  const problems = configProblems(raw);
247
265
  if (problems.length > 0) {
248
266
  throw new ConfigError(`${source}: invalid config
249
267
  ${problems.map((p) => ` - ${p}`).join("\n")}`);
250
268
  }
251
- const config = raw;
269
+ const config = withSourceDefaults(raw);
252
270
  const sf = config.storefront;
253
271
  if (!sf) return config;
254
272
  const workbenchUrl = sf.workbenchUrl !== void 0 ? withOneTrailingSlash(sf.workbenchUrl) : void 0;
255
- const url = sf.url !== void 0 ? `${new URL(sf.url).origin}/` : void 0;
273
+ const url = sf.url !== void 0 ? siteUrlForm(sf.url) : void 0;
256
274
  if (workbenchUrl === sf.workbenchUrl && url === sf.url) return config;
257
275
  return { ...config, storefront: {
258
276
  ...sf,
@@ -260,24 +278,165 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
260
278
  ...url !== void 0 ? { url } : {}
261
279
  } };
262
280
  }
281
+ function siteUrlForm(value) {
282
+ const u = new URL(value);
283
+ return `${u.origin}${withOneTrailingSlash(u.pathname)}`;
284
+ }
285
+ function storefrontBasePath(storefront) {
286
+ return storefront?.url === void 0 ? "/" : new URL(storefront.url).pathname;
287
+ }
263
288
  function sameHostWorkbenchPath(storefront) {
264
289
  const w = storefront?.workbenchUrl;
265
290
  if (w === void 0) return void 0;
266
291
  if (w.startsWith("/")) return w;
267
292
  if (storefront?.url === void 0) return void 0;
268
293
  const u = new URL(w);
269
- return `${u.origin}/` === storefront.url ? u.pathname : void 0;
294
+ return u.origin === new URL(storefront.url).origin ? u.pathname : void 0;
295
+ }
296
+
297
+ // packages/config/src/own-manifest.ts
298
+ var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
299
+ var child = (o, key) => {
300
+ if (!isObject2(o[key])) o[key] = {};
301
+ return o[key];
302
+ };
303
+ var at = (o, ...keys) => keys.reduce((v, k) => isObject2(v) ? v[k] : void 0, o);
304
+ var top = (from, to, paths = true) => ({
305
+ from,
306
+ get: (m) => m[from],
307
+ to,
308
+ read: (c) => c[to],
309
+ write: (c, v) => {
310
+ c[to] = v;
311
+ },
312
+ paths
313
+ });
314
+ var FIELDS = [
315
+ top("patterns", "patternsDir"),
316
+ top("data", "dataDir"),
317
+ top("helpers", "helpers"),
318
+ top("scripts", "scriptsDir"),
319
+ top("globalScripts", "globalScripts", false),
320
+ top("exclude", "exclude", false),
321
+ {
322
+ from: "assets",
323
+ get: (m) => m["assets"],
324
+ to: "assets.dir",
325
+ read: (c) => at(c, "assets", "dir"),
326
+ write: (c, v) => {
327
+ child(c, "assets")["dir"] = v;
328
+ },
329
+ paths: true
330
+ },
331
+ {
332
+ from: "styles.dir",
333
+ get: (m) => at(m, "styles", "dir"),
334
+ to: "styles.dir",
335
+ read: (c) => at(c, "styles", "dir"),
336
+ write: (c, v) => {
337
+ child(c, "styles")["dir"] = v;
338
+ },
339
+ paths: true
340
+ },
341
+ {
342
+ from: "styles.entry",
343
+ get: (m) => at(m, "styles", "entry"),
344
+ to: "styles.entry",
345
+ read: (c) => at(c, "styles", "entry"),
346
+ write: (c, v) => {
347
+ child(c, "styles")["entry"] = v;
348
+ },
349
+ paths: true
350
+ },
351
+ {
352
+ from: "styles.documentBase",
353
+ get: (m) => at(m, "styles", "documentBase"),
354
+ to: "documentBase",
355
+ read: (c) => c["documentBase"],
356
+ write: (c, v) => {
357
+ c["documentBase"] = v;
358
+ },
359
+ paths: true
360
+ },
361
+ {
362
+ from: "themes.dir",
363
+ get: (m) => at(m, "themes", "dir"),
364
+ to: "themes.dir",
365
+ read: (c) => at(c, "themes", "dir"),
366
+ write: (c, v) => {
367
+ child(c, "themes")["dir"] = v;
368
+ },
369
+ paths: true
370
+ },
371
+ {
372
+ from: "themes.default",
373
+ get: (m) => at(m, "themes", "default"),
374
+ to: "themes.default",
375
+ read: (c) => at(c, "themes", "default"),
376
+ write: (c, v) => {
377
+ child(c, "themes")["default"] = v;
378
+ },
379
+ paths: false
380
+ },
381
+ {
382
+ from: "themes.switch",
383
+ get: (m) => at(m, "themes", "switch"),
384
+ to: "themes.switch",
385
+ read: (c) => at(c, "themes", "switch"),
386
+ write: (c, v) => {
387
+ child(c, "themes")["switch"] = v;
388
+ },
389
+ paths: false
390
+ }
391
+ ];
392
+ function samePath(p) {
393
+ const parts = [];
394
+ for (const seg of p.replace(/\\/g, "/").split("/")) {
395
+ if (seg === "" || seg === ".") continue;
396
+ if (seg === ".." && parts.length && parts[parts.length - 1] !== "..") parts.pop();
397
+ else parts.push(seg);
398
+ }
399
+ return parts.join("/") || ".";
400
+ }
401
+ function comparable(v, paths) {
402
+ const list = Array.isArray(v) ? v : [v];
403
+ return JSON.stringify(paths ? list.map((x) => typeof x === "string" ? samePath(x) : x) : Array.isArray(v) ? v : [v]);
404
+ }
405
+ var shown = (v) => JSON.stringify(v);
406
+ function withOwnManifest(raw, manifest, where) {
407
+ if (!isObject2(raw) || !isObject2(manifest)) return raw;
408
+ const config = structuredClone(raw);
409
+ const conflicts = [];
410
+ for (const f of FIELDS) {
411
+ const value = f.get(manifest);
412
+ if (value === void 0) continue;
413
+ const own = f.read(config);
414
+ if (own === void 0) {
415
+ f.write(config, structuredClone(value));
416
+ continue;
417
+ }
418
+ if (comparable(own, f.paths) !== comparable(value, f.paths)) {
419
+ conflicts.push(`"${f.to}" is ${shown(own)} in ${where.config}, but "patternDump.${f.from}" is ${shown(value)} in ${where.manifest}: name it in one of them, or give both the same value`);
420
+ }
421
+ }
422
+ if (conflicts.length) {
423
+ throw new ConfigError(`${where.config}: the config and the project's own package manifest disagree
424
+ ${conflicts.map((c) => ` - ${c}`).join("\n")}`);
425
+ }
426
+ return config;
270
427
  }
271
428
 
272
429
  // packages/config/src/index.ts
273
430
  function defineConfig(config) {
274
- return config;
431
+ return withSourceDefaults(config);
275
432
  }
276
433
 
277
434
  export {
278
435
  suggest,
279
436
  ConfigError,
280
437
  validateConfig,
438
+ storefrontBasePath,
281
439
  sameHostWorkbenchPath,
440
+ withOwnManifest,
282
441
  defineConfig
283
442
  };
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-DIDJPCLS.js";
4
4
  export {
5
5
  defineConfig
6
6
  };