ultra11y 2.22.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/LICENSE +21 -0
- package/README.md +223 -0
- package/docker/Dockerfile +7 -0
- package/docker/package.json +9 -0
- package/docker/runner.mjs +23 -0
- package/package.json +73 -0
- package/scripts/bench.mjs +151 -0
- package/scripts/build-act-corpus.mjs +143 -0
- package/scripts/build-adjudication.mjs +503 -0
- package/scripts/build-pack-rgaa.mjs +397 -0
- package/scripts/build-standards.mjs +548 -0
- package/scripts/copy-bundle.mjs +39 -0
- package/scripts/import-pack.mjs +107 -0
- package/scripts/sync-engine.mjs +84 -0
- package/scripts/sync-version.mjs +43 -0
- package/scripts/ultra11y.mjs +52739 -0
- package/scripts/vendor/act-testcases.json +12297 -0
- package/scripts/vendor/rgaa/SOURCE.json +8 -0
- package/scripts/vendor/rgaa/criteres.json +4144 -0
- package/scripts/vendor/rgaa/glossaire.json +480 -0
- package/scripts/vendor/wcag-2.2-fr.json +82 -0
- package/scripts/vendor/wcag-2.2-sc-universe.json +950 -0
- package/scripts/vendor/wcag-2.2-sc.json +574 -0
- package/scripts/verify-rgaa-source.mjs +126 -0
- package/scripts/verify-skill-bundle.mjs +91 -0
- package/skills/review-a11y/SKILL.md +123 -0
- package/skills/review-a11y/scripts/ultra11y.mjs +52739 -0
- package/skills/ultra11y/SKILL.md +292 -0
- package/skills/ultra11y/references/act.md +163 -0
- package/skills/ultra11y/references/adjudication.md +599 -0
- package/skills/ultra11y/references/audit.md +100 -0
- package/skills/ultra11y/references/authoring.md +37 -0
- package/skills/ultra11y/references/automation.md +113 -0
- package/skills/ultra11y/references/correction.md +41 -0
- package/skills/ultra11y/references/criteria.md +129 -0
- package/skills/ultra11y/references/cross-file.md +82 -0
- package/skills/ultra11y/references/dynamic.md +152 -0
- package/skills/ultra11y/references/false-positives.md +81 -0
- package/skills/ultra11y/references/fix.md +46 -0
- package/skills/ultra11y/references/focus-and-logic.md +64 -0
- package/skills/ultra11y/references/forbidden-patterns.md +61 -0
- package/skills/ultra11y/references/forms-and-errors.md +147 -0
- package/skills/ultra11y/references/guidance.md +55 -0
- package/skills/ultra11y/references/judgment.md +90 -0
- package/skills/ultra11y/references/media-and-motion.md +116 -0
- package/skills/ultra11y/references/methodology.md +72 -0
- package/skills/ultra11y/references/naming.md +117 -0
- package/skills/ultra11y/references/orchestration.md +44 -0
- package/skills/ultra11y/references/packs.md +193 -0
- package/skills/ultra11y/references/prd.md +104 -0
- package/skills/ultra11y/references/rendered.md +110 -0
- package/skills/ultra11y/references/scale.md +49 -0
- package/skills/ultra11y/references/standards.md +97 -0
- package/skills/ultra11y/references/structure.md +138 -0
- package/skills/ultra11y/references/verify.md +88 -0
- package/skills/ultra11y/references/widgets.md +106 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +52739 -0
- package/src/act.ts +441 -0
- package/src/adjudicate.ts +630 -0
- package/src/aria.ts +230 -0
- package/src/audit.ts +396 -0
- package/src/auditor.ts +399 -0
- package/src/autofill.ts +102 -0
- package/src/axe-map.ts +294 -0
- package/src/baseline.ts +75 -0
- package/src/capture.ts +182 -0
- package/src/check.ts +243 -0
- package/src/cli.ts +1927 -0
- package/src/color.ts +102 -0
- package/src/config.ts +182 -0
- package/src/crawl.ts +75 -0
- package/src/criteria.ts +183 -0
- package/src/data/adjudication.json +770 -0
- package/src/data/guidance/rgaa.json +2395 -0
- package/src/data/standards/rgaa.glossary.json +478 -0
- package/src/data/standards/rgaa.json +3454 -0
- package/src/data/wcag-universe.json +530 -0
- package/src/data/wcag.json +1178 -0
- package/src/discover.ts +203 -0
- package/src/fix/codemods.ts +76 -0
- package/src/fix/diff.ts +64 -0
- package/src/fix/edits.ts +126 -0
- package/src/fix.ts +215 -0
- package/src/gh.ts +214 -0
- package/src/glob.ts +206 -0
- package/src/graph/build.ts +114 -0
- package/src/graph/engine-resolve.ts +0 -0
- package/src/graph/graph.ts +126 -0
- package/src/graph/imports.ts +359 -0
- package/src/graph/resolve.ts +73 -0
- package/src/graph/tsconfig.ts +113 -0
- package/src/grounding.ts +119 -0
- package/src/guidance/index.ts +45 -0
- package/src/guidance/types.ts +36 -0
- package/src/init.ts +113 -0
- package/src/mcp/handlers.ts +388 -0
- package/src/mcp/http.ts +260 -0
- package/src/mcp/prompts.ts +183 -0
- package/src/mcp/protocol.ts +187 -0
- package/src/mcp/resources.ts +136 -0
- package/src/mcp/server.ts +228 -0
- package/src/mcp/stdio.ts +119 -0
- package/src/mcp/tools.ts +347 -0
- package/src/messages.ts +1011 -0
- package/src/name.ts +174 -0
- package/src/orchestrate-templates.ts +231 -0
- package/src/orchestrate.ts +186 -0
- package/src/output.ts +108 -0
- package/src/pack.ts +187 -0
- package/src/parse/html.ts +258 -0
- package/src/parse/jsx-ast.ts +67 -0
- package/src/parse/jsx-bridge.ts +247 -0
- package/src/parse/jsx.ts +19 -0
- package/src/parse/source.ts +75 -0
- package/src/prd.ts +406 -0
- package/src/project-lock.ts +42 -0
- package/src/render.ts +307 -0
- package/src/report.ts +399 -0
- package/src/rules/colors.ts +125 -0
- package/src/rules/cross-registry.ts +264 -0
- package/src/rules/cross-rule.ts +83 -0
- package/src/rules/forms.ts +590 -0
- package/src/rules/frames.ts +33 -0
- package/src/rules/headings.ts +227 -0
- package/src/rules/images.ts +200 -0
- package/src/rules/links.ts +193 -0
- package/src/rules/mandatory.ts +173 -0
- package/src/rules/multimedia.ts +77 -0
- package/src/rules/navigation.ts +190 -0
- package/src/rules/presentation.ts +75 -0
- package/src/rules/registry.ts +57 -0
- package/src/rules/rule.ts +122 -0
- package/src/rules/scripts-aria.ts +636 -0
- package/src/rules/tables.ts +264 -0
- package/src/rules/timing.ts +55 -0
- package/src/sample.ts +176 -0
- package/src/scan-local.ts +866 -0
- package/src/scan.ts +538 -0
- package/src/standards/derive.ts +195 -0
- package/src/standards/index.ts +31 -0
- package/src/standards/pack-rules.ts +111 -0
- package/src/standards/pack.ts +81 -0
- package/src/standards/registry.ts +100 -0
- package/src/standards/types.ts +202 -0
- package/src/standards/validate.ts +544 -0
- package/src/standards/vocabulary.ts +75 -0
- package/src/types.ts +355 -0
- package/src/util.ts +32 -0
- package/src/vendor/codeindex-engine.d.mts +1009 -0
- package/src/vendor/codeindex-engine.mjs +13701 -0
- package/src/vendor/engine.meta.json +9 -0
- package/src/verify.ts +275 -0
- package/src/wcag.ts +128 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// DEV-ONLY (not in `bin`). Builds the canonical WCAG 2.2 Level AA dataset that the
|
|
3
|
+
// engine ships at src/data/wcag.json. The success-criterion list (id/title/level/
|
|
4
|
+
// version-added/principle/guideline) is DERIVED PROGRAMMATICALLY from the official
|
|
5
|
+
// W3C source (https://github.com/w3c/wcag) — never hand-typed — by positional
|
|
6
|
+
// numbering of guidelines/index.html, then decorated with the engine-specific
|
|
7
|
+
// fields (rule coverage, automatability, and manual-test seeds rolled up from any
|
|
8
|
+
// shipped standards pack). WCAG 2.2 © W3C, reused under the W3C Document License;
|
|
9
|
+
// only SC ids/titles/levels are reproduced. See NOTICE.
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// node scripts/build-standards.mjs # emit src/data/wcag.json + wcag-universe.json from the vendored snapshots
|
|
13
|
+
// node scripts/build-standards.mjs --offline # same (alias; the snapshots are always local)
|
|
14
|
+
// node scripts/build-standards.mjs --refresh <dir> # re-derive the vendored AA snapshot from a w3c/wcag checkout
|
|
15
|
+
// node scripts/build-standards.mjs --refresh-universe # re-fetch (network) the vendored FULL SC universe (all levels + removed 4.1.1)
|
|
16
|
+
// node scripts/build-standards.mjs --refresh-fr # re-fetch (network) the vendored French SC/guideline/principle titles
|
|
17
|
+
import { writeFileSync, readFileSync, readdirSync, existsSync, mkdirSync } from "node:fs";
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
|
+
const DATA = join(root, "src", "data");
|
|
24
|
+
const VENDOR = join(root, "scripts", "vendor", "wcag-2.2-sc.json");
|
|
25
|
+
const VENDOR_UNIVERSE = join(root, "scripts", "vendor", "wcag-2.2-sc-universe.json");
|
|
26
|
+
const VENDOR_FR = join(root, "scripts", "vendor", "wcag-2.2-fr.json");
|
|
27
|
+
const PACKS_DIR = join(DATA, "standards");
|
|
28
|
+
const BIOME = join(root, "node_modules", ".bin", "biome");
|
|
29
|
+
|
|
30
|
+
const VER_DIR = { 20: "2.0", 21: "2.1", 22: "2.2" };
|
|
31
|
+
|
|
32
|
+
// The committed src/data/*.json datasets are biome-formatted (short arrays collapse onto
|
|
33
|
+
// one line — see biome.json's default `--expand=auto`), not raw `JSON.stringify` output
|
|
34
|
+
// (see scripts/build-pack-rgaa.mjs, which solves the same problem for the RGAA pack).
|
|
35
|
+
// Route every write through biome so a bare rebuild is byte-stable vs the committed
|
|
36
|
+
// files; `relPath` (project-relative, e.g. "src/data/wcag.json") only picks the JSON
|
|
37
|
+
// formatter, no file is touched.
|
|
38
|
+
function biomeFormat(text, relPath) {
|
|
39
|
+
return execFileSync(BIOME, ["format", `--stdin-file-path=${relPath}`], { input: text, encoding: "utf8" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Refresh mode: parse a w3c/wcag checkout into the vendored SC snapshot.
|
|
44
|
+
// SC numbers are positional within guidelines/index.html (principle → guideline →
|
|
45
|
+
// success-criterion include order) — exactly how the W3C Eleventy build numbers
|
|
46
|
+
// them. The obsolete 4.1.1 Parsing is still physically listed (so it must be
|
|
47
|
+
// counted to keep 4.1.2 correct) but has no conformance level and is dropped below.
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
function deriveSnapshot(srcDir) {
|
|
50
|
+
const idx = readFileSync(join(srcDir, "guidelines", "index.html"), "utf8");
|
|
51
|
+
const tok =
|
|
52
|
+
/<section class="(principle|guideline)" id="([^"]+)">|<h([23])>\s*([\s\S]*?)\s*<\/h\3>|data-include="sc\/(\d+)\/([^"]+)\.html"/g;
|
|
53
|
+
let p = 0,
|
|
54
|
+
g = 0,
|
|
55
|
+
s = 0,
|
|
56
|
+
curP = null,
|
|
57
|
+
curG = null,
|
|
58
|
+
awaitP = false,
|
|
59
|
+
awaitG = false;
|
|
60
|
+
const principles = [];
|
|
61
|
+
const guidelines = [];
|
|
62
|
+
const criteria = [];
|
|
63
|
+
let m;
|
|
64
|
+
while ((m = tok.exec(idx))) {
|
|
65
|
+
if (m[1] === "principle") {
|
|
66
|
+
p++;
|
|
67
|
+
g = 0;
|
|
68
|
+
curP = { number: p, id: m[2], title: "" };
|
|
69
|
+
principles.push(curP);
|
|
70
|
+
awaitP = true;
|
|
71
|
+
} else if (m[1] === "guideline") {
|
|
72
|
+
g++;
|
|
73
|
+
s = 0;
|
|
74
|
+
curG = { number: `${p}.${g}`, id: m[2], title: "" };
|
|
75
|
+
guidelines.push(curG);
|
|
76
|
+
awaitG = true;
|
|
77
|
+
} else if (m[3] === "2" && awaitP) {
|
|
78
|
+
curP.title = m[4].replace(/\s+/g, " ").trim();
|
|
79
|
+
awaitP = false;
|
|
80
|
+
} else if (m[3] === "3" && awaitG) {
|
|
81
|
+
curG.title = m[4].replace(/\s+/g, " ").trim();
|
|
82
|
+
awaitG = false;
|
|
83
|
+
} else if (m[5]) {
|
|
84
|
+
s++;
|
|
85
|
+
const version = Number(m[5]);
|
|
86
|
+
const slug = m[6];
|
|
87
|
+
const frag = readFileSync(join(srcDir, "guidelines", "sc", m[5], `${slug}.html`), "utf8");
|
|
88
|
+
const title = (frag.match(/<h4>\s*([\s\S]*?)\s*<\/h4>/) || [])[1]?.replace(/\s+/g, " ").trim() ?? "";
|
|
89
|
+
const level = (frag.match(/<p class="conformance-level">\s*([A]{1,3})\s*<\/p>/) || [])[1] ?? "";
|
|
90
|
+
criteria.push({ sc: `${p}.${g}.${s}`, slug, title, level, addedIn: VER_DIR[version], principle: p, guideline: curG.number });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const aa = criteria.filter((c) => (c.level === "A" || c.level === "AA") && c.sc !== "4.1.1");
|
|
94
|
+
const usedGuidelines = new Set(aa.map((c) => c.guideline));
|
|
95
|
+
const snapshot = {
|
|
96
|
+
wcagVersion: "2.2",
|
|
97
|
+
source: "https://www.w3.org/TR/WCAG22/",
|
|
98
|
+
criteriaSource: "https://github.com/w3c/wcag",
|
|
99
|
+
principles: principles.map((x) => ({ number: x.number, title: x.title })),
|
|
100
|
+
guidelines: guidelines.filter((x) => usedGuidelines.has(x.number)).map((x) => ({ number: x.number, title: x.title })),
|
|
101
|
+
criteria: aa,
|
|
102
|
+
};
|
|
103
|
+
mkdirSync(dirname(VENDOR), { recursive: true });
|
|
104
|
+
writeFileSync(VENDOR, JSON.stringify(snapshot, null, 2) + "\n");
|
|
105
|
+
console.log(`build-standards --refresh: ${aa.length} A/AA criteria derived from ${srcDir} → ${VENDOR}`);
|
|
106
|
+
return snapshot;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Universe mode: the COMPLETE WCAG 2.x success-criterion list — every level (A/AA/AAA)
|
|
111
|
+
// PLUS the obsolete/removed 4.1.1 Parsing — vendored so a pack's out-of-core SC mapping
|
|
112
|
+
// (e.g. an EN 301 549 criterion citing an AAA success criterion, or RGAA citing the
|
|
113
|
+
// removed 4.1.1) can be checked against the REAL W3C universe instead of a single
|
|
114
|
+
// hardcoded exception. deriveSnapshot() above already reads this same W3C numbering from
|
|
115
|
+
// a *local* w3c/wcag checkout, but only persists the shipped AA slice; this fetches
|
|
116
|
+
// (network, dev-only, mirrors scripts/build-pack-rgaa.mjs's own `--fetch` source
|
|
117
|
+
// vendoring) the same guidelines/index.html + every sc/<version>/<slug>.html fragment via
|
|
118
|
+
// raw.githubusercontent.com and vendors the UNFILTERED result — nothing invented, only
|
|
119
|
+
// the minimum extra data (id/title/level) needed to classify what falls outside the core.
|
|
120
|
+
// Re-run only when the W3C source changes (a WCAG erratum or new version):
|
|
121
|
+
// node scripts/build-standards.mjs --refresh-universe
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
const RAW_BASE = "https://raw.githubusercontent.com/w3c/wcag/main";
|
|
124
|
+
|
|
125
|
+
async function fetchText(path) {
|
|
126
|
+
const r = await fetch(`${RAW_BASE}/${path}`);
|
|
127
|
+
if (!r.ok) throw new Error(`build-standards --refresh-universe: GET ${path} → HTTP ${r.status}`);
|
|
128
|
+
return r.text();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function deriveUniverse() {
|
|
132
|
+
const idx = await fetchText("guidelines/index.html");
|
|
133
|
+
const tok =
|
|
134
|
+
/<section class="(principle|guideline)" id="([^"]+)">|<h([23])>\s*([\s\S]*?)\s*<\/h\3>|data-include="sc\/(\d+)\/([^"]+)\.html"/g;
|
|
135
|
+
let p = 0,
|
|
136
|
+
g = 0,
|
|
137
|
+
s = 0,
|
|
138
|
+
curP = null,
|
|
139
|
+
curG = null,
|
|
140
|
+
awaitP = false,
|
|
141
|
+
awaitG = false;
|
|
142
|
+
const principles = [];
|
|
143
|
+
const guidelines = [];
|
|
144
|
+
const stubs = []; // { sc, slug, version, principle, guideline } — title/level fetched next
|
|
145
|
+
let m;
|
|
146
|
+
while ((m = tok.exec(idx))) {
|
|
147
|
+
if (m[1] === "principle") {
|
|
148
|
+
p++;
|
|
149
|
+
g = 0;
|
|
150
|
+
curP = { number: p, title: "" };
|
|
151
|
+
principles.push(curP);
|
|
152
|
+
awaitP = true;
|
|
153
|
+
} else if (m[1] === "guideline") {
|
|
154
|
+
g++;
|
|
155
|
+
s = 0;
|
|
156
|
+
curG = { number: `${p}.${g}`, title: "" };
|
|
157
|
+
guidelines.push(curG);
|
|
158
|
+
awaitG = true;
|
|
159
|
+
} else if (m[3] === "2" && awaitP) {
|
|
160
|
+
curP.title = m[4].replace(/\s+/g, " ").trim();
|
|
161
|
+
awaitP = false;
|
|
162
|
+
} else if (m[3] === "3" && awaitG) {
|
|
163
|
+
curG.title = m[4].replace(/\s+/g, " ").trim();
|
|
164
|
+
awaitG = false;
|
|
165
|
+
} else if (m[5]) {
|
|
166
|
+
s++;
|
|
167
|
+
stubs.push({ sc: `${p}.${g}.${s}`, slug: m[6], version: Number(m[5]), principle: p, guideline: curG.number });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const criteria = [];
|
|
172
|
+
for (const stub of stubs) {
|
|
173
|
+
const frag = await fetchText(`guidelines/sc/${stub.version}/${stub.slug}.html`);
|
|
174
|
+
const title = (frag.match(/<h4>\s*([\s\S]*?)\s*<\/h4>/) || [])[1]?.replace(/\s+/g, " ").trim() ?? "";
|
|
175
|
+
// No <p class="conformance-level"> at all ⇒ the SC has no current level — the
|
|
176
|
+
// (so far unique) case is the obsolete/removed 4.1.1 Parsing.
|
|
177
|
+
const level = (frag.match(/<p class="conformance-level">\s*([A]{1,3})\s*<\/p>/) || [])[1] ?? "";
|
|
178
|
+
const status = !level ? "removed" : level === "AAA" ? "out-of-core" : "core-AA";
|
|
179
|
+
criteria.push({
|
|
180
|
+
sc: stub.sc,
|
|
181
|
+
slug: stub.slug,
|
|
182
|
+
title,
|
|
183
|
+
level,
|
|
184
|
+
addedIn: VER_DIR[stub.version],
|
|
185
|
+
principle: stub.principle,
|
|
186
|
+
guideline: stub.guideline,
|
|
187
|
+
status,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const universe = {
|
|
192
|
+
wcagVersion: "2.2",
|
|
193
|
+
source: "https://www.w3.org/TR/WCAG22/",
|
|
194
|
+
criteriaSource: "https://github.com/w3c/wcag",
|
|
195
|
+
provenance:
|
|
196
|
+
`Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from ` +
|
|
197
|
+
`raw.githubusercontent.com/w3c/wcag@main on ${new Date().toISOString().slice(0, 10)} via ` +
|
|
198
|
+
"`node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in " +
|
|
199
|
+
"src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
200
|
+
principles,
|
|
201
|
+
guidelines,
|
|
202
|
+
criteria,
|
|
203
|
+
};
|
|
204
|
+
mkdirSync(dirname(VENDOR_UNIVERSE), { recursive: true });
|
|
205
|
+
writeFileSync(VENDOR_UNIVERSE, JSON.stringify(universe, null, 2) + "\n");
|
|
206
|
+
console.log(`build-standards --refresh-universe: ${criteria.length} SCs (all levels) derived → ${VENDOR_UNIVERSE}`);
|
|
207
|
+
return universe;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// French-titles mode: fetch the W3C AUTHORIZED French translation of WCAG 2.2
|
|
212
|
+
// (https://www.w3.org/Translations/WCAG22-fr/ — a single-page document, unlike the
|
|
213
|
+
// split w3c/wcag English source) and vendor ONLY the principle/guideline/SC TITLES for
|
|
214
|
+
// the shipped AA core (nothing invented, no paraphrase — every title is lifted verbatim
|
|
215
|
+
// from this page). The fr page numbers every heading exactly like the English source
|
|
216
|
+
// (<bdi class="secno">1.4.3 </bdi>) — h2 = principle ("1. "), h3 = guideline ("Règle
|
|
217
|
+
// 1.1 "), h4 = success criterion ("Critère de succès 1.1.1 ") — so titles are read
|
|
218
|
+
// directly off the dotted id, no positional counting needed. Re-run only when the W3C
|
|
219
|
+
// translation changes: node scripts/build-standards.mjs --refresh-fr
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
const FR_SOURCE = "https://www.w3.org/Translations/WCAG22-fr/";
|
|
222
|
+
|
|
223
|
+
// Minimal HTML → plaintext (mirrors scripts/build-pack-rgaa.mjs's `deHtml`): the fr
|
|
224
|
+
// titles are plain text, so this only needs to strip stray tags/entities, not full markup.
|
|
225
|
+
function deHtmlFr(s) {
|
|
226
|
+
return s
|
|
227
|
+
.replace(/<[^>]+>/g, " ")
|
|
228
|
+
.replace(/ /g, " ")
|
|
229
|
+
.replace(/&/g, "&")
|
|
230
|
+
.replace(/</g, "<")
|
|
231
|
+
.replace(/>/g, ">")
|
|
232
|
+
.replace(/'|’|’/g, "’")
|
|
233
|
+
.replace(/"/g, '"')
|
|
234
|
+
.replace(/\s+/g, " ")
|
|
235
|
+
.trim();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function deriveFr() {
|
|
239
|
+
if (!existsSync(VENDOR)) {
|
|
240
|
+
console.error(`build-standards --refresh-fr: missing vendored English snapshot ${VENDOR}. Run: node scripts/build-standards.mjs --refresh <w3c/wcag checkout> first.`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
const snap = JSON.parse(readFileSync(VENDOR, "utf8"));
|
|
244
|
+
const r = await fetch(FR_SOURCE);
|
|
245
|
+
if (!r.ok) throw new Error(`build-standards --refresh-fr: GET ${FR_SOURCE} → HTTP ${r.status}`);
|
|
246
|
+
const html = await r.text();
|
|
247
|
+
|
|
248
|
+
// <h2|h3|h4><bdi class="secno">[prefix ]<id>[.] </bdi>Title</hN> — id is the bare
|
|
249
|
+
// dotted number ("1" for a principle, "1.1" for a guideline, "1.1.1" for an SC); the
|
|
250
|
+
// "Règle "/"Critère de succès " word prefixes and level are NOT captured, only the id.
|
|
251
|
+
const tok = /<h([234])[^>]*>\s*<bdi class="secno">([^<]*)<\/bdi>\s*([\s\S]*?)<\/h\1>/g;
|
|
252
|
+
const rawPrinciples = {};
|
|
253
|
+
const rawGuidelines = {};
|
|
254
|
+
const rawCriteria = {};
|
|
255
|
+
let m;
|
|
256
|
+
while ((m = tok.exec(html))) {
|
|
257
|
+
const idMatch = m[2].match(/(\d+(?:\.\d+)*)/);
|
|
258
|
+
if (!idMatch) continue;
|
|
259
|
+
const id = idMatch[1];
|
|
260
|
+
const title = deHtmlFr(m[3]);
|
|
261
|
+
if (m[1] === "2") {
|
|
262
|
+
if (/^[1-4]$/.test(id)) rawPrinciples[id] = title; // "5./6./7." are Conformance/Glossary/Input-purposes, not WCAG principles
|
|
263
|
+
} else if (m[1] === "3") rawGuidelines[id] = title;
|
|
264
|
+
else if (m[1] === "4") rawCriteria[id] = title;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Only vendor what the shipped AA core actually needs — cross-referenced against the
|
|
268
|
+
// English snapshot's own principle/guideline/SC id lists, never a blind full-page dump.
|
|
269
|
+
const principles = {};
|
|
270
|
+
const guidelines = {};
|
|
271
|
+
const criteria = {};
|
|
272
|
+
const missing = [];
|
|
273
|
+
for (const p of snap.principles) {
|
|
274
|
+
const t = rawPrinciples[String(p.number)];
|
|
275
|
+
if (t) principles[String(p.number)] = t;
|
|
276
|
+
else missing.push(`principle ${p.number}`);
|
|
277
|
+
}
|
|
278
|
+
for (const g of snap.guidelines) {
|
|
279
|
+
const t = rawGuidelines[g.number];
|
|
280
|
+
if (t) guidelines[g.number] = t;
|
|
281
|
+
else missing.push(`guideline ${g.number}`);
|
|
282
|
+
}
|
|
283
|
+
for (const c of snap.criteria) {
|
|
284
|
+
const t = rawCriteria[c.sc];
|
|
285
|
+
if (t) criteria[c.sc] = t;
|
|
286
|
+
else missing.push(`SC ${c.sc}`);
|
|
287
|
+
}
|
|
288
|
+
if (missing.length) {
|
|
289
|
+
console.error(`build-standards --refresh-fr: no French title found on ${FR_SOURCE} for: ${missing.join(", ")}. Nothing invented — refusing to vendor a partial/paraphrased dataset.`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const out = {
|
|
294
|
+
source: FR_SOURCE,
|
|
295
|
+
fetchedAt: new Date().toISOString().slice(0, 10),
|
|
296
|
+
principles,
|
|
297
|
+
guidelines,
|
|
298
|
+
criteria,
|
|
299
|
+
};
|
|
300
|
+
mkdirSync(dirname(VENDOR_FR), { recursive: true });
|
|
301
|
+
writeFileSync(VENDOR_FR, JSON.stringify(out, null, 2) + "\n");
|
|
302
|
+
console.log(
|
|
303
|
+
`build-standards --refresh-fr: ${Object.keys(criteria).length} SC + ${Object.keys(guidelines).length} guideline + ${Object.keys(principles).length} principle French titles → ${VENDOR_FR}`,
|
|
304
|
+
);
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// Engine-specific decoration (the only hand-maintained part: which SCs the static
|
|
310
|
+
// engine can fully decide, which need a rendered DOM, and the per-SC rule coverage).
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
// An SC is `static` (absence of a finding can be reported Conforming) ONLY when the
|
|
314
|
+
// engine both detects every failure AND can judge applicability. WCAG SCs are coarser
|
|
315
|
+
// than the rules, so this set is deliberately tiny — every other mapped SC raises
|
|
316
|
+
// DEFINITE non-conformities on a finding and stays `manual` (residual risk) otherwise,
|
|
317
|
+
// never silently Conforming.
|
|
318
|
+
const STATIC = new Set(["1.4.2", "2.4.2", "3.1.1"]);
|
|
319
|
+
|
|
320
|
+
// SCs that fundamentally need a rendered DOM (computed colour/layout/focus/zoom).
|
|
321
|
+
// They may still carry rules that raise definite NCs; no finding ⇒ `manual`.
|
|
322
|
+
const NEEDS_RENDERING = new Set([
|
|
323
|
+
"1.3.4", "1.4.1", "1.4.3", "1.4.4", "1.4.5", "1.4.10", "1.4.11", "1.4.12", "1.4.13",
|
|
324
|
+
"2.1.2", "2.3.1", "2.4.7", "2.4.11", "2.5.8",
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
// SC → engine rule ids. Inverse of the per-finding rule→SC map. Rules whose findings
|
|
328
|
+
// branch across SCs (clickable-noninteractive, lang-invalid, autoplay-media,
|
|
329
|
+
// icon-only-control-unnamed) appear under BOTH their SCs so registry's bidirectional
|
|
330
|
+
// rule↔dataset cross-check holds.
|
|
331
|
+
const RULE_SC_COVERAGE = {
|
|
332
|
+
"1.1.1": ["img-alt-missing", "canvas-fallback-missing", "decorative-alt-misuse", "input-image-alt-missing", "object-embed-no-name", "chart-no-accessible-name"],
|
|
333
|
+
"1.2.2": ["media-no-track"],
|
|
334
|
+
"1.3.1": [
|
|
335
|
+
"fieldset-legend-missing", "data-table-no-headers", "table-caption-missing",
|
|
336
|
+
"layout-table-data-markup", "heading-order-skip", "h1-missing", "h1-multiple", "list-structure", "dl-structure",
|
|
337
|
+
"empty-heading", "label-for-dangling", "missing-main-landmark", "multiple-main-landmark",
|
|
338
|
+
"sortable-header-no-aria-sort", "nav-landmark-missing", "nav-landmark-unnamed", "aria-required-parent",
|
|
339
|
+
"headers-attr-dangling", "th-no-data-cells",
|
|
340
|
+
"radio-checkbox-group-ungrouped", "table-empty-data-cell", "css-generated-content-informative",
|
|
341
|
+
],
|
|
342
|
+
"1.3.5": ["field-purpose-incomplete", "autocomplete-token-invalid"],
|
|
343
|
+
"1.4.2": ["autoplay-media"],
|
|
344
|
+
"1.4.3": ["contrast-literal"],
|
|
345
|
+
"1.4.4": ["meta-viewport-zoom-block"],
|
|
346
|
+
"2.1.1": ["clickable-noninteractive"],
|
|
347
|
+
"2.2.1": ["meta-refresh-redirect"],
|
|
348
|
+
"2.2.2": ["autoplay-media", "blink-marquee"],
|
|
349
|
+
"2.4.1": ["skip-link-target-missing"],
|
|
350
|
+
"2.4.2": ["title-missing-empty"],
|
|
351
|
+
"2.4.3": ["positive-tabindex"],
|
|
352
|
+
"2.4.4": ["link-empty-name", "icon-only-control-unnamed"],
|
|
353
|
+
"2.5.3": ["label-in-name-mismatch"],
|
|
354
|
+
"3.2.2": ["on-input-context-change"],
|
|
355
|
+
"3.3.8": ["credential-entry-blocked"],
|
|
356
|
+
"3.1.1": ["html-lang-missing", "lang-invalid"],
|
|
357
|
+
"3.1.2": ["inline-lang-change-missing", "lang-invalid"],
|
|
358
|
+
"3.3.1": ["aria-invalid-no-description", "error-not-associated"],
|
|
359
|
+
"3.3.2": ["radio-checkbox-group-ungrouped", "date-fields-ungrouped"],
|
|
360
|
+
"4.1.2": [
|
|
361
|
+
"iframe-title-missing", "invalid-aria-role", "aria-ref-missing-id", "redundant-aria",
|
|
362
|
+
"clickable-noninteractive", "aria-required-children", "aria-hidden-focusable", "nested-interactive", "invalid-aria-attr", "invalid-aria-value", "aria-required-attr", "aria-prohibited-attr",
|
|
363
|
+
"duplicate-id", "control-label-missing", "placeholder-as-label", "form-field-multiple-labels",
|
|
364
|
+
"select-has-option", "button-empty-name", "icon-only-control-unnamed", "control-name-title-only",
|
|
365
|
+
"field-purpose-incomplete", "disabled-context-content",
|
|
366
|
+
],
|
|
367
|
+
"4.1.3": ["live-region-conflict", "status-message-not-assertive"],
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
function automatabilityOf(sc) {
|
|
371
|
+
if (STATIC.has(sc)) return "static";
|
|
372
|
+
if (NEEDS_RENDERING.has(sc)) return "needs-rendering";
|
|
373
|
+
return "judgment";
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Best-effort, language-NEUTRAL technique seeds: roll up every shipped standards pack's
|
|
377
|
+
// criteria onto the WCAG SCs they map to. Only the W3C technique CODES (e.g. "H36",
|
|
378
|
+
// "ARIA6") are carried — they are language-neutral, so the WCAG core stays
|
|
379
|
+
// English-clean. Localized test PROSE stays in the packs; the WCAG `verify` worklist
|
|
380
|
+
// grounds on these codes + each SC's W3C Understanding URL instead.
|
|
381
|
+
function seedFromPacks() {
|
|
382
|
+
const techniques = {}; // sc -> string[]
|
|
383
|
+
const sources = [];
|
|
384
|
+
if (existsSync(PACKS_DIR)) {
|
|
385
|
+
for (const f of readdirSync(PACKS_DIR)) if (f.endsWith(".json") && !f.endsWith(".glossary.json")) sources.push(join(PACKS_DIR, f));
|
|
386
|
+
}
|
|
387
|
+
for (const path of sources) {
|
|
388
|
+
let pack;
|
|
389
|
+
try {
|
|
390
|
+
pack = JSON.parse(readFileSync(path, "utf8"));
|
|
391
|
+
} catch {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
for (const c of pack.criteria || []) {
|
|
395
|
+
// Packs map to bare SC ids, e.g. "1.1.1".
|
|
396
|
+
const scs = (c.wcag || []).map((w) => String(w).trim().split(/\s+/)[0]);
|
|
397
|
+
for (const sc of scs) if (Array.isArray(c.techniques)) (techniques[sc] ??= []).push(...c.techniques);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const dedup = (m) => Object.fromEntries(Object.entries(m).map(([k, v]) => [k, [...new Set(v)].sort()]));
|
|
401
|
+
return { techniques: dedup(techniques), sources };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Emit src/data/wcag-universe.json — the classification dataset src/wcag.ts
|
|
405
|
+
// `knownScStatus()` loads — from the vendored full universe. `shipped` is the AA
|
|
406
|
+
// criteria list `build()` just wrote to wcag.json: the guard below fails loudly if the
|
|
407
|
+
// two independently-vendored snapshots ever disagree on what's in the core.
|
|
408
|
+
function buildUniverse(shipped) {
|
|
409
|
+
if (!existsSync(VENDOR_UNIVERSE)) {
|
|
410
|
+
console.error(`build-standards: missing vendored universe snapshot ${VENDOR_UNIVERSE}. Run: node scripts/build-standards.mjs --refresh-universe`);
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
const univ = JSON.parse(readFileSync(VENDOR_UNIVERSE, "utf8"));
|
|
414
|
+
const criteria = univ.criteria.map((c) => ({ id: c.sc, title: c.title, level: c.level, status: c.status }));
|
|
415
|
+
const out = {
|
|
416
|
+
wcagVersion: univ.wcagVersion,
|
|
417
|
+
source: univ.source,
|
|
418
|
+
criteriaSource: univ.criteriaSource,
|
|
419
|
+
provenance: univ.provenance,
|
|
420
|
+
criteria,
|
|
421
|
+
};
|
|
422
|
+
mkdirSync(DATA, { recursive: true });
|
|
423
|
+
writeFileSync(join(DATA, "wcag-universe.json"), biomeFormat(JSON.stringify(out, null, 2) + "\n", "src/data/wcag-universe.json"));
|
|
424
|
+
|
|
425
|
+
const coreIds = new Set(criteria.filter((c) => c.status === "core-AA").map((c) => c.id));
|
|
426
|
+
const shippedIds = new Set(shipped.map((c) => c.sc));
|
|
427
|
+
const missingFromUniverse = [...shippedIds].filter((id) => !coreIds.has(id));
|
|
428
|
+
const extraInUniverse = [...coreIds].filter((id) => !shippedIds.has(id));
|
|
429
|
+
if (missingFromUniverse.length || extraInUniverse.length) {
|
|
430
|
+
console.error(
|
|
431
|
+
`build-standards: wcag-universe.json's core-AA set disagrees with src/data/wcag.json — ` +
|
|
432
|
+
`missing: ${missingFromUniverse.join(", ") || "none"}; extra: ${extraInUniverse.join(", ") || "none"}`,
|
|
433
|
+
);
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
const tally = { "core-AA": 0, "out-of-core": 0, removed: 0 };
|
|
437
|
+
for (const c of criteria) tally[c.status]++;
|
|
438
|
+
console.log(
|
|
439
|
+
`build-standards: ${criteria.length} WCAG 2.x SCs classified — core-AA ${tally["core-AA"]}, out-of-core ${tally["out-of-core"]}, removed ${tally.removed} → src/data/wcag-universe.json`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function build() {
|
|
444
|
+
if (!existsSync(VENDOR)) {
|
|
445
|
+
console.error(`build-standards: missing vendored snapshot ${VENDOR}. Run: node scripts/build-standards.mjs --refresh <w3c/wcag checkout>`);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
if (!existsSync(VENDOR_FR)) {
|
|
449
|
+
console.error(`build-standards: missing vendored French titles ${VENDOR_FR}. Run: node scripts/build-standards.mjs --refresh-fr`);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
const snap = JSON.parse(readFileSync(VENDOR, "utf8"));
|
|
453
|
+
const fr = JSON.parse(readFileSync(VENDOR_FR, "utf8"));
|
|
454
|
+
const { techniques, sources } = seedFromPacks();
|
|
455
|
+
|
|
456
|
+
// Completeness guard: every shipped core-AA SC/guideline/principle MUST carry a French
|
|
457
|
+
// title from the vendored W3C authorized translation — never a silent English-only
|
|
458
|
+
// fallback in the shipped dataset (src/wcag.ts still falls back at read time for older
|
|
459
|
+
// snapshots, but a freshly-built one is never allowed to be incomplete).
|
|
460
|
+
// `?? {}` guards a corrupted/partial vendor file (e.g. missing a top-level
|
|
461
|
+
// principles/guidelines/criteria key) so it is reported through the clean
|
|
462
|
+
// "missing a French title for: …" gate below, not a raw TypeError.
|
|
463
|
+
const frPrinciples = fr.principles ?? {};
|
|
464
|
+
const frGuidelines = fr.guidelines ?? {};
|
|
465
|
+
const frCriteria = fr.criteria ?? {};
|
|
466
|
+
const missingFr = [
|
|
467
|
+
...snap.principles.filter((p) => !frPrinciples[String(p.number)]).map((p) => `principle ${p.number}`),
|
|
468
|
+
...snap.guidelines.filter((g) => !frGuidelines[g.number]).map((g) => `guideline ${g.number}`),
|
|
469
|
+
...snap.criteria.filter((c) => !frCriteria[c.sc]).map((c) => `SC ${c.sc}`),
|
|
470
|
+
];
|
|
471
|
+
if (missingFr.length) {
|
|
472
|
+
console.error(`build-standards: ${VENDOR_FR} is missing a French title for: ${missingFr.join(", ")}. Re-run: node scripts/build-standards.mjs --refresh-fr`);
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const principles = snap.principles.map((p) => ({ number: p.number, title: p.title, titleFr: frPrinciples[String(p.number)] }));
|
|
477
|
+
const guidelines = snap.guidelines.map((g) => ({ number: g.number, title: g.title, titleFr: frGuidelines[g.number] }));
|
|
478
|
+
|
|
479
|
+
const criteria = snap.criteria.map((c) => {
|
|
480
|
+
const sc = {
|
|
481
|
+
sc: c.sc,
|
|
482
|
+
principle: c.principle,
|
|
483
|
+
guideline: c.guideline,
|
|
484
|
+
title: c.title,
|
|
485
|
+
titleFr: frCriteria[c.sc],
|
|
486
|
+
level: c.level,
|
|
487
|
+
addedIn: c.addedIn,
|
|
488
|
+
automatability: automatabilityOf(c.sc),
|
|
489
|
+
ruleIds: RULE_SC_COVERAGE[c.sc] || [],
|
|
490
|
+
understanding: `https://www.w3.org/WAI/WCAG22/Understanding/${c.slug}.html`,
|
|
491
|
+
};
|
|
492
|
+
if (techniques[c.sc]?.length) sc.techniques = techniques[c.sc];
|
|
493
|
+
return sc;
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const out = {
|
|
497
|
+
wcagVersion: snap.wcagVersion,
|
|
498
|
+
level: "AA",
|
|
499
|
+
source: snap.source,
|
|
500
|
+
license: "W3C Document License",
|
|
501
|
+
criteriaSource: snap.criteriaSource,
|
|
502
|
+
principles,
|
|
503
|
+
guidelines,
|
|
504
|
+
criteria,
|
|
505
|
+
};
|
|
506
|
+
mkdirSync(DATA, { recursive: true });
|
|
507
|
+
writeFileSync(join(DATA, "wcag.json"), biomeFormat(JSON.stringify(out, null, 2) + "\n", "src/data/wcag.json"));
|
|
508
|
+
|
|
509
|
+
// --- guards
|
|
510
|
+
const all = new Set(criteria.map((c) => c.sc));
|
|
511
|
+
const missingForRules = Object.keys(RULE_SC_COVERAGE).filter((sc) => !all.has(sc));
|
|
512
|
+
if (missingForRules.length) {
|
|
513
|
+
console.error(`build-standards: RULE_SC_COVERAGE references SCs absent from the dataset: ${missingForRules.join(", ")}`);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
// Any pack referencing an SC not in the core is a dangling ref (e.g. dropped 4.1.1).
|
|
517
|
+
for (const path of sources) {
|
|
518
|
+
const pack = JSON.parse(readFileSync(path, "utf8"));
|
|
519
|
+
if (!pack.criteria) continue;
|
|
520
|
+
const dangling = new Set();
|
|
521
|
+
for (const c of pack.criteria) for (const w of c.wcag || []) {
|
|
522
|
+
const id = String(w).trim().split(/\s+/)[0];
|
|
523
|
+
if (!all.has(id)) dangling.add(id);
|
|
524
|
+
}
|
|
525
|
+
if (dangling.size) console.warn(`build-standards: ${path} maps to SCs absent from WCAG 2.2 AA core: ${[...dangling].sort().join(", ")} (expected for out-of-core SCs — WCAG AAA or removed/obsolete)`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const tally = { static: 0, "needs-rendering": 0, judgment: 0 };
|
|
529
|
+
for (const c of criteria) tally[c.automatability]++;
|
|
530
|
+
console.log(`build-standards: ${criteria.length} WCAG 2.2 A/AA criteria → src/data/wcag.json`);
|
|
531
|
+
console.log(`build-standards: automatability — static ${tally.static}, needs-rendering ${tally["needs-rendering"]}, judgment ${tally.judgment}`);
|
|
532
|
+
console.log(`build-standards: seeded techniques from ${sources.length ? sources.map((s) => s.replace(root + "/", "")).join(", ") : "(no pack found — empty)"}`);
|
|
533
|
+
|
|
534
|
+
buildUniverse(criteria);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function main() {
|
|
538
|
+
const refreshIdx = process.argv.indexOf("--refresh");
|
|
539
|
+
if (refreshIdx !== -1) deriveSnapshot(process.argv[refreshIdx + 1]);
|
|
540
|
+
if (process.argv.includes("--refresh-universe")) await deriveUniverse();
|
|
541
|
+
if (process.argv.includes("--refresh-fr")) await deriveFr();
|
|
542
|
+
build();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
main().catch((e) => {
|
|
546
|
+
console.error(e);
|
|
547
|
+
process.exit(1);
|
|
548
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Normalise the source-of-truth bundle (scripts/ultra11y.mjs, produced by
|
|
3
|
+
// tsup) and mirror it byte-for-byte into every skill package. Skills ship
|
|
4
|
+
// standalone — `npx skills add` copies one skill directory (skills/<name>/),
|
|
5
|
+
// so the engine has to live next to each SKILL.md, not just at the repo root.
|
|
6
|
+
// Keeping the copies identical is what `check:build` asserts so a published
|
|
7
|
+
// skill can never drift from the tested bundle.
|
|
8
|
+
//
|
|
9
|
+
// REPRODUCIBILITY: esbuild annotates each bundled module with a `// <path>`
|
|
10
|
+
// comment derived from where the dependency physically resides in node_modules.
|
|
11
|
+
// pnpm may place a transitive dep either hoisted at node_modules/<pkg>/ or in
|
|
12
|
+
// its virtual store at node_modules/.pnpm/<pkg>@<version>/node_modules/<pkg>/,
|
|
13
|
+
// and the layout differs between a developer's incremental install and CI's
|
|
14
|
+
// frozen one. That made the committed bundle non-reproducible and broke
|
|
15
|
+
// `check:build`'s `git diff --exit-code`. We canonicalise those store paths to
|
|
16
|
+
// the hoisted form so the emitted bundle is byte-identical everywhere.
|
|
17
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
22
|
+
const source = join(root, "scripts", "ultra11y.mjs");
|
|
23
|
+
const targets = [
|
|
24
|
+
join(root, "skills", "ultra11y", "scripts", "ultra11y.mjs"),
|
|
25
|
+
join(root, "skills", "review-a11y", "scripts", "ultra11y.mjs"),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// node_modules/.pnpm/<pkg>@<version>[_<peerhash>]/node_modules/ -> node_modules/
|
|
29
|
+
const normalizePnpmPaths = (src) =>
|
|
30
|
+
src.replace(/node_modules\/\.pnpm\/[^/]+\/node_modules\//g, "node_modules/");
|
|
31
|
+
|
|
32
|
+
const normalized = normalizePnpmPaths(readFileSync(source, "utf8"));
|
|
33
|
+
writeFileSync(source, normalized);
|
|
34
|
+
|
|
35
|
+
for (const target of targets) {
|
|
36
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
37
|
+
writeFileSync(target, normalized);
|
|
38
|
+
console.log(`copy-bundle: ${source} -> ${target}`);
|
|
39
|
+
}
|