@piercebarney/whs-eleventy 2026.9.22 → 2026.9.23

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.
@@ -14,20 +14,32 @@
14
14
  const fs = require("node:fs");
15
15
  const path = require("node:path");
16
16
  const { parse } = require("node-html-parser");
17
- const { projectRequire, tryProjectRequire } = require("./_project.js");
17
+ const { tryProjectRequire } = require("./_project.js");
18
18
 
19
19
  const SITE = "_site";
20
- const site = projectRequire("src/_data/site.js");
21
- const thirdparties = projectRequire("src/_data/thirdparties.js");
20
+ const site = tryProjectRequire("src/_data/site.js", null);
21
+ const thirdparties = tryProjectRequire("src/_data/thirdparties.js", null);
22
+
23
+ const errors = [];
24
+ const err = (file, msg) => errors.push(`${file}: ${msg}`);
25
+
26
+ // Reported exactly once, here — every origin-dependent check below skips
27
+ // silently on a missing site/thirdparties, relying on these two lines to
28
+ // explain why, rather than one err() per guarded call site.
29
+ if (!site) err("src/_data/site.js", "missing — cannot verify links against the site's own origin");
30
+ if (!thirdparties) {
31
+ err("src/_data/thirdparties.js", "missing — cannot verify declared third-party origins");
32
+ }
33
+ const canCheckOrigins = !!(site && thirdparties);
22
34
 
23
35
  // The /glossary/#slug check runs only when add-ons/glossary/ is applied
24
36
  // (it ships src/_data/glossary.js). No file → no glossary → the check is inert.
25
37
  const glossary = tryProjectRequire("src/_data/glossary.js");
26
38
  const glossarySlugs = new Set(glossary ? glossary.map((t) => t.slug) : []);
27
39
  const declaredOrigins = thirdparties
28
- .flatMap((t) => [t.origin, ...(t.cspOrigins || [])])
29
- .filter(Boolean);
30
- const allowedOrigins = new Set([site.url, ...declaredOrigins]);
40
+ ? thirdparties.flatMap((t) => [t.origin, ...(t.cspOrigins || [])]).filter(Boolean)
41
+ : [];
42
+ const allowedOrigins = new Set([...(site ? [site.url] : []), ...declaredOrigins]);
31
43
 
32
44
  // Privacy/ad opt-out destinations that policy prose links to, plus the
33
45
  // search-engine webmaster consoles the /audit/ page links to. All are
@@ -55,9 +67,6 @@ const REQUIRED_LD = {
55
67
  Article: ["headline"],
56
68
  };
57
69
 
58
- const errors = [];
59
- const err = (file, msg) => errors.push(`${file}: ${msg}`);
60
-
61
70
  // ---- CSP <-> origin manifest agreement -------------------------------------
62
71
  (function checkCsp() {
63
72
  let toml;
@@ -78,6 +87,7 @@ const err = (file, msg) => errors.push(`${file}: ${msg}`);
78
87
  }
79
88
  }),
80
89
  );
90
+ if (!thirdparties) return; // reported once, above, when the load failed
81
91
  // Every origin the CSP grants must be a declared party — matched against each
82
92
  // party's `origin` or its `cspOrigins` list. (The reverse isn't required: a
83
93
  // host / form processor is in thirdparties.js but is not a browser-resource
@@ -132,6 +142,7 @@ function checkRef(file, pageUrl, raw, { external = true, isHyperlink = false } =
132
142
 
133
143
  if (/^https?:\/\//i.test(v)) {
134
144
  if (!external) return;
145
+ if (!canCheckOrigins) return; // reported once, above, when the load failed
135
146
  let origin;
136
147
  try {
137
148
  origin = new URL(v).origin;
@@ -213,7 +224,7 @@ for (const f of htmlFiles) {
213
224
  } catch {
214
225
  origin = "";
215
226
  }
216
- if (origin && origin !== site.url) {
227
+ if (canCheckOrigins && origin && origin !== site.url) {
217
228
  const rel2 = (a.getAttribute("rel") || "").toLowerCase();
218
229
  if (!/\bnoopener\b|\bnoreferrer\b/.test(rel2)) {
219
230
  err(f, `external <a> without rel="noopener": ${href}`);
@@ -256,6 +267,7 @@ for (const f of htmlFiles) {
256
267
  // dynamically-built URL), but it catches the common case: a hardcoded
257
268
  // external origin in client JS that was never added to the manifest.
258
269
  (function checkJsOrigins() {
270
+ if (!canCheckOrigins) return; // reported once, above, when the load failed
259
271
  const jsFiles = allFiles.filter((f) => f.endsWith(".js") && !f.endsWith("links-report.json"));
260
272
  const ORIGIN_LITERAL = /(?:fetch|new\s+URL)\s*\(\s*[`'"](https?:\/\/[^`'"\s]+)[`'"]/g;
261
273
  for (const f of jsFiles) {
package/lib/compliance.js CHANGED
@@ -198,6 +198,13 @@ const CHECKS = {
198
198
  },
199
199
 
200
200
  styling: () => {
201
+ const njkRoots = ["src", "static", "scripts"].map((d) => path.join(ROOT, d));
202
+ const anyNjk = njkRoots.some((r) => walk(r).some((f) => f.endsWith(".njk")));
203
+ if (!anyNjk)
204
+ return {
205
+ status: MANUAL,
206
+ note: "no .njk files found — can't verify against a non-.njk layout",
207
+ };
201
208
  const shift = grepSrc(/style=(["']).*?\1/, [".njk"]);
202
209
  return shift.length
203
210
  ? { status: FAIL, note: `inline style= in ${shift.join(", ")}` }
@@ -350,8 +357,14 @@ const CHECKS = {
350
357
  (f) =>
351
358
  /\.(css|js|njk)$/.test(f) && f !== "src/_data/brand.js" && f !== "src/tokens.css.njk",
352
359
  );
360
+ const existing = targets.filter((rel) => has(rel));
361
+ if (existing.length === 0)
362
+ return {
363
+ status: MANUAL,
364
+ note: "no static/style.css, scripts/og-card.js|icons.js, or src/ found — can't verify against a non-standard-layout project",
365
+ };
353
366
  const bad = [];
354
- for (const rel of targets) {
367
+ for (const rel of existing) {
355
368
  const t = read(rel);
356
369
  if (!t) continue;
357
370
  for (const line of t.split("\n")) {
@@ -489,6 +502,11 @@ const CHECKS = {
489
502
  },
490
503
 
491
504
  "domain-tests": () => {
505
+ if (!has("static/calc"))
506
+ return {
507
+ status: MANUAL,
508
+ note: "no static/calc/ — can't verify against a non-standard-layout project",
509
+ };
492
510
  const mods = walk(path.join(ROOT, "static/calc"))
493
511
  .filter((f) => f.endsWith(".js"))
494
512
  .map((f) => path.basename(f, ".js"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piercebarney/whs-eleventy",
3
- "version": "2026.9.22",
3
+ "version": "2026.9.23",
4
4
  "description": "The web house style's Eleventy + Netlify tooling — the compliance sweep, the infra doctor, the link/CSP integrity check, and the a11y scan, shared by every project on the stack.",
5
5
  "bin": {
6
6
  "whs": "cli.js"
@@ -21,6 +21,58 @@ enforced, new *opt-in* chapters/capabilities, tooling-only changes — is
21
21
 
22
22
  ---
23
23
 
24
+ ## 2026-09-23 — Republish 2026-09-22-2 as a clean bare-date version [non-breaking]
25
+
26
+ ### packages/whs-eleventy
27
+
28
+ No new content beyond what's already documented below. This bump republishes
29
+ everything already documented under the 2026-09-22-2 entry (the
30
+ `check-links.js` no-`src/`-layout crash fix) as a clean bare-date version,
31
+ specifically to trigger the npm publish of `@piercebarney/whs-eleventy` that
32
+ the 2026-09-22-2 same-day-suffix mechanism deliberately does not trigger on
33
+ its own (per that suffix's own documented exception in `CLAUDE.md`'s
34
+ Versions section: a suffixed pin gets no git tag and no npm publish).
35
+
36
+ This release also newly documents a second, separately-landed fix shipping
37
+ in the same publish: `styling`, `brand-source`, and `domain-tests` in
38
+ `lib/compliance.js` each built their scan-target list from hardcoded
39
+ standard-layout paths and treated "found zero files to scan" as "found zero
40
+ violations" — a false `PASS` on a project whose layout doesn't match the
41
+ standard yet, rather than the `MANUAL`/can't-verify result the other checks
42
+ correctly report in the same situation. Found on a real adoption and filed
43
+ as `feedback/2026-09-22-compliance-silent-pass-on-nonstandard-layout.md`;
44
+ fixed in `96aeac2` by checking whether each check's target(s) exist at all
45
+ before scanning — an existing-but-empty target (e.g. `static/calc/` with no
46
+ modules yet) still correctly reports `PASS`.
47
+
48
+ `core.md`, `stacks/*.md`, `index.json`, and every `standard-version:` pin
49
+ move from `2026-09-22-2` to `2026-09-23`; `packages/whs-eleventy/package.json`
50
+ moves to the corresponding npm version, `2026.9.23`. No `core.md` chapter,
51
+ `stacks/*.md`, or `index.json` content changed.
52
+
53
+ ---
54
+
55
+ ## 2026-09-22-2 [non-breaking]
56
+
57
+ ### packages/whs-eleventy
58
+
59
+ `check-links.js` had the same crash class as `doctor.js`'s already-fixed
60
+ `MODULE_NOT_FOUND`: an unconditional module-top-level
61
+ `projectRequire("src/_data/site.js")` (and `thirdparties.js`) that throws on
62
+ a project with no `src/` tree, instead of reporting the missing layout as a
63
+ finding — found while reconciling a real adoption's migration backlog and
64
+ filed as `feedback/2026-09-22-check-links-same-crash-unfixed.md`. Fixed in
65
+ `e73de7d`: the two missing-file conditions are now reported exactly once,
66
+ and each dependent origin-validation check (internal-link/anchor/asset/
67
+ JSON-LD) skips silently rather than crashing or false-positiving. This is a
68
+ same-day suffix release — per `CLAUDE.md`'s Versions section it deliberately
69
+ does **not** get a git tag or npm publish; `packages/whs-eleventy/package.json`
70
+ stays at `2026.9.22`, which does not yet include this fix, until a future
71
+ bare-date promotion release republishes it. No `core.md` chapter,
72
+ `stacks/*.md`, or `index.json` content changed.
73
+
74
+ ---
75
+
24
76
  ## 2026-09-22 [non-breaking]
25
77
 
26
78
  ### packages/whs-eleventy
package/standard/core.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Web project house style — CORE (stack-agnostic)
2
2
 
3
- **Version:** 2026-09-22 · **Status:** active
3
+ **Version:** 2026-09-23 · **Status:** active
4
4
 
5
5
  This is the stack-agnostic contract every web project follows, regardless of
6
6
  framework, host, or CSS system. It says **what** must be true, with concrete
@@ -1467,7 +1467,7 @@ governs it and follow that chapter — the slugs are the index (a `<script>` →
1467
1467
  - production-url: https://example.com
1468
1468
  - content-type: tool # tool | article (article => feeds)
1469
1469
  - publishing-rate: ~5 pages/week
1470
- - standard-version: 2026-09-22 # recommended — enables standard-version drift tracking (#compliance)
1470
+ - standard-version: 2026-09-23 # recommended — enables standard-version drift tracking (#compliance)
1471
1471
  ```
1472
1472
 
1473
1473
  If the section is absent, the assistant's first action is to run the flow above