tessera-learn 0.0.11 → 0.0.13
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/README.md +1 -0
- package/dist/audit-BBJpQGqb.js +204 -0
- package/dist/audit-BBJpQGqb.js.map +1 -0
- package/dist/plugin/a11y-cli.d.ts +1 -0
- package/dist/plugin/a11y-cli.js +36 -0
- package/dist/plugin/a11y-cli.js.map +1 -0
- package/dist/plugin/cli.js +2 -1
- package/dist/plugin/cli.js.map +1 -1
- package/dist/plugin/index.d.ts +16 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +85 -10
- package/dist/plugin/index.js.map +1 -1
- package/dist/{validation-D9DXlqNP.js → validation-B-xTvM9B.js} +342 -18
- package/dist/validation-B-xTvM9B.js.map +1 -0
- package/package.json +17 -2
- package/src/components/Accordion.svelte +3 -1
- package/src/components/AccordionItem.svelte +1 -5
- package/src/components/Audio.svelte +17 -3
- package/src/components/Callout.svelte +5 -1
- package/src/components/Carousel.svelte +24 -8
- package/src/components/DefaultLayout.svelte +41 -12
- package/src/components/FillInTheBlank.svelte +16 -6
- package/src/components/Image.svelte +12 -3
- package/src/components/LockedBanner.svelte +2 -1
- package/src/components/Matching.svelte +48 -19
- package/src/components/MediaTracks.svelte +21 -0
- package/src/components/MultipleChoice.svelte +33 -13
- package/src/components/Quiz.svelte +61 -20
- package/src/components/ResultIcon.svelte +20 -4
- package/src/components/RevealModal.svelte +25 -22
- package/src/components/Sorting.svelte +61 -26
- package/src/components/Transcript.svelte +37 -0
- package/src/components/Video.svelte +21 -18
- package/src/components/util.ts +3 -1
- package/src/components/video-embed.ts +25 -0
- package/src/index.ts +2 -7
- package/src/plugin/a11y/audit.ts +299 -0
- package/src/plugin/a11y/contrast.ts +67 -0
- package/src/plugin/a11y-cli.ts +35 -0
- package/src/plugin/cli.ts +4 -1
- package/src/plugin/export.ts +42 -14
- package/src/plugin/index.ts +216 -44
- package/src/plugin/manifest.ts +62 -22
- package/src/plugin/validation.ts +736 -122
- package/src/runtime/App.svelte +119 -48
- package/src/runtime/LoadingBar.svelte +12 -3
- package/src/runtime/Sidebar.svelte +24 -8
- package/src/runtime/access.ts +15 -3
- package/src/runtime/adapters/cmi5.ts +55 -33
- package/src/runtime/adapters/index.ts +22 -10
- package/src/runtime/adapters/retry.ts +25 -20
- package/src/runtime/adapters/scorm-base.ts +19 -15
- package/src/runtime/adapters/scorm12.ts +7 -8
- package/src/runtime/adapters/scorm2004.ts +11 -14
- package/src/runtime/adapters/web.ts +1 -1
- package/src/runtime/hooks.svelte.ts +152 -326
- package/src/runtime/interaction-format.ts +30 -12
- package/src/runtime/interaction.ts +44 -11
- package/src/runtime/navigation.svelte.ts +27 -11
- package/src/runtime/persistence.ts +2 -2
- package/src/runtime/progress.svelte.ts +13 -9
- package/src/runtime/quiz-engine.svelte.ts +361 -0
- package/src/runtime/quiz-policy.ts +9 -3
- package/src/runtime/types.ts +24 -2
- package/src/runtime/xapi/agent-rules.ts +4 -1
- package/src/runtime/xapi/client.ts +5 -5
- package/src/runtime/xapi/derive-actor.ts +2 -2
- package/src/runtime/xapi/publisher.ts +32 -29
- package/src/runtime/xapi/setup.ts +18 -15
- package/src/runtime/xapi/validation.ts +15 -6
- package/src/virtual.d.ts +4 -1
- package/styles/base.css +32 -11
- package/styles/layout.css +39 -18
- package/styles/theme.css +15 -3
- package/dist/validation-D9DXlqNP.js.map +0 -1
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ That creates a project with Tessera wired up, a starter page structure, and the
|
|
|
20
20
|
- **Vite plugin** (`tessera-learn/plugin`): `tesseraPlugin()` — wires page/layout discovery, the LMS adapter, and the export pipeline. Used in your project's `vite.config.js`.
|
|
21
21
|
- **Built-in components** (`tessera-learn`): `Callout`, `Image`, `Audio`, `Video`, `Accordion` / `AccordionItem`, `Carousel` / `CarouselSlide`, `RevealModal`, `Quiz`, `MultipleChoice`, `FillInTheBlank`, `Matching`, `Sorting`, `DefaultLayout`.
|
|
22
22
|
- **LMS adapters**: SCORM 1.2, SCORM 2004 4th Edition, cmi5, static Web — selected via `course.config.js` `export.standard`.
|
|
23
|
+
- **Accessibility checks**: static rules (alt text, media titles/captions, heading order, contrast, `<html lang>`) run inside validation and the build with zero extra dependencies; an opt-in runtime audit (`tessera-a11y`, with `playwright` + `@axe-core/playwright` as optional peers) renders every page and gates on axe-core violations.
|
|
23
24
|
|
|
24
25
|
See `AGENTS.md` for usage, signatures, and authoring conventions.
|
|
25
26
|
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { o as generateManifest, r as normalizeA11y, s as readCourseConfig } from "./validation-B-xTvM9B.js";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
4
|
+
//#region src/plugin/a11y/audit.ts
|
|
5
|
+
const IMPACT_RANK = {
|
|
6
|
+
minor: 1,
|
|
7
|
+
moderate: 2,
|
|
8
|
+
serious: 3,
|
|
9
|
+
critical: 4
|
|
10
|
+
};
|
|
11
|
+
const AUDIT_ENV_FLAG = "TESSERA_A11Y_AUDIT";
|
|
12
|
+
/** Map the `a11y.standard` enum to axe's cumulative `runOnly` tag list. */
|
|
13
|
+
function axeTags(standard) {
|
|
14
|
+
switch (standard) {
|
|
15
|
+
case "wcag2a": return ["wcag2a"];
|
|
16
|
+
case "wcag21aa": return [
|
|
17
|
+
"wcag2a",
|
|
18
|
+
"wcag2aa",
|
|
19
|
+
"wcag21a",
|
|
20
|
+
"wcag21aa"
|
|
21
|
+
];
|
|
22
|
+
default: return ["wcag2a", "wcag2aa"];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** axe-applicable ignore entries: drop the Tier-1a/1b namespaces. */
|
|
26
|
+
function axeIgnoreRules(ignore) {
|
|
27
|
+
return ignore.filter((id) => !id.startsWith("tessera/") && !id.startsWith("a11y_"));
|
|
28
|
+
}
|
|
29
|
+
function isFailing(v, thresholdRank) {
|
|
30
|
+
return !v.impact || IMPACT_RANK[v.impact] >= thresholdRank;
|
|
31
|
+
}
|
|
32
|
+
async function tryImport(specifier) {
|
|
33
|
+
return import(specifier);
|
|
34
|
+
}
|
|
35
|
+
async function loadDeps() {
|
|
36
|
+
let chromium;
|
|
37
|
+
for (const spec of ["playwright", "@playwright/test"]) try {
|
|
38
|
+
const mod = await tryImport(spec);
|
|
39
|
+
if (mod.chromium) {
|
|
40
|
+
chromium = mod.chromium;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
if (!chromium) return {
|
|
45
|
+
ok: false,
|
|
46
|
+
missing: "playwright"
|
|
47
|
+
};
|
|
48
|
+
try {
|
|
49
|
+
const mod = await tryImport("@axe-core/playwright");
|
|
50
|
+
if (!mod.default) return {
|
|
51
|
+
ok: false,
|
|
52
|
+
missing: "@axe-core/playwright"
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
deps: {
|
|
57
|
+
chromium,
|
|
58
|
+
AxeBuilder: mod.default
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
} catch {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
missing: "@axe-core/playwright"
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Run the Tier-2 runtime accessibility audit against a built course. Builds (or
|
|
70
|
+
* reuses) dist/, serves it, drives Playwright + axe-core over each page, writes
|
|
71
|
+
* a11y-report.json, and returns a process exit code (0 pass, 1 fail/error).
|
|
72
|
+
*/
|
|
73
|
+
async function runAudit(projectRoot, options = {}) {
|
|
74
|
+
const threshold = options.threshold ?? "serious";
|
|
75
|
+
const deps = await loadDeps();
|
|
76
|
+
if (!deps.ok) {
|
|
77
|
+
console.error("\x1B[31m[tessera a11y]\x1B[0m Tier 2 needs Playwright + axe-core, which aren't installed.\n Install them to run the runtime audit:\n npm i -D playwright @axe-core/playwright\n npx playwright install chromium");
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
const { chromium, AxeBuilder } = deps.deps;
|
|
81
|
+
const read = readCourseConfig(projectRoot);
|
|
82
|
+
const settings = normalizeA11y(read.ok ? read.config.a11y : void 0);
|
|
83
|
+
const tags = axeTags(settings.standard);
|
|
84
|
+
const disableRules = axeIgnoreRules(settings.ignore);
|
|
85
|
+
const manifest = generateManifest(resolve(projectRoot, "pages"));
|
|
86
|
+
const vite = await import("vite");
|
|
87
|
+
const auditDist = resolve(projectRoot, "node_modules", ".tessera-a11y");
|
|
88
|
+
const distHtml = resolve(auditDist, "index.html");
|
|
89
|
+
const prevEnv = process.env[AUDIT_ENV_FLAG];
|
|
90
|
+
process.env[AUDIT_ENV_FLAG] = "1";
|
|
91
|
+
let server;
|
|
92
|
+
try {
|
|
93
|
+
if (options.rebuild || !existsSync(distHtml)) {
|
|
94
|
+
console.log("[tessera a11y] Building course…");
|
|
95
|
+
await vite.build({
|
|
96
|
+
root: projectRoot,
|
|
97
|
+
build: {
|
|
98
|
+
outDir: auditDist,
|
|
99
|
+
emptyOutDir: true
|
|
100
|
+
},
|
|
101
|
+
logLevel: "warn"
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
server = await vite.preview({
|
|
105
|
+
root: projectRoot,
|
|
106
|
+
build: { outDir: auditDist },
|
|
107
|
+
preview: {
|
|
108
|
+
port: 0,
|
|
109
|
+
host: "127.0.0.1"
|
|
110
|
+
},
|
|
111
|
+
logLevel: "warn"
|
|
112
|
+
});
|
|
113
|
+
const baseUrl = server.resolvedUrls?.local?.[0];
|
|
114
|
+
if (!baseUrl) {
|
|
115
|
+
console.error("[tessera a11y] Could not determine preview server URL.");
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
const browser = await chromium.launch();
|
|
119
|
+
const pages = [];
|
|
120
|
+
try {
|
|
121
|
+
const page = await (await browser.newContext()).newPage();
|
|
122
|
+
const auditUrl = new URL(baseUrl);
|
|
123
|
+
auditUrl.searchParams.set("__tessera_audit", "1");
|
|
124
|
+
await page.goto(auditUrl.href, { waitUntil: "networkidle" });
|
|
125
|
+
await page.waitForSelector("#tessera-app", { timeout: 2e4 });
|
|
126
|
+
const scan = async () => {
|
|
127
|
+
const builder = new AxeBuilder({ page }).withTags(tags);
|
|
128
|
+
if (disableRules.length > 0) builder.disableRules(disableRules);
|
|
129
|
+
return (await builder.analyze()).violations.map((v) => ({
|
|
130
|
+
id: v.id,
|
|
131
|
+
impact: v.impact ?? null,
|
|
132
|
+
help: v.help,
|
|
133
|
+
helpUrl: v.helpUrl,
|
|
134
|
+
nodes: v.nodes.length
|
|
135
|
+
}));
|
|
136
|
+
};
|
|
137
|
+
const navCount = await page.locator("button.tessera-nav-page").count();
|
|
138
|
+
if (navCount === 0) pages.push({
|
|
139
|
+
index: 0,
|
|
140
|
+
title: manifest.pages[0]?.title ?? "(entry)",
|
|
141
|
+
violations: await scan()
|
|
142
|
+
});
|
|
143
|
+
else for (let i = 0; i < navCount; i++) {
|
|
144
|
+
const btn = page.locator("button.tessera-nav-page").nth(i);
|
|
145
|
+
const title = (await btn.textContent())?.trim() || `Page ${i + 1}`;
|
|
146
|
+
await btn.click();
|
|
147
|
+
await page.waitForFunction((idx) => document.querySelectorAll("button.tessera-nav-page")[idx]?.getAttribute("aria-current") === "page", i, { timeout: 2e4 });
|
|
148
|
+
await page.waitForLoadState("networkidle");
|
|
149
|
+
pages.push({
|
|
150
|
+
index: i,
|
|
151
|
+
title,
|
|
152
|
+
violations: await scan()
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
} finally {
|
|
156
|
+
await browser.close();
|
|
157
|
+
}
|
|
158
|
+
const thresholdRank = IMPACT_RANK[threshold];
|
|
159
|
+
let totalViolations = 0;
|
|
160
|
+
let failingViolations = 0;
|
|
161
|
+
for (const p of pages) for (const v of p.violations) {
|
|
162
|
+
totalViolations++;
|
|
163
|
+
if (isFailing(v, thresholdRank)) failingViolations++;
|
|
164
|
+
}
|
|
165
|
+
const report = {
|
|
166
|
+
standard: settings.standard,
|
|
167
|
+
threshold,
|
|
168
|
+
pages,
|
|
169
|
+
totalViolations,
|
|
170
|
+
failingViolations,
|
|
171
|
+
passed: failingViolations === 0
|
|
172
|
+
};
|
|
173
|
+
const reportPath = resolve(projectRoot, "a11y-report.json");
|
|
174
|
+
writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
175
|
+
printSummary(report, reportPath);
|
|
176
|
+
return report.passed ? 0 : 1;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error(`\x1b[31m[tessera a11y]\x1b[0m Audit could not complete: ${err instanceof Error ? err.message : String(err)}`);
|
|
179
|
+
return 1;
|
|
180
|
+
} finally {
|
|
181
|
+
server?.httpServer?.close?.();
|
|
182
|
+
if (prevEnv === void 0) delete process.env[AUDIT_ENV_FLAG];
|
|
183
|
+
else process.env[AUDIT_ENV_FLAG] = prevEnv;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function printSummary(report, reportPath) {
|
|
187
|
+
const thresholdRank = IMPACT_RANK[report.threshold];
|
|
188
|
+
for (const p of report.pages) {
|
|
189
|
+
if (p.violations.length === 0) {
|
|
190
|
+
console.log(`\x1b[32m ✓\x1b[0m ${p.title}`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const mark = p.violations.some((v) => isFailing(v, thresholdRank)) ? "\x1B[31m ✗\x1B[0m" : "\x1B[33m ⚠\x1B[0m";
|
|
194
|
+
console.log(`${mark} ${p.title}`);
|
|
195
|
+
for (const v of p.violations) console.log(` [${v.impact ?? "n/a"}] ${v.id} — ${v.help} (${v.nodes} node${v.nodes === 1 ? "" : "s"})`);
|
|
196
|
+
}
|
|
197
|
+
console.log(`\n[tessera a11y] Report written to ${reportPath}`);
|
|
198
|
+
if (report.passed) console.log(`\x1b[32m[tessera a11y] Passed\x1b[0m — ${report.totalViolations} total finding(s), none at/above "${report.threshold}".`);
|
|
199
|
+
else console.log(`\x1b[31m[tessera a11y] Failed\x1b[0m — ${report.failingViolations} finding(s) at/above "${report.threshold}" (of ${report.totalViolations} total).`);
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
export { runAudit as n, AUDIT_ENV_FLAG as t };
|
|
203
|
+
|
|
204
|
+
//# sourceMappingURL=audit-BBJpQGqb.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-BBJpQGqb.js","names":[],"sources":["../src/plugin/a11y/audit.ts"],"sourcesContent":["import { existsSync, writeFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { generateManifest, readCourseConfig } from '../manifest.js';\nimport { normalizeA11y, type A11ySettings } from '../validation.js';\n\nexport interface AuditOptions {\n /** Minimum violation impact that fails the run (CI gate). Default 'serious'. */\n threshold?: ImpactLevel;\n /** Force a fresh `vite build` even if dist/ exists. */\n rebuild?: boolean;\n}\n\nexport type ImpactLevel = 'minor' | 'moderate' | 'serious' | 'critical';\n\nconst IMPACT_RANK: Record<ImpactLevel, number> = {\n minor: 1,\n moderate: 2,\n serious: 3,\n critical: 4,\n};\n\n// Set by runAudit during its build/preview; the plugin forces the WebAdapter,\n// skips export packaging, and stubs xAPI while it's set. See plugin/index.ts.\nexport const AUDIT_ENV_FLAG = 'TESSERA_A11Y_AUDIT';\n\ninterface AxeViolation {\n id: string;\n impact: ImpactLevel | null;\n help: string;\n helpUrl: string;\n nodes: number;\n}\n\ninterface PageAuditResult {\n index: number;\n title: string;\n violations: AxeViolation[];\n}\n\ninterface AuditReport {\n standard: A11ySettings['standard'];\n threshold: ImpactLevel;\n pages: PageAuditResult[];\n totalViolations: number;\n failingViolations: number;\n passed: boolean;\n}\n\n/** Map the `a11y.standard` enum to axe's cumulative `runOnly` tag list. */\nexport function axeTags(standard: A11ySettings['standard']): string[] {\n switch (standard) {\n case 'wcag2a':\n return ['wcag2a'];\n case 'wcag21aa':\n return ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'];\n case 'wcag2aa':\n default:\n return ['wcag2a', 'wcag2aa'];\n }\n}\n\n/** axe-applicable ignore entries: drop the Tier-1a/1b namespaces. */\nexport function axeIgnoreRules(ignore: string[]): string[] {\n return ignore.filter(\n (id) => !id.startsWith('tessera/') && !id.startsWith('a11y_'),\n );\n}\n\n// A violation with no impact is treated as failing rather than slipping the\n// gate at every threshold.\nfunction isFailing(v: AxeViolation, thresholdRank: number): boolean {\n return !v.impact || IMPACT_RANK[v.impact] >= thresholdRank;\n}\n\n// Optional deps loaded by variable specifier so tsc doesn't require them to be\n// installed — Tier 2 is opt-in and the absence is handled with a clear message.\nasync function tryImport(specifier: string): Promise<unknown> {\n return import(specifier);\n}\n\ninterface LoadedDeps {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n chromium: any;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n AxeBuilder: any;\n}\n\nasync function loadDeps(): Promise<\n { ok: true; deps: LoadedDeps } | { ok: false; missing: string }\n> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let chromium: any;\n for (const spec of ['playwright', '@playwright/test']) {\n try {\n const mod = (await tryImport(spec)) as { chromium?: unknown };\n if (mod.chromium) {\n chromium = mod.chromium;\n break;\n }\n } catch {\n // try the next specifier\n }\n }\n if (!chromium) return { ok: false, missing: 'playwright' };\n\n try {\n const mod = (await tryImport('@axe-core/playwright')) as {\n default?: unknown;\n };\n if (!mod.default) return { ok: false, missing: '@axe-core/playwright' };\n return { ok: true, deps: { chromium, AxeBuilder: mod.default } };\n } catch {\n return { ok: false, missing: '@axe-core/playwright' };\n }\n}\n\n/**\n * Run the Tier-2 runtime accessibility audit against a built course. Builds (or\n * reuses) dist/, serves it, drives Playwright + axe-core over each page, writes\n * a11y-report.json, and returns a process exit code (0 pass, 1 fail/error).\n */\nexport async function runAudit(\n projectRoot: string,\n options: AuditOptions = {},\n): Promise<number> {\n const threshold: ImpactLevel = options.threshold ?? 'serious';\n\n const deps = await loadDeps();\n if (!deps.ok) {\n console.error(\n `\\x1b[31m[tessera a11y]\\x1b[0m Tier 2 needs Playwright + axe-core, which aren't installed.\\n` +\n ` Install them to run the runtime audit:\\n` +\n ` npm i -D playwright @axe-core/playwright\\n` +\n ` npx playwright install chromium`,\n );\n return 1;\n }\n const { chromium, AxeBuilder } = deps.deps;\n\n const read = readCourseConfig(projectRoot);\n const settings = normalizeA11y(read.ok ? read.config.a11y : undefined);\n const tags = axeTags(settings.standard);\n const disableRules = axeIgnoreRules(settings.ignore);\n\n const manifest = generateManifest(resolve(projectRoot, 'pages'));\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const vite = (await import('vite')) as any;\n\n // A throwaway web build, kept out of dist/ so a real LMS export is untouched.\n const auditDist = resolve(projectRoot, 'node_modules', '.tessera-a11y');\n const distHtml = resolve(auditDist, 'index.html');\n\n const prevEnv = process.env[AUDIT_ENV_FLAG];\n process.env[AUDIT_ENV_FLAG] = '1';\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let server: any;\n try {\n if (options.rebuild || !existsSync(distHtml)) {\n console.log('[tessera a11y] Building course…');\n await vite.build({\n root: projectRoot,\n build: { outDir: auditDist, emptyOutDir: true },\n logLevel: 'warn',\n });\n }\n\n server = await vite.preview({\n root: projectRoot,\n build: { outDir: auditDist },\n preview: { port: 0, host: '127.0.0.1' },\n logLevel: 'warn',\n });\n const baseUrl: string | undefined = server.resolvedUrls?.local?.[0];\n if (!baseUrl) {\n console.error('[tessera a11y] Could not determine preview server URL.');\n return 1;\n }\n\n const browser = await chromium.launch();\n const pages: PageAuditResult[] = [];\n try {\n // axe-core/playwright requires a page from an explicit context.\n const context = await browser.newContext();\n const page = await context.newPage();\n // ?__tessera_audit unlocks navigation so quiz-gated pages can be scanned.\n const auditUrl = new URL(baseUrl);\n auditUrl.searchParams.set('__tessera_audit', '1');\n await page.goto(auditUrl.href, { waitUntil: 'networkidle' });\n await page.waitForSelector('#tessera-app', { timeout: 20_000 });\n\n const scan = async (): Promise<AxeViolation[]> => {\n const builder = new AxeBuilder({ page }).withTags(tags);\n if (disableRules.length > 0) builder.disableRules(disableRules);\n const out = await builder.analyze();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return out.violations.map((v: any) => ({\n id: v.id,\n impact: v.impact ?? null,\n help: v.help,\n helpUrl: v.helpUrl,\n nodes: v.nodes.length,\n }));\n };\n\n const navCount = await page.locator('button.tessera-nav-page').count();\n if (navCount === 0) {\n // No sidebar (custom chrome) — audit whatever is rendered at the entry.\n pages.push({\n index: 0,\n title: manifest.pages[0]?.title ?? '(entry)',\n violations: await scan(),\n });\n } else {\n for (let i = 0; i < navCount; i++) {\n const btn = page.locator('button.tessera-nav-page').nth(i);\n const title = (await btn.textContent())?.trim() || `Page ${i + 1}`;\n await btn.click();\n await page.waitForFunction(\n (idx: number) =>\n document\n .querySelectorAll('button.tessera-nav-page')\n [idx]?.getAttribute('aria-current') === 'page',\n i,\n { timeout: 20_000 },\n );\n await page.waitForLoadState('networkidle');\n pages.push({ index: i, title, violations: await scan() });\n }\n }\n } finally {\n await browser.close();\n }\n\n const thresholdRank = IMPACT_RANK[threshold];\n let totalViolations = 0;\n let failingViolations = 0;\n for (const p of pages) {\n for (const v of p.violations) {\n totalViolations++;\n if (isFailing(v, thresholdRank)) failingViolations++;\n }\n }\n\n const report: AuditReport = {\n standard: settings.standard,\n threshold,\n pages,\n totalViolations,\n failingViolations,\n passed: failingViolations === 0,\n };\n const reportPath = resolve(projectRoot, 'a11y-report.json');\n writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8');\n\n printSummary(report, reportPath);\n return report.passed ? 0 : 1;\n } catch (err) {\n console.error(\n `\\x1b[31m[tessera a11y]\\x1b[0m Audit could not complete: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n return 1;\n } finally {\n server?.httpServer?.close?.();\n if (prevEnv === undefined) delete process.env[AUDIT_ENV_FLAG];\n else process.env[AUDIT_ENV_FLAG] = prevEnv;\n }\n}\n\nfunction printSummary(report: AuditReport, reportPath: string): void {\n const thresholdRank = IMPACT_RANK[report.threshold];\n for (const p of report.pages) {\n if (p.violations.length === 0) {\n console.log(`\\x1b[32m ✓\\x1b[0m ${p.title}`);\n continue;\n }\n const failing = p.violations.some((v) => isFailing(v, thresholdRank));\n const mark = failing ? '\\x1b[31m ✗\\x1b[0m' : '\\x1b[33m ⚠\\x1b[0m';\n console.log(`${mark} ${p.title}`);\n for (const v of p.violations) {\n console.log(\n ` [${v.impact ?? 'n/a'}] ${v.id} — ${v.help} (${v.nodes} node${v.nodes === 1 ? '' : 's'})`,\n );\n }\n }\n console.log(`\\n[tessera a11y] Report written to ${reportPath}`);\n if (report.passed) {\n console.log(\n `\\x1b[32m[tessera a11y] Passed\\x1b[0m — ${report.totalViolations} total finding(s), none at/above \"${report.threshold}\".`,\n );\n } else {\n console.log(\n `\\x1b[31m[tessera a11y] Failed\\x1b[0m — ${report.failingViolations} finding(s) at/above \"${report.threshold}\" (of ${report.totalViolations} total).`,\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as runAudit } from "../audit-BBJpQGqb.js";
|
|
3
|
+
//#region src/plugin/a11y-cli.ts
|
|
4
|
+
const VALID_THRESHOLDS = [
|
|
5
|
+
"minor",
|
|
6
|
+
"moderate",
|
|
7
|
+
"serious",
|
|
8
|
+
"critical"
|
|
9
|
+
];
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
let threshold;
|
|
12
|
+
let rebuild = false;
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
const arg = args[i];
|
|
15
|
+
if (arg === "--threshold") {
|
|
16
|
+
const value = args[++i];
|
|
17
|
+
if (!VALID_THRESHOLDS.includes(value)) {
|
|
18
|
+
console.error(`[tessera a11y] --threshold must be one of: ${VALID_THRESHOLDS.join(", ")}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
threshold = value;
|
|
22
|
+
} else if (arg === "--build") rebuild = true;
|
|
23
|
+
else {
|
|
24
|
+
console.error(`[tessera a11y] Unknown argument: ${arg}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const code = await runAudit(process.cwd(), {
|
|
29
|
+
threshold,
|
|
30
|
+
rebuild
|
|
31
|
+
});
|
|
32
|
+
process.exit(code);
|
|
33
|
+
//#endregion
|
|
34
|
+
export {};
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=a11y-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a11y-cli.js","names":[],"sources":["../../src/plugin/a11y-cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { runAudit, type ImpactLevel } from './a11y/audit.js';\n\nconst VALID_THRESHOLDS: ImpactLevel[] = [\n 'minor',\n 'moderate',\n 'serious',\n 'critical',\n];\n\nconst args = process.argv.slice(2);\nlet threshold: ImpactLevel | undefined;\nlet rebuild = false;\n\nfor (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg === '--threshold') {\n const value = args[++i] as ImpactLevel;\n if (!VALID_THRESHOLDS.includes(value)) {\n console.error(\n `[tessera a11y] --threshold must be one of: ${VALID_THRESHOLDS.join(', ')}`,\n );\n process.exit(1);\n }\n threshold = value;\n } else if (arg === '--build') {\n rebuild = true;\n } else {\n console.error(`[tessera a11y] Unknown argument: ${arg}`);\n process.exit(1);\n }\n}\n\nconst code = await runAudit(process.cwd(), { threshold, rebuild });\nprocess.exit(code);\n"],"mappings":";;;AAGA,MAAM,mBAAkC;CACtC;CACA;CACA;CACA;CACD;AAED,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE;AAClC,IAAI;AACJ,IAAI,UAAU;AAEd,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;CACpC,MAAM,MAAM,KAAK;CACjB,IAAI,QAAQ,eAAe;EACzB,MAAM,QAAQ,KAAK,EAAE;EACrB,IAAI,CAAC,iBAAiB,SAAS,MAAM,EAAE;GACrC,QAAQ,MACN,8CAA8C,iBAAiB,KAAK,KAAK,GAC1E;GACD,QAAQ,KAAK,EAAE;;EAEjB,YAAY;QACP,IAAI,QAAQ,WACjB,UAAU;MACL;EACL,QAAQ,MAAM,oCAAoC,MAAM;EACxD,QAAQ,KAAK,EAAE;;;AAInB,MAAM,OAAO,MAAM,SAAS,QAAQ,KAAK,EAAE;CAAE;CAAW;CAAS,CAAC;AAClE,QAAQ,KAAK,KAAK"}
|
package/dist/plugin/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { a as validateProject, i as reportValidationIssues } from "../validation-B-xTvM9B.js";
|
|
3
3
|
//#region src/plugin/cli.ts
|
|
4
4
|
const { errors, warnings } = validateProject(process.cwd());
|
|
5
5
|
reportValidationIssues({
|
|
@@ -13,6 +13,7 @@ if (errors.length > 0) {
|
|
|
13
13
|
}
|
|
14
14
|
if (warnings.length > 0) console.log(`\n\x1b[33mValidation passed with ${warnings.length} warning(s).\x1b[0m`);
|
|
15
15
|
else console.log("\x1B[32m[tessera]\x1B[0m Validation passed — no issues found.");
|
|
16
|
+
console.log("\x1B[2m[tessera] Static checks only. For a full runtime accessibility audit, run: npm run accessibility-check\x1B[0m");
|
|
16
17
|
process.exit(0);
|
|
17
18
|
//#endregion
|
|
18
19
|
export {};
|
package/dist/plugin/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../../src/plugin/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { validateProject, reportValidationIssues } from './validation.js';\n\nconst projectRoot = process.cwd();\nconst { errors, warnings } = validateProject(projectRoot);\n\nreportValidationIssues({ errors, warnings });\n\nif (errors.length > 0) {\n const summary =\n `Validation failed with ${errors.length} error(s)` +\n (warnings.length > 0 ? ` and ${warnings.length} warning(s)` : '') +\n '.';\n console.error(`\\n\\x1b[31m${summary}\\x1b[0m`);\n process.exit(1);\n}\n\nif (warnings.length > 0) {\n console.log(\n `\\n\\x1b[33mValidation passed with ${warnings.length} warning(s).\\x1b[0m
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../../src/plugin/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { validateProject, reportValidationIssues } from './validation.js';\n\nconst projectRoot = process.cwd();\nconst { errors, warnings } = validateProject(projectRoot);\n\nreportValidationIssues({ errors, warnings });\n\nif (errors.length > 0) {\n const summary =\n `Validation failed with ${errors.length} error(s)` +\n (warnings.length > 0 ? ` and ${warnings.length} warning(s)` : '') +\n '.';\n console.error(`\\n\\x1b[31m${summary}\\x1b[0m`);\n process.exit(1);\n}\n\nif (warnings.length > 0) {\n console.log(\n `\\n\\x1b[33mValidation passed with ${warnings.length} warning(s).\\x1b[0m`,\n );\n} else {\n console.log('\\x1b[32m[tessera]\\x1b[0m Validation passed — no issues found.');\n}\nconsole.log(\n '\\x1b[2m[tessera] Static checks only. For a full runtime accessibility audit, run: npm run accessibility-check\\x1b[0m',\n);\nprocess.exit(0);\n"],"mappings":";;;AAIA,MAAM,EAAE,QAAQ,aAAa,gBADT,QAAQ,KAC4B,CAAC;AAEzD,uBAAuB;CAAE;CAAQ;CAAU,CAAC;AAE5C,IAAI,OAAO,SAAS,GAAG;CACrB,MAAM,UACJ,0BAA0B,OAAO,OAAO,cACvC,SAAS,SAAS,IAAI,QAAQ,SAAS,OAAO,eAAe,MAC9D;CACF,QAAQ,MAAM,aAAa,QAAQ,SAAS;CAC5C,QAAQ,KAAK,EAAE;;AAGjB,IAAI,SAAS,SAAS,GACpB,QAAQ,IACN,oCAAoC,SAAS,OAAO,qBACrD;KAED,QAAQ,IAAI,gEAAgE;AAE9E,QAAQ,IACN,uHACD;AACD,QAAQ,KAAK,EAAE"}
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { Plugin } from "vite";
|
|
2
2
|
|
|
3
|
+
//#region src/plugin/a11y/audit.d.ts
|
|
4
|
+
interface AuditOptions {
|
|
5
|
+
/** Minimum violation impact that fails the run (CI gate). Default 'serious'. */
|
|
6
|
+
threshold?: ImpactLevel;
|
|
7
|
+
/** Force a fresh `vite build` even if dist/ exists. */
|
|
8
|
+
rebuild?: boolean;
|
|
9
|
+
}
|
|
10
|
+
type ImpactLevel = 'minor' | 'moderate' | 'serious' | 'critical';
|
|
11
|
+
/**
|
|
12
|
+
* Run the Tier-2 runtime accessibility audit against a built course. Builds (or
|
|
13
|
+
* reuses) dist/, serves it, drives Playwright + axe-core over each page, writes
|
|
14
|
+
* a11y-report.json, and returns a process exit code (0 pass, 1 fail/error).
|
|
15
|
+
*/
|
|
16
|
+
declare function runAudit(projectRoot: string, options?: AuditOptions): Promise<number>;
|
|
17
|
+
//#endregion
|
|
3
18
|
//#region src/plugin/index.d.ts
|
|
4
19
|
declare function tesseraPlugin(): (Plugin<any> | Plugin<any>[])[];
|
|
5
20
|
//#endregion
|
|
6
|
-
export { tesseraPlugin };
|
|
21
|
+
export { type AuditOptions, type ImpactLevel, runAudit, tesseraPlugin };
|
|
7
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/plugin/index.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/plugin/a11y/audit.ts","../../src/plugin/index.ts"],"mappings":";;;UAKiB,YAAA;;EAEf,SAAA,GAAY,WAAA;EAFe;EAI3B,OAAA;AAAA;AAAA,KAGU,WAAA;;;;;AA6GZ;iBAAsB,QAAA,CACpB,WAAA,UACA,OAAA,GAAS,YAAA,GACR,OAAA;;;iBCtCa,aAAA,CAAA,IAAa,MAAA,QAAA,MAAA"}
|
package/dist/plugin/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { i as
|
|
1
|
+
import { a as validateProject, i as reportValidationIssues, n as isPlausibleLanguageTag, o as generateManifest, r as normalizeA11y, s as readCourseConfig, t as isIgnored } from "../validation-B-xTvM9B.js";
|
|
2
|
+
import { n as runAudit, t as AUDIT_ENV_FLAG } from "../audit-BBJpQGqb.js";
|
|
2
3
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
5
6
|
import { cpSync, createWriteStream, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { createHash } from "node:crypto";
|
|
7
8
|
import { ZipArchive } from "archiver";
|
|
@@ -251,6 +252,9 @@ function tesseraQuizPlugin() {
|
|
|
251
252
|
}
|
|
252
253
|
//#endregion
|
|
253
254
|
//#region src/plugin/index.ts
|
|
255
|
+
function isAuditBuild() {
|
|
256
|
+
return process.env[AUDIT_ENV_FLAG] === "1";
|
|
257
|
+
}
|
|
254
258
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
255
259
|
function resolveRuntimeDir() {
|
|
256
260
|
return resolve(resolve(__dirname, "..", ".."), "src", "runtime");
|
|
@@ -258,13 +262,44 @@ function resolveRuntimeDir() {
|
|
|
258
262
|
function resolveStylesDir() {
|
|
259
263
|
return resolve(resolve(__dirname, "..", ".."), "styles");
|
|
260
264
|
}
|
|
265
|
+
function projectFileRel(filename, projectRoot) {
|
|
266
|
+
if (!filename || !projectRoot) return null;
|
|
267
|
+
if (filename.startsWith("\0") || filename.includes("virtual:") || filename.includes("node_modules")) return null;
|
|
268
|
+
const rel = relative(projectRoot, isAbsolute(filename) ? filename : resolve(projectRoot, filename));
|
|
269
|
+
if (rel.startsWith("..") || isAbsolute(rel) || rel.includes("node_modules")) return null;
|
|
270
|
+
return rel;
|
|
271
|
+
}
|
|
261
272
|
function tesseraPlugin() {
|
|
262
273
|
const manifestRef = {
|
|
263
274
|
current: null,
|
|
264
275
|
root: ""
|
|
265
276
|
};
|
|
277
|
+
const a11y = {
|
|
278
|
+
warnings: [],
|
|
279
|
+
projectRoot: "",
|
|
280
|
+
isBuild: false,
|
|
281
|
+
settings: normalizeA11y(void 0)
|
|
282
|
+
};
|
|
266
283
|
return [
|
|
267
|
-
svelte({
|
|
284
|
+
svelte({
|
|
285
|
+
compilerOptions: { css: "external" },
|
|
286
|
+
onwarn(warning, defaultHandler) {
|
|
287
|
+
if (warning.code?.startsWith("a11y")) {
|
|
288
|
+
const rel = projectFileRel(warning.filename, a11y.projectRoot);
|
|
289
|
+
if (rel !== null) {
|
|
290
|
+
const msg = `[${warning.code}] ${rel}: ${warning.message}`;
|
|
291
|
+
if (a11y.isBuild) a11y.warnings.push(msg);
|
|
292
|
+
else if (!a11y.settings.ignore.includes(warning.code)) reportValidationIssues({
|
|
293
|
+
errors: [],
|
|
294
|
+
warnings: [msg]
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
defaultHandler?.(warning);
|
|
300
|
+
}
|
|
301
|
+
}),
|
|
302
|
+
tesseraA11yCompilerPlugin(a11y),
|
|
268
303
|
tesseraValidationPlugin(),
|
|
269
304
|
tesseraEntryPlugin(),
|
|
270
305
|
tesseraConfigPlugin(),
|
|
@@ -287,16 +322,18 @@ function tesseraEntryPlugin() {
|
|
|
287
322
|
const stylesDir = resolveStylesDir();
|
|
288
323
|
const appSveltePath = resolve(runtimeDir, "App.svelte");
|
|
289
324
|
let projectRoot;
|
|
325
|
+
let outDir;
|
|
290
326
|
let isBuild = false;
|
|
291
327
|
return {
|
|
292
328
|
name: "tessera:entry",
|
|
293
329
|
enforce: "pre",
|
|
294
330
|
configResolved(config) {
|
|
295
331
|
projectRoot = config.root;
|
|
332
|
+
outDir = resolve(config.root, config.build.outDir);
|
|
296
333
|
isBuild = config.command === "build";
|
|
297
334
|
},
|
|
298
335
|
buildStart() {
|
|
299
|
-
if (isBuild) writeFileSync(resolve(projectRoot, "index.html"), generateIndexHtml(), "utf-8");
|
|
336
|
+
if (isBuild) writeFileSync(resolve(projectRoot, "index.html"), generateIndexHtml(readLanguage(projectRoot)), "utf-8");
|
|
300
337
|
},
|
|
301
338
|
closeBundle() {
|
|
302
339
|
if (isBuild) {
|
|
@@ -305,7 +342,7 @@ function tesseraEntryPlugin() {
|
|
|
305
342
|
unlinkSync(htmlPath);
|
|
306
343
|
} catch {}
|
|
307
344
|
const assetsDir = resolve(projectRoot, "assets");
|
|
308
|
-
const distAssetsDir = resolve(
|
|
345
|
+
const distAssetsDir = resolve(outDir, "assets");
|
|
309
346
|
if (existsSync(assetsDir)) {
|
|
310
347
|
mkdirSync(distAssetsDir, { recursive: true });
|
|
311
348
|
cpSync(assetsDir, distAssetsDir, { recursive: true });
|
|
@@ -316,7 +353,7 @@ function tesseraEntryPlugin() {
|
|
|
316
353
|
return () => {
|
|
317
354
|
server.middlewares.use(async (req, res, next) => {
|
|
318
355
|
if (req.url === "/" || req.url === "/index.html") {
|
|
319
|
-
const html = generateIndexHtml();
|
|
356
|
+
const html = generateIndexHtml(readLanguage(projectRoot));
|
|
320
357
|
const transformed = await server.transformIndexHtml(req.url, html);
|
|
321
358
|
res.setHeader("Content-Type", "text/html");
|
|
322
359
|
res.statusCode = 200;
|
|
@@ -338,9 +375,14 @@ function tesseraEntryPlugin() {
|
|
|
338
375
|
}
|
|
339
376
|
};
|
|
340
377
|
}
|
|
341
|
-
function
|
|
378
|
+
function readLanguage(projectRoot) {
|
|
379
|
+
const read = readCourseConfig(projectRoot);
|
|
380
|
+
const lang = read.ok ? read.config.language : void 0;
|
|
381
|
+
return isPlausibleLanguageTag(lang) ? lang : "en";
|
|
382
|
+
}
|
|
383
|
+
function generateIndexHtml(lang) {
|
|
342
384
|
return `<!DOCTYPE html>
|
|
343
|
-
<html lang="
|
|
385
|
+
<html lang="${lang}">
|
|
344
386
|
<head>
|
|
345
387
|
<meta charset="UTF-8" />
|
|
346
388
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
@@ -399,7 +441,7 @@ function tesseraConfigPlugin() {
|
|
|
399
441
|
return {
|
|
400
442
|
base: "./",
|
|
401
443
|
build: { assetsDir: "tessera" },
|
|
402
|
-
resolve: { alias: {
|
|
444
|
+
resolve: { alias: { $assets: resolve(config.root || process.cwd(), "assets") } },
|
|
403
445
|
optimizeDeps: { exclude: ["tessera-learn"] }
|
|
404
446
|
};
|
|
405
447
|
},
|
|
@@ -489,6 +531,36 @@ function tesseraValidationPlugin() {
|
|
|
489
531
|
}
|
|
490
532
|
};
|
|
491
533
|
}
|
|
534
|
+
function tesseraA11yCompilerPlugin(a11y) {
|
|
535
|
+
return {
|
|
536
|
+
name: "tessera:a11y-compiler",
|
|
537
|
+
enforce: "pre",
|
|
538
|
+
configResolved(config) {
|
|
539
|
+
a11y.projectRoot = config.root;
|
|
540
|
+
a11y.isBuild = config.command === "build";
|
|
541
|
+
const read = readCourseConfig(config.root);
|
|
542
|
+
a11y.settings = normalizeA11y(read.ok ? read.config.a11y : void 0);
|
|
543
|
+
},
|
|
544
|
+
buildEnd() {
|
|
545
|
+
if (!a11y.isBuild || a11y.warnings.length === 0) return;
|
|
546
|
+
const ignored = new Set(a11y.settings.ignore);
|
|
547
|
+
const warnings = a11y.warnings.filter((msg) => !isIgnored(msg, ignored));
|
|
548
|
+
a11y.warnings = [];
|
|
549
|
+
if (warnings.length === 0) return;
|
|
550
|
+
if (a11y.settings.level === "error") {
|
|
551
|
+
reportValidationIssues({
|
|
552
|
+
errors: warnings,
|
|
553
|
+
warnings: []
|
|
554
|
+
});
|
|
555
|
+
throw new Error(`Tessera: ${warnings.length} a11y issue(s) with a11y.level: 'error'. Fix the errors above to continue.`);
|
|
556
|
+
}
|
|
557
|
+
reportValidationIssues({
|
|
558
|
+
errors: [],
|
|
559
|
+
warnings
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
}
|
|
492
564
|
function runValidation(projectRoot) {
|
|
493
565
|
const result = validateProject(projectRoot);
|
|
494
566
|
reportValidationIssues(result);
|
|
@@ -506,6 +578,7 @@ function tesseraExportPlugin() {
|
|
|
506
578
|
},
|
|
507
579
|
async closeBundle() {
|
|
508
580
|
if (!isBuild) return;
|
|
581
|
+
if (isAuditBuild()) return;
|
|
509
582
|
const read = readCourseConfig(projectRoot);
|
|
510
583
|
if (!read.ok) {
|
|
511
584
|
if (read.reason === "missing") throw new Error("[tessera:export] course.config.js not found at closeBundle. The file must exist for the export step to run.");
|
|
@@ -588,6 +661,7 @@ function tesseraAdapterPlugin() {
|
|
|
588
661
|
let standard = "web";
|
|
589
662
|
const read = readCourseConfig(projectRoot);
|
|
590
663
|
if (read.ok && typeof read.config.export?.standard === "string") standard = read.config.export.standard;
|
|
664
|
+
if (isAuditBuild()) standard = "web";
|
|
591
665
|
switch (standard) {
|
|
592
666
|
case "scorm12": return `
|
|
593
667
|
import { SCORM12Adapter } from 'tessera-learn/runtime/adapters/scorm12.js';
|
|
@@ -647,6 +721,7 @@ function tesseraXAPISetupPlugin() {
|
|
|
647
721
|
load(id) {
|
|
648
722
|
if (id !== RESOLVED_XAPI_SETUP_ID) return null;
|
|
649
723
|
if (!isBuild) return `export { buildXAPIClient } from 'tessera-learn/runtime/xapi/setup.js';`;
|
|
724
|
+
if (isAuditBuild()) return `export async function buildXAPIClient() { return null; }`;
|
|
650
725
|
let standard = "web";
|
|
651
726
|
let hasXapi = false;
|
|
652
727
|
const read = readCourseConfig(projectRoot);
|
|
@@ -684,6 +759,6 @@ function tesseraFirstPagePreloadPlugin(manifestRef) {
|
|
|
684
759
|
};
|
|
685
760
|
}
|
|
686
761
|
//#endregion
|
|
687
|
-
export { tesseraPlugin };
|
|
762
|
+
export { runAudit, tesseraPlugin };
|
|
688
763
|
|
|
689
764
|
//# sourceMappingURL=index.js.map
|