tessera-learn 0.2.1 → 0.2.3

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.
Files changed (30) hide show
  1. package/AGENTS.md +280 -916
  2. package/README.md +3 -3
  3. package/dist/{audit-BNrvFHq_.js → audit--fSWIOgK.js} +156 -33
  4. package/dist/{audit-BNrvFHq_.js.map → audit--fSWIOgK.js.map} +1 -1
  5. package/dist/{build-commands-BWnATKat.js → build-commands-Qyrlsp3n.js} +2 -2
  6. package/dist/{build-commands-BWnATKat.js.map → build-commands-Qyrlsp3n.js.map} +1 -1
  7. package/dist/{inline-config-Dudu5r8w.js → inline-config-DqAKsCNl.js} +2 -2
  8. package/dist/{inline-config-Dudu5r8w.js.map → inline-config-DqAKsCNl.js.map} +1 -1
  9. package/dist/plugin/cli.d.ts.map +1 -1
  10. package/dist/plugin/cli.js +33 -18
  11. package/dist/plugin/cli.js.map +1 -1
  12. package/dist/plugin/index.d.ts +0 -2
  13. package/dist/plugin/index.d.ts.map +1 -1
  14. package/dist/plugin/index.js +2 -2
  15. package/dist/{plugin-diNZaDJK.js → plugin-B-aiL9-V.js} +2 -2
  16. package/dist/{plugin-diNZaDJK.js.map → plugin-B-aiL9-V.js.map} +1 -1
  17. package/package.json +11 -8
  18. package/src/components/FillInTheBlank.svelte +3 -27
  19. package/src/components/Matching.svelte +4 -26
  20. package/src/components/MultipleChoice.svelte +8 -27
  21. package/src/components/QuestionShell.svelte +35 -0
  22. package/src/components/Sorting.svelte +4 -26
  23. package/src/plugin/a11y/audit.ts +239 -39
  24. package/src/plugin/a11y-cli.ts +1 -4
  25. package/src/plugin/cli.ts +2 -3
  26. package/src/plugin/course-root.ts +37 -9
  27. package/src/plugin/validate-cli.ts +10 -4
  28. package/src/runtime/adapters/cmi5.ts +5 -14
  29. package/src/runtime/adapters/index.ts +41 -38
  30. package/src/runtime/adapters/scorm12.ts +1 -1
package/README.md CHANGED
@@ -12,11 +12,11 @@ You probably don't want to install this package directly. Use the scaffolder:
12
12
  pnpm create tessera@latest my-course
13
13
  ```
14
14
 
15
- That creates a workspace with Tessera wired up, a seed course, and the authoring guide (`AGENTS.md`) at the workspace root. Add more courses with `pnpm tessera new <name>`.
15
+ That creates a workspace with Tessera wired up, a seed course, and a root `AGENTS.md` that points to the authoring guide. Add more courses with `pnpm tessera new <name>`.
16
16
 
17
17
  ## What's included
18
18
 
19
- - **Hooks** (`tessera-learn`): `useQuestion`, `useQuiz`, `useNavigation`, `useProgress`, `usePersistence`, `useXAPI`.
19
+ - **Hooks** (`tessera-learn`): `useQuestion`, `useQuiz`, `useNavigation`, `useProgress`, `useCompletion`, `usePersistence`, `useXAPI`.
20
20
  - **Vite plugin** (`tessera-learn/plugin`): `tesseraPlugin()` — wires page/layout discovery, the LMS adapter, the `$shared` alias, and the export pipeline. The `tessera` CLI (`new`/`dev`/`export`/`validate`/`a11y`/`check`) runs Vite with this plugin for you, so scaffolded workspaces need no `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`.
@@ -26,7 +26,7 @@ See `AGENTS.md` for usage, signatures, and authoring conventions.
26
26
 
27
27
  ## Documentation
28
28
 
29
- The full authoring guide ships with this package at `node_modules/tessera-learn/AGENTS.md`, at the root of any scaffolded project, and on [GitHub](https://github.com/redmodd/tessera/blob/main/AGENTS.md).
29
+ The full authoring guide ships with this package at `node_modules/tessera-learn/AGENTS.md` scaffolded projects get a small root `AGENTS.md` that points to it — and is on [GitHub](https://github.com/redmodd/tessera/blob/main/packages/tessera-learn/AGENTS.md).
30
30
 
31
31
  ## License
32
32
 
@@ -1,9 +1,11 @@
1
+ import { createRequire } from "node:module";
1
2
  import { basename, dirname, extname, relative, resolve } from "node:path";
2
3
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
4
  import JSON5 from "json5";
4
5
  import { Parser } from "acorn";
5
6
  import { tsPlugin } from "@sveltejs/acorn-typescript";
6
7
  import { parse } from "svelte/compiler";
8
+ import { spawn } from "node:child_process";
7
9
  //#region src/plugin/ast.ts
8
10
  const rootCache = /* @__PURE__ */ new Map();
9
11
  /** Drop every cached root. Call at the start of a run to scope the cache. */
@@ -1385,9 +1387,132 @@ function axeTags(standard) {
1385
1387
  function axeIgnoreRules(ignore) {
1386
1388
  return ignore.filter((id) => !id.startsWith("tessera/") && !id.startsWith("a11y_"));
1387
1389
  }
1390
+ const MAX_HTML_LENGTH = 200;
1391
+ const MAX_ELEMENTS_SHOWN = 5;
1392
+ function mapNodeDetail(node) {
1393
+ const target = Array.isArray(node?.target) ? node.target.flat(Infinity).join(" ") : String(node?.target ?? "");
1394
+ const html = String(node?.html ?? "");
1395
+ return {
1396
+ target,
1397
+ html: html.length > MAX_HTML_LENGTH ? `${html.slice(0, MAX_HTML_LENGTH - 1)}…` : html,
1398
+ summary: String(node?.failureSummary ?? "").replace(/\s*\n\s*/g, " ").trim()
1399
+ };
1400
+ }
1401
+ function mapViolation(v) {
1402
+ return {
1403
+ id: v.id,
1404
+ impact: v.impact ?? null,
1405
+ help: v.help,
1406
+ helpUrl: v.helpUrl,
1407
+ nodes: v.nodes.length,
1408
+ elements: v.nodes.map(mapNodeDetail)
1409
+ };
1410
+ }
1388
1411
  function isMissingBrowserError(message) {
1389
1412
  return /Executable doesn't exist|playwright install/i.test(message);
1390
1413
  }
1414
+ function isMissingDepsError(message) {
1415
+ return /Host system is missing dependencies|missing dependencies to run browser/i.test(message);
1416
+ }
1417
+ const INSTALL_CHROMIUM = "pnpm exec playwright install chromium";
1418
+ const PLAYWRIGHT_SPECS = ["playwright", "@playwright/test"];
1419
+ function reportManualInstall(lead) {
1420
+ console.error(`\x1b[31m[tessera a11y]\x1b[0m ${lead}\n Install it once:\n ${INSTALL_CHROMIUM}`);
1421
+ }
1422
+ function resolvePlaywrightBin() {
1423
+ const require = createRequire(import.meta.url);
1424
+ for (const spec of PLAYWRIGHT_SPECS) try {
1425
+ const pkgPath = require.resolve(`${spec}/package.json`);
1426
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1427
+ const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.playwright;
1428
+ if (!binRel) continue;
1429
+ return {
1430
+ command: process.execPath,
1431
+ args: [
1432
+ resolve(dirname(pkgPath), binRel),
1433
+ "install",
1434
+ "chromium"
1435
+ ]
1436
+ };
1437
+ } catch {
1438
+ continue;
1439
+ }
1440
+ }
1441
+ const INSTALL_TIMEOUT_MS = 10 * 6e4;
1442
+ async function installChromium(workspaceRoot, spawnFn = spawn, timeoutMs = INSTALL_TIMEOUT_MS) {
1443
+ const bin = resolvePlaywrightBin();
1444
+ if (!bin) {
1445
+ console.error(`\x1b[31m[tessera a11y]\x1b[0m Could not locate the Playwright CLI to install Chromium.`);
1446
+ return false;
1447
+ }
1448
+ return new Promise((resolvePromise) => {
1449
+ const child = spawnFn(bin.command, bin.args, {
1450
+ stdio: "inherit",
1451
+ cwd: workspaceRoot
1452
+ });
1453
+ let settled = false;
1454
+ const finish = (ok) => {
1455
+ if (settled) return;
1456
+ settled = true;
1457
+ clearTimeout(timer);
1458
+ resolvePromise(ok);
1459
+ };
1460
+ const timer = setTimeout(() => {
1461
+ console.error(`\x1b[31m[tessera a11y]\x1b[0m Chromium install timed out after ${Math.round(timeoutMs / 6e4)} min; aborting.`);
1462
+ child.kill?.("SIGKILL");
1463
+ finish(false);
1464
+ }, timeoutMs);
1465
+ timer.unref?.();
1466
+ child.on("error", (err) => {
1467
+ console.error(`\x1b[31m[tessera a11y]\x1b[0m Failed to start the Chromium install: ${err.message}`);
1468
+ finish(false);
1469
+ });
1470
+ child.on("exit", (code) => finish(code === 0));
1471
+ });
1472
+ }
1473
+ function reportLaunchFailure(message, isLinux) {
1474
+ console.error(`\x1b[31m[tessera a11y]\x1b[0m Chromium is installed but failed to launch.\n` + (isLinux ? ` Install system dependencies:\n pnpm exec playwright install --with-deps chromium\n` : ``) + ` Original error: ${message}`);
1475
+ }
1476
+ async function launchWithInstall({ launch, install, isLinux = process.platform === "linux" }) {
1477
+ try {
1478
+ return {
1479
+ ok: true,
1480
+ browser: await launch()
1481
+ };
1482
+ } catch (err) {
1483
+ const message = err instanceof Error ? err.message : String(err);
1484
+ if (isMissingDepsError(message)) {
1485
+ reportLaunchFailure(message, isLinux);
1486
+ return {
1487
+ ok: false,
1488
+ code: 1
1489
+ };
1490
+ }
1491
+ if (!isMissingBrowserError(message)) throw err;
1492
+ console.log("[tessera a11y] Chromium isn't installed for Playwright. Installing it once now…");
1493
+ if (!await install()) {
1494
+ reportManualInstall("Chromium isn't installed for Playwright.");
1495
+ return {
1496
+ ok: false,
1497
+ code: 1
1498
+ };
1499
+ }
1500
+ try {
1501
+ return {
1502
+ ok: true,
1503
+ browser: await launch()
1504
+ };
1505
+ } catch (retryErr) {
1506
+ const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
1507
+ if (isMissingBrowserError(retryMessage) && !isMissingDepsError(retryMessage)) reportManualInstall("Chromium still isn't installed after the install step.");
1508
+ else reportLaunchFailure(retryMessage, isLinux);
1509
+ return {
1510
+ ok: false,
1511
+ code: 1
1512
+ };
1513
+ }
1514
+ }
1515
+ }
1391
1516
  function isFailing(v, thresholdRank) {
1392
1517
  return !v.impact || IMPACT_RANK[v.impact] >= thresholdRank;
1393
1518
  }
@@ -1396,7 +1521,7 @@ async function tryImport(specifier) {
1396
1521
  }
1397
1522
  async function loadDeps() {
1398
1523
  let chromium;
1399
- for (const spec of ["playwright", "@playwright/test"]) try {
1524
+ for (const spec of PLAYWRIGHT_SPECS) try {
1400
1525
  const mod = await tryImport(spec);
1401
1526
  if (mod.chromium) {
1402
1527
  chromium = mod.chromium;
@@ -1436,7 +1561,10 @@ async function runAudit(projectRoot, workspaceRoot, options = {}) {
1436
1561
  const threshold = options.threshold ?? "serious";
1437
1562
  const deps = await loadDeps();
1438
1563
  if (!deps.ok) {
1439
- 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 pnpm add -D playwright @axe-core/playwright\n pnpm exec playwright install chromium");
1564
+ console.error(`[tessera a11y] Tier 2 needs Playwright + axe-core, which aren't installed.
1565
+ Install them to run the runtime audit:
1566
+ pnpm add -D playwright @axe-core/playwright
1567
+ ${INSTALL_CHROMIUM}`);
1440
1568
  return 1;
1441
1569
  }
1442
1570
  const { chromium, AxeBuilder } = deps.deps;
@@ -1446,27 +1574,24 @@ async function runAudit(projectRoot, workspaceRoot, options = {}) {
1446
1574
  const disableRules = axeIgnoreRules(settings.ignore);
1447
1575
  const manifest = generateManifest(resolve(projectRoot, "pages"));
1448
1576
  const vite = await import("vite");
1449
- const { resolveTesseraConfig } = await import("./inline-config-Dudu5r8w.js");
1577
+ const { resolveTesseraConfig } = await import("./inline-config-DqAKsCNl.js");
1450
1578
  const auditBaseConfig = await resolveTesseraConfig(projectRoot, workspaceRoot, {
1451
1579
  command: "build",
1452
1580
  mode: "production"
1453
1581
  });
1454
1582
  const auditDist = resolve(projectRoot, "node_modules", ".tessera-a11y");
1455
- const distHtml = resolve(auditDist, "index.html");
1456
1583
  const prevEnv = process.env[AUDIT_ENV_FLAG];
1457
1584
  process.env[AUDIT_ENV_FLAG] = "1";
1458
1585
  let server;
1459
1586
  try {
1460
- if (options.rebuild || !existsSync(distHtml)) {
1461
- console.log("[tessera a11y] Building course…");
1462
- await vite.build(vite.mergeConfig(auditBaseConfig, {
1463
- build: {
1464
- outDir: auditDist,
1465
- emptyOutDir: true
1466
- },
1467
- logLevel: "warn"
1468
- }));
1469
- }
1587
+ console.log("[tessera a11y] Building course…");
1588
+ await vite.build(vite.mergeConfig(auditBaseConfig, {
1589
+ build: {
1590
+ outDir: auditDist,
1591
+ emptyOutDir: true
1592
+ },
1593
+ logLevel: "warn"
1594
+ }));
1470
1595
  server = await vite.preview({
1471
1596
  root: projectRoot,
1472
1597
  base: auditBaseConfig.base,
@@ -1482,16 +1607,12 @@ async function runAudit(projectRoot, workspaceRoot, options = {}) {
1482
1607
  console.error("[tessera a11y] Could not determine preview server URL.");
1483
1608
  return 1;
1484
1609
  }
1485
- let browser;
1486
- try {
1487
- browser = await chromium.launch();
1488
- } catch (err) {
1489
- if (isMissingBrowserError(err instanceof Error ? err.message : String(err))) {
1490
- console.error("\x1B[31m[tessera a11y]\x1B[0m Chromium isn't installed for Playwright.\n Install it once:\n pnpm exec playwright install chromium");
1491
- return 1;
1492
- }
1493
- throw err;
1494
- }
1610
+ const launched = await launchWithInstall({
1611
+ launch: () => chromium.launch(),
1612
+ install: () => installChromium(workspaceRoot)
1613
+ });
1614
+ if (!launched.ok) return launched.code;
1615
+ const browser = launched.browser;
1495
1616
  const pages = [];
1496
1617
  try {
1497
1618
  const page = await (await browser.newContext()).newPage();
@@ -1502,13 +1623,7 @@ async function runAudit(projectRoot, workspaceRoot, options = {}) {
1502
1623
  const scan = async () => {
1503
1624
  const builder = new AxeBuilder({ page }).withTags(tags);
1504
1625
  if (disableRules.length > 0) builder.disableRules(disableRules);
1505
- return (await builder.analyze()).violations.map((v) => ({
1506
- id: v.id,
1507
- impact: v.impact ?? null,
1508
- help: v.help,
1509
- helpUrl: v.helpUrl,
1510
- nodes: v.nodes.length
1511
- }));
1626
+ return (await builder.analyze()).violations.map(mapViolation);
1512
1627
  };
1513
1628
  const recordPage = async (index, title) => {
1514
1629
  const loadFailed = await page.evaluate(() => document.getElementById("tessera-app")?.dataset.tesseraPageError === "true");
@@ -1585,7 +1700,15 @@ function printSummary(report, reportPath) {
1585
1700
  }
1586
1701
  const mark = p.violations.some((v) => isFailing(v, thresholdRank)) ? "\x1B[31m ✗\x1B[0m" : "\x1B[33m ⚠\x1B[0m";
1587
1702
  console.log(`${mark} ${p.title}`);
1588
- for (const v of p.violations) console.log(` [${v.impact ?? "n/a"}] ${v.id} — ${v.help} (${v.nodes} node${v.nodes === 1 ? "" : "s"})`);
1703
+ for (const v of p.violations) {
1704
+ console.log(` [${v.impact ?? "n/a"}] ${v.id} — ${v.help} (${v.nodes} node${v.nodes === 1 ? "" : "s"})`);
1705
+ for (const el of v.elements.slice(0, MAX_ELEMENTS_SHOWN)) {
1706
+ console.log(`\x1b[90m → ${el.target || "(unknown element)"}\x1b[0m`);
1707
+ if (el.summary) console.log(`\x1b[90m ${el.summary}\x1b[0m`);
1708
+ }
1709
+ const hidden = v.elements.length - MAX_ELEMENTS_SHOWN;
1710
+ if (hidden > 0) console.log(`\x1b[90m … and ${hidden} more — see a11y-report.json\x1b[0m`);
1711
+ }
1589
1712
  }
1590
1713
  console.log(`\n[tessera a11y] Report written to ${reportPath}`);
1591
1714
  if (report.pagesAudited < report.totalPages) console.log(`\x1b[33m[tessera a11y] Covered ${report.pagesAudited} of ${report.totalPages} page(s)\x1b[0m — reduced scope, the rest were not audited.`);
@@ -1604,4 +1727,4 @@ function printSummary(report, reportPath) {
1604
1727
  //#endregion
1605
1728
  export { isPlausibleLanguageTag as a, validateProject as c, isIgnored as i, generateManifest as l, runAudit as n, normalizeA11y as o, resolvePackageRoot as r, reportValidationIssues as s, AUDIT_ENV_FLAG as t, readCourseConfig as u };
1606
1729
 
1607
- //# sourceMappingURL=audit-BNrvFHq_.js.map
1730
+ //# sourceMappingURL=audit--fSWIOgK.js.map