// `render` — get auditable RENDERED HTML for a project, so component libraries // (DSFR, MUI…) are audited as the HTML they actually produce, not their JSX // sources. ultra11y is install-free and cannot import a project's components // itself (no React, no toolchain), so `render` does the two things it honestly // can: DETECT the framework and advise the exact build→audit path, and SCAFFOLD a // runnable SSR-snapshot harness that uses the PROJECT'S OWN react-dom/server. The // emitted HTML is then audited by the normal `audit` command. import type { Lang } from "./types.js"; import type { CaptureProvenance } from "./parse/html.js"; export interface FrameworkHit { name: string; buildCmd: string; outDir: string; // directory of emitted HTML to audit } export interface Detection { frameworks: FrameworkHit[]; componentLibraries: string[]; // design systems whose rendered output source-audit can't see } const KNOWN_LIBS: Record = { "@codegouvfr/react-dsfr": "DSFR (Système de Design de l'État)", "@gouvfr/dsfr": "DSFR", "@mui/material": "MUI", "@chakra-ui/react": "Chakra UI", antd: "Ant Design", "@mantine/core": "Mantine", "react-bootstrap": "React-Bootstrap", "@radix-ui/react-dropdown-menu": "Radix UI", }; /** Pure detection from a merged deps map + a "does this config file exist" probe. */ export function detectFrameworks(deps: Record, has: (file: string) => boolean): Detection { const dep = (n: string): boolean => Object.hasOwn(deps, n); const anyConfig = (...files: string[]): boolean => files.some((f) => has(f)); const frameworks: FrameworkHit[] = []; if (dep("next") || anyConfig("next.config.js", "next.config.mjs", "next.config.ts")) frameworks.push({ name: "Next.js", buildCmd: "npx next build # set `output: 'export'` in next.config to emit static HTML", outDir: "out" }); if (dep("astro") || anyConfig("astro.config.mjs", "astro.config.ts", "astro.config.js")) frameworks.push({ name: "Astro", buildCmd: "npx astro build", outDir: "dist" }); if (dep("@sveltejs/kit")) frameworks.push({ name: "SvelteKit", buildCmd: "npx vite build", outDir: "build" }); if (dep("react-scripts")) frameworks.push({ name: "CRA", buildCmd: "npx react-scripts build", outDir: "build" }); if (Object.keys(deps).some((d) => d === "storybook" || d.startsWith("@storybook/")) || has(".storybook")) frameworks.push({ name: "Storybook", buildCmd: "npx storybook build", outDir: "storybook-static" }); // Vite last (generic) so a more specific framework above is preferred in guidance. if (dep("vite") || anyConfig("vite.config.js", "vite.config.ts", "vite.config.mjs")) frameworks.push({ name: "Vite", buildCmd: "npx vite build", outDir: "dist" }); const componentLibraries: string[] = []; for (const [pkg, label] of Object.entries(KNOWN_LIBS)) if (dep(pkg)) componentLibraries.push(label); return { frameworks, componentLibraries }; } const L = { fr: { title: "Obtenir du HTML rendu à auditer (render)", why: "Auditer les sources JSX d'une bibliothèque de composants donne des faux négatifs : il faut auditer le HTML réellement produit.", libNote: (libs: string) => `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.`, detected: "Frameworks détectés", none: "Aucun framework détecté.", build: "Build", then: "puis auditer", sbReco: "Le plus simple pour un design-system : un build Storybook statique, puis auditer chaque story rendue.", ssr: "Sinon, snapshot SSR : `render --scaffold` écrit un harnais react-dom/server à compléter.", dyn: "Pour les critères de rendu (contraste calculé, focus, reflow) : `scan ` (tier Docker).", scaffoldWrote: (p: string) => `Harnais SSR écrit : ${p}`, scaffoldRun: "Complétez COMPONENTS, exécutez-le avec votre toolchain (ex. `npx tsx`), puis auditez le dossier produit.", }, en: { title: "Get rendered HTML to audit (render)", why: "Auditing the JSX sources of a component library yields false negatives: audit the HTML it actually produces.", libNote: (libs: string) => `Detected library(ies): ${libs}. Their output is NOT visible to source analysis — audit the build or an SSR snapshot.`, detected: "Detected frameworks", none: "No framework detected.", build: "Build", then: "then audit", sbReco: "Easiest for a design system: a static Storybook build, then audit each rendered story.", ssr: "Otherwise, SSR snapshot: `render --scaffold` writes a react-dom/server harness to fill in.", dyn: "For rendering criteria (computed contrast, focus, reflow): `scan ` (Docker tier).", scaffoldWrote: (p: string) => `SSR harness written: ${p}`, scaffoldRun: "Fill in COMPONENTS, run it with your toolchain (e.g. `npx tsx`), then audit the produced folder.", }, } as const; export function renderPlan(d: Detection, lang: Lang = "fr"): string { const s = L[lang]; const out: string[] = [`# ${s.title}`, "", `> ${s.why}`, ""]; if (d.componentLibraries.length) out.push(`> 🧩 ${s.libNote(d.componentLibraries.join(", "))}`, ""); out.push(`## ${s.detected}`, ""); if (!d.frameworks.length) out.push(s.none, ""); else for (const f of d.frameworks) { out.push(`- **${f.name}** — ${s.build} : \`${f.buildCmd}\``); out.push(` - ${s.then} : \`node scripts/ultra11y.mjs audit "${f.outDir}/**/*.html"\``); } out.push("", `- ${s.sbReco}`, `- ${s.ssr}`, `- ${s.dyn}`, ""); return out.join("\n"); } /** The SSR-snapshot harness written by `render --scaffold`. Uses the PROJECT'S own * React + components; ultra11y bundles none of it. */ export function ssrHarness(): string { return `// ultra11y SSR snapshot harness — render your components to static HTML for audit. // 1. Fill in COMPONENTS with your real components (DSFR, design-system, pages…). // 2. Run it with your project's toolchain, e.g.: npx tsx ultra11y-render.tsx // 3. Audit the produced HTML: node scripts/ultra11y.mjs audit "audits/rendered/**/*.html" import { mkdirSync, writeFileSync } from "node:fs"; import { renderToStaticMarkup } from "react-dom/server"; // import { Button } from "@codegouvfr/react-dsfr/Button"; const COMPONENTS: { name: string; element: JSX.Element }[] = [ // { name: "button-icon", element: