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
package/src/render.ts
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// `render` — get auditable RENDERED HTML for a project, so component libraries
|
|
2
|
+
// (DSFR, MUI…) are audited as the HTML they actually produce, not their JSX
|
|
3
|
+
// sources. ultra11y is install-free and cannot import a project's components
|
|
4
|
+
// itself (no React, no toolchain), so `render` does the two things it honestly
|
|
5
|
+
// can: DETECT the framework and advise the exact build→audit path, and SCAFFOLD a
|
|
6
|
+
// runnable SSR-snapshot harness that uses the PROJECT'S OWN react-dom/server. The
|
|
7
|
+
// emitted HTML is then audited by the normal `audit` command.
|
|
8
|
+
import type { Lang } from "./types.js";
|
|
9
|
+
import type { CaptureProvenance } from "./parse/html.js";
|
|
10
|
+
|
|
11
|
+
export interface FrameworkHit {
|
|
12
|
+
name: string;
|
|
13
|
+
buildCmd: string;
|
|
14
|
+
outDir: string; // directory of emitted HTML to audit
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Detection {
|
|
18
|
+
frameworks: FrameworkHit[];
|
|
19
|
+
componentLibraries: string[]; // design systems whose rendered output source-audit can't see
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const KNOWN_LIBS: Record<string, string> = {
|
|
23
|
+
"@codegouvfr/react-dsfr": "DSFR (Système de Design de l'État)",
|
|
24
|
+
"@gouvfr/dsfr": "DSFR",
|
|
25
|
+
"@mui/material": "MUI",
|
|
26
|
+
"@chakra-ui/react": "Chakra UI",
|
|
27
|
+
antd: "Ant Design",
|
|
28
|
+
"@mantine/core": "Mantine",
|
|
29
|
+
"react-bootstrap": "React-Bootstrap",
|
|
30
|
+
"@radix-ui/react-dropdown-menu": "Radix UI",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Pure detection from a merged deps map + a "does this config file exist" probe. */
|
|
34
|
+
export function detectFrameworks(deps: Record<string, string>, has: (file: string) => boolean): Detection {
|
|
35
|
+
const dep = (n: string): boolean => Object.hasOwn(deps, n);
|
|
36
|
+
const anyConfig = (...files: string[]): boolean => files.some((f) => has(f));
|
|
37
|
+
const frameworks: FrameworkHit[] = [];
|
|
38
|
+
|
|
39
|
+
if (dep("next") || anyConfig("next.config.js", "next.config.mjs", "next.config.ts"))
|
|
40
|
+
frameworks.push({ name: "Next.js", buildCmd: "npx next build # set `output: 'export'` in next.config to emit static HTML", outDir: "out" });
|
|
41
|
+
if (dep("astro") || anyConfig("astro.config.mjs", "astro.config.ts", "astro.config.js"))
|
|
42
|
+
frameworks.push({ name: "Astro", buildCmd: "npx astro build", outDir: "dist" });
|
|
43
|
+
if (dep("@sveltejs/kit")) frameworks.push({ name: "SvelteKit", buildCmd: "npx vite build", outDir: "build" });
|
|
44
|
+
if (dep("react-scripts")) frameworks.push({ name: "CRA", buildCmd: "npx react-scripts build", outDir: "build" });
|
|
45
|
+
if (Object.keys(deps).some((d) => d === "storybook" || d.startsWith("@storybook/")) || has(".storybook"))
|
|
46
|
+
frameworks.push({ name: "Storybook", buildCmd: "npx storybook build", outDir: "storybook-static" });
|
|
47
|
+
// Vite last (generic) so a more specific framework above is preferred in guidance.
|
|
48
|
+
if (dep("vite") || anyConfig("vite.config.js", "vite.config.ts", "vite.config.mjs"))
|
|
49
|
+
frameworks.push({ name: "Vite", buildCmd: "npx vite build", outDir: "dist" });
|
|
50
|
+
|
|
51
|
+
const componentLibraries: string[] = [];
|
|
52
|
+
for (const [pkg, label] of Object.entries(KNOWN_LIBS)) if (dep(pkg)) componentLibraries.push(label);
|
|
53
|
+
|
|
54
|
+
return { frameworks, componentLibraries };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const L = {
|
|
58
|
+
fr: {
|
|
59
|
+
title: "Obtenir du HTML rendu à auditer (render)",
|
|
60
|
+
why: "Auditer les sources JSX d'une bibliothèque de composants donne des faux négatifs : il faut auditer le HTML réellement produit.",
|
|
61
|
+
libNote: (libs: string) =>
|
|
62
|
+
`Bibliothèque(s) détectée(s) : ${libs}. Leur sortie n'est PAS visible en analyse de source — auditez le build ou un snapshot SSR.`,
|
|
63
|
+
detected: "Frameworks détectés",
|
|
64
|
+
none: "Aucun framework détecté.",
|
|
65
|
+
build: "Build",
|
|
66
|
+
then: "puis auditer",
|
|
67
|
+
sbReco: "Le plus simple pour un design-system : un build Storybook statique, puis auditer chaque story rendue.",
|
|
68
|
+
ssr: "Sinon, snapshot SSR : `render --scaffold` écrit un harnais react-dom/server à compléter.",
|
|
69
|
+
dyn: "Pour les critères de rendu (contraste calculé, focus, reflow) : `scan <url>` (tier Docker).",
|
|
70
|
+
scaffoldWrote: (p: string) => `Harnais SSR écrit : ${p}`,
|
|
71
|
+
scaffoldRun: "Complétez COMPONENTS, exécutez-le avec votre toolchain (ex. `npx tsx`), puis auditez le dossier produit.",
|
|
72
|
+
},
|
|
73
|
+
en: {
|
|
74
|
+
title: "Get rendered HTML to audit (render)",
|
|
75
|
+
why: "Auditing the JSX sources of a component library yields false negatives: audit the HTML it actually produces.",
|
|
76
|
+
libNote: (libs: string) => `Detected library(ies): ${libs}. Their output is NOT visible to source analysis — audit the build or an SSR snapshot.`,
|
|
77
|
+
detected: "Detected frameworks",
|
|
78
|
+
none: "No framework detected.",
|
|
79
|
+
build: "Build",
|
|
80
|
+
then: "then audit",
|
|
81
|
+
sbReco: "Easiest for a design system: a static Storybook build, then audit each rendered story.",
|
|
82
|
+
ssr: "Otherwise, SSR snapshot: `render --scaffold` writes a react-dom/server harness to fill in.",
|
|
83
|
+
dyn: "For rendering criteria (computed contrast, focus, reflow): `scan <url>` (Docker tier).",
|
|
84
|
+
scaffoldWrote: (p: string) => `SSR harness written: ${p}`,
|
|
85
|
+
scaffoldRun: "Fill in COMPONENTS, run it with your toolchain (e.g. `npx tsx`), then audit the produced folder.",
|
|
86
|
+
},
|
|
87
|
+
} as const;
|
|
88
|
+
|
|
89
|
+
export function renderPlan(d: Detection, lang: Lang = "fr"): string {
|
|
90
|
+
const s = L[lang];
|
|
91
|
+
const out: string[] = [`# ${s.title}`, "", `> ${s.why}`, ""];
|
|
92
|
+
if (d.componentLibraries.length) out.push(`> 🧩 ${s.libNote(d.componentLibraries.join(", "))}`, "");
|
|
93
|
+
out.push(`## ${s.detected}`, "");
|
|
94
|
+
if (!d.frameworks.length) out.push(s.none, "");
|
|
95
|
+
else
|
|
96
|
+
for (const f of d.frameworks) {
|
|
97
|
+
out.push(`- **${f.name}** — ${s.build} : \`${f.buildCmd}\``);
|
|
98
|
+
out.push(` - ${s.then} : \`node scripts/ultra11y.mjs audit "${f.outDir}/**/*.html"\``);
|
|
99
|
+
}
|
|
100
|
+
out.push("", `- ${s.sbReco}`, `- ${s.ssr}`, `- ${s.dyn}`, "");
|
|
101
|
+
return out.join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The SSR-snapshot harness written by `render --scaffold`. Uses the PROJECT'S own
|
|
105
|
+
* React + components; ultra11y bundles none of it. */
|
|
106
|
+
export function ssrHarness(): string {
|
|
107
|
+
return `// ultra11y SSR snapshot harness — render your components to static HTML for audit.
|
|
108
|
+
// 1. Fill in COMPONENTS with your real components (DSFR, design-system, pages…).
|
|
109
|
+
// 2. Run it with your project's toolchain, e.g.: npx tsx ultra11y-render.tsx
|
|
110
|
+
// 3. Audit the produced HTML: node scripts/ultra11y.mjs audit "audits/rendered/**/*.html"
|
|
111
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
112
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
113
|
+
// import { Button } from "@codegouvfr/react-dsfr/Button";
|
|
114
|
+
|
|
115
|
+
const COMPONENTS: { name: string; element: JSX.Element }[] = [
|
|
116
|
+
// { name: "button-icon", element: <Button iconId="fr-icon-add-line" title="Ajouter" /> },
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const OUT = "audits/rendered";
|
|
120
|
+
mkdirSync(OUT, { recursive: true });
|
|
121
|
+
for (const { name, element } of COMPONENTS) {
|
|
122
|
+
const body = renderToStaticMarkup(element);
|
|
123
|
+
const html = \`<!doctype html>
|
|
124
|
+
<html lang="fr">
|
|
125
|
+
<head><meta charset="utf-8"><title>\${name}</title></head>
|
|
126
|
+
<body>
|
|
127
|
+
\${body}
|
|
128
|
+
</body>
|
|
129
|
+
</html>
|
|
130
|
+
\`;
|
|
131
|
+
writeFileSync(\`\${OUT}/\${name}.html\`, html);
|
|
132
|
+
console.log(\`ultra11y-render: wrote \${OUT}/\${name}.html\`);
|
|
133
|
+
}
|
|
134
|
+
if (COMPONENTS.length === 0) console.error("ultra11y-render: COMPONENTS is empty — add your components, then re-run.");
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface TestRunner {
|
|
139
|
+
runner: "vitest" | "jest" | null;
|
|
140
|
+
dom: "jsdom" | "happy-dom" | null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Detect the test runner + DOM implementation from a deps map + config-file probe, so
|
|
144
|
+
* `render --setup` can print the exact wiring (and warn when a DOM env is missing). */
|
|
145
|
+
export function detectTestRunner(deps: Record<string, string>, has: (file: string) => boolean): TestRunner {
|
|
146
|
+
const dep = (n: string): boolean => Object.hasOwn(deps, n);
|
|
147
|
+
const runner: TestRunner["runner"] =
|
|
148
|
+
dep("vitest") || has("vitest.config.ts") || has("vitest.config.js") || has("vitest.config.mjs")
|
|
149
|
+
? "vitest"
|
|
150
|
+
: dep("jest") || has("jest.config.ts") || has("jest.config.js") || has("jest.config.cjs") || has("jest.config.mjs")
|
|
151
|
+
? "jest"
|
|
152
|
+
: null;
|
|
153
|
+
const dom: TestRunner["dom"] = dep("jsdom") ? "jsdom" : dep("happy-dom") ? "happy-dom" : null;
|
|
154
|
+
return { runner, dom };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The zero-touch capture harvester written by `render --setup`. Added ONCE to the test
|
|
158
|
+
* runner's setup, its global afterEach serializes every rendered component's DOM to a
|
|
159
|
+
* provenance-tagged .html the static engine audits. Dependency-free; runs in the user's
|
|
160
|
+
* test process (jsdom/happy-dom document + node fs), embeds no framework. */
|
|
161
|
+
export function captureSetup(): string {
|
|
162
|
+
return `// ultra11y capture harvester — zero-touch rendered-DOM snapshots for accessibility audit.
|
|
163
|
+
// Add this ONCE to your test runner's setup; every component your tests render is then
|
|
164
|
+
// serialized to .ultra11y/captures/*.html and audited by \`ultra11y audit\`. No per-test code.
|
|
165
|
+
// Vitest: test: { environment: "jsdom", globals: true, setupFiles: [".ultra11y/capture-setup.mjs"] }
|
|
166
|
+
// Jest: testEnvironment: "jsdom", setupFilesAfterEnv: ["<rootDir>/.ultra11y/capture-setup.mjs"]
|
|
167
|
+
// List it LAST so its afterEach runs BEFORE Testing-Library cleanup empties the DOM.
|
|
168
|
+
// Off: ULTRA11Y_CAPTURES=off. Custom dir: ULTRA11Y_CAPTURES=path/to/dir.
|
|
169
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
170
|
+
import { dirname, relative, basename } from "node:path";
|
|
171
|
+
|
|
172
|
+
const OUT = process.env.ULTRA11Y_CAPTURES || ".ultra11y/captures";
|
|
173
|
+
const afterEachFn = typeof globalThis.afterEach === "function" ? globalThis.afterEach : null;
|
|
174
|
+
const hasDom = typeof document !== "undefined";
|
|
175
|
+
|
|
176
|
+
// Optional per-test override, consumed by the NEXT capture — for a test file whose name
|
|
177
|
+
// doesn't match the component, or a single test rendering several distinct components.
|
|
178
|
+
// import { captureAs } from ".ultra11y/capture-setup.mjs"; captureAs("Button", "src/Button.tsx")
|
|
179
|
+
// (or globalThis.ultra11yCaptureAs("Button")).
|
|
180
|
+
let __override = null;
|
|
181
|
+
export function captureAs(component, source) { __override = { component: component, source: source }; }
|
|
182
|
+
if (typeof globalThis !== "undefined") globalThis.ultra11yCaptureAs = captureAs;
|
|
183
|
+
|
|
184
|
+
if (OUT !== "off" && OUT !== "0") {
|
|
185
|
+
if (!hasDom) {
|
|
186
|
+
// Not a DOM test environment — nothing to serialize. Set environment: "jsdom".
|
|
187
|
+
} else if (!afterEachFn) {
|
|
188
|
+
process.stderr.write("ultra11y capture: afterEach is not global — enable Vitest \\\`globals: true\\\` (or use Jest setupFilesAfterEnv) so captures can register.\\n");
|
|
189
|
+
} else {
|
|
190
|
+
let written = 0;
|
|
191
|
+
const esc = (s) => String(s).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">").replace(/--/g, "--");
|
|
192
|
+
afterEachFn(() => {
|
|
193
|
+
try {
|
|
194
|
+
if (typeof document === "undefined" || !document.body) return;
|
|
195
|
+
const html = document.body.innerHTML;
|
|
196
|
+
if (!html || !html.trim()) return;
|
|
197
|
+
const st = (globalThis.expect && globalThis.expect.getState && globalThis.expect.getState()) || {};
|
|
198
|
+
const testPath = typeof st.testPath === "string" ? st.testPath : "";
|
|
199
|
+
const name = typeof st.currentTestName === "string" ? st.currentTestName : "";
|
|
200
|
+
const rel = testPath ? relative(process.cwd(), testPath).split("\\\\").join("/") : "";
|
|
201
|
+
let source = rel ? rel.replace(/\\.(test|spec)\\.([jt]sx?)$/i, ".$2") : "";
|
|
202
|
+
let component = source ? basename(source).replace(/\\.[jt]sx?$/i, "") : "";
|
|
203
|
+
if (__override) {
|
|
204
|
+
if (__override.component) component = __override.component;
|
|
205
|
+
if (__override.source) source = __override.source;
|
|
206
|
+
__override = null;
|
|
207
|
+
}
|
|
208
|
+
const attrs = ['v="1"'];
|
|
209
|
+
if (source) attrs.push('source="' + esc(source) + '"');
|
|
210
|
+
if (component) attrs.push('component="' + esc(component) + '"');
|
|
211
|
+
if (rel) attrs.push('test="' + esc(rel) + '"');
|
|
212
|
+
if (name) attrs.push('name="' + esc(name) + '"');
|
|
213
|
+
const content = "<!-- ultra11y:capture " + attrs.join(" ") + " -->\\n" + html + "\\n";
|
|
214
|
+
let h = 5381;
|
|
215
|
+
const keyStr = testPath + "::" + name;
|
|
216
|
+
for (let i = 0; i < keyStr.length; i++) h = ((h << 5) + h + keyStr.charCodeAt(i)) >>> 0;
|
|
217
|
+
const file = OUT + "/" + (component || "capture") + "__" + h.toString(36) + ".html";
|
|
218
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
219
|
+
if (existsSync(file) && readFileSync(file, "utf8") === content) return; // unchanged — keep byte-stable
|
|
220
|
+
writeFileSync(file, content);
|
|
221
|
+
written++;
|
|
222
|
+
} catch {
|
|
223
|
+
// never fail a user's test because a capture could not be written
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
if (typeof process.on === "function")
|
|
227
|
+
process.on("exit", () => {
|
|
228
|
+
if (written === 0)
|
|
229
|
+
process.stderr.write("ultra11y capture: 0 captures written — ensure a DOM env (jsdom) and that this setup runs before Testing-Library cleanup (list it LAST in setupFiles).\\n");
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface StorybookStory {
|
|
237
|
+
id: string;
|
|
238
|
+
title?: string;
|
|
239
|
+
name?: string;
|
|
240
|
+
importPath?: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Parse a Storybook `index.json` (v7+, `entries`) or `stories.json` (v6, `stories`) into
|
|
244
|
+
* its story entries (docs/MDX entries dropped). Returns [] on anything malformed. */
|
|
245
|
+
export function parseStorybookIndex(json: string): StorybookStory[] {
|
|
246
|
+
let data: unknown;
|
|
247
|
+
try {
|
|
248
|
+
data = JSON.parse(json);
|
|
249
|
+
} catch {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
const rec = data as { entries?: Record<string, unknown>; stories?: Record<string, unknown> } | null;
|
|
253
|
+
const entries = rec?.entries ?? rec?.stories;
|
|
254
|
+
if (!entries || typeof entries !== "object") return [];
|
|
255
|
+
const out: StorybookStory[] = [];
|
|
256
|
+
for (const [id, raw] of Object.entries(entries)) {
|
|
257
|
+
if (!raw || typeof raw !== "object") continue; // tolerate null/garbage entry values
|
|
258
|
+
const e = raw as { id?: string; title?: string; name?: string; importPath?: string; type?: string };
|
|
259
|
+
if (e.type && e.type !== "story") continue; // skip docs/MDX entries (v7 `type`)
|
|
260
|
+
out.push({ id: e.id ?? id, title: e.title, name: e.name, importPath: e.importPath });
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Best-effort provenance for a Storybook story: source from importPath (a `.stories.*`
|
|
266
|
+
* file → its sibling component file), component from the title's last segment. */
|
|
267
|
+
export function storyProvenance(story: StorybookStory): CaptureProvenance | undefined {
|
|
268
|
+
const imp = story.importPath;
|
|
269
|
+
if (!imp) return undefined;
|
|
270
|
+
const sourceFile = imp.replace(/^\.\//, "").replace(/\.stories\.([jt]sx?)$/i, ".$1");
|
|
271
|
+
const fromTitle = story.title ? story.title.split("/").pop()?.trim() : undefined;
|
|
272
|
+
const component = fromTitle || sourceFile.replace(/^.*\//, "").replace(/\.[jt]sx?$/i, "");
|
|
273
|
+
return { v: 1, sourceFile, ...(component ? { component } : {}), ...(story.name ? { name: story.name } : {}) };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Instructions printed by `render --setup` after writing the harvester. */
|
|
277
|
+
export function captureSetupPlan(tr: TestRunner, path: string, lang: Lang = "en"): string {
|
|
278
|
+
const fr = lang === "fr";
|
|
279
|
+
const out: string[] = [];
|
|
280
|
+
out.push(fr ? `Harnais de capture écrit : ${path}` : `Capture harvester written: ${path}`);
|
|
281
|
+
out.push("");
|
|
282
|
+
if (tr.runner === "jest") {
|
|
283
|
+
out.push(fr ? "Jest — ajoutez à votre config :" : "Jest — add to your config:");
|
|
284
|
+
out.push(` testEnvironment: "jsdom",`);
|
|
285
|
+
out.push(` setupFilesAfterEnv: ["<rootDir>/${path}"] // ${fr ? "PAS setupFiles (afterEach doit exister)" : "NOT setupFiles (afterEach must exist)"}`);
|
|
286
|
+
} else {
|
|
287
|
+
out.push(fr ? "Vitest — ajoutez à vitest.config.ts (test:) :" : "Vitest — add to vitest.config.ts (test:):");
|
|
288
|
+
out.push(` environment: "jsdom",`);
|
|
289
|
+
out.push(
|
|
290
|
+
` globals: true, // ${fr ? "requis : sans ça, afterEach n'est pas global et rien n'est capturé" : "required: without it afterEach is not global and nothing is captured"}`,
|
|
291
|
+
);
|
|
292
|
+
out.push(` setupFiles: ["${path}"], // ${fr ? "en DERNIER (avant le cleanup Testing-Library)" : "list LAST (before Testing-Library cleanup)"}`);
|
|
293
|
+
}
|
|
294
|
+
out.push("");
|
|
295
|
+
if (!tr.dom)
|
|
296
|
+
out.push(
|
|
297
|
+
fr
|
|
298
|
+
? "⚠️ Aucun jsdom/happy-dom détecté — installez-en un et activez l'environnement DOM."
|
|
299
|
+
: "⚠️ No jsdom/happy-dom detected — install one and enable the DOM environment.",
|
|
300
|
+
);
|
|
301
|
+
out.push(fr ? "Puis : lancez vos tests, committez .ultra11y/captures, et auditez :" : "Then: run your tests, commit .ultra11y/captures, and audit:");
|
|
302
|
+
out.push(` node scripts/ultra11y.mjs audit --require-captures # ${fr ? "gate : composants sans capture" : "gate: components lacking a capture"}`);
|
|
303
|
+
out.push(
|
|
304
|
+
` git add .ultra11y .gitattributes # ${fr ? "committez les captures (+ .gitattributes généré)" : "commit the captures (+ generated .gitattributes)"}`,
|
|
305
|
+
);
|
|
306
|
+
return out.join("\n");
|
|
307
|
+
}
|