tessera-learn 0.2.1 → 0.2.2

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 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,7 +1574,7 @@ 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-eHjv9XuA.js");
1450
1578
  const auditBaseConfig = await resolveTesseraConfig(projectRoot, workspaceRoot, {
1451
1579
  command: "build",
1452
1580
  mode: "production"
@@ -1482,16 +1610,12 @@ async function runAudit(projectRoot, workspaceRoot, options = {}) {
1482
1610
  console.error("[tessera a11y] Could not determine preview server URL.");
1483
1611
  return 1;
1484
1612
  }
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
- }
1613
+ const launched = await launchWithInstall({
1614
+ launch: () => chromium.launch(),
1615
+ install: () => installChromium(workspaceRoot)
1616
+ });
1617
+ if (!launched.ok) return launched.code;
1618
+ const browser = launched.browser;
1495
1619
  const pages = [];
1496
1620
  try {
1497
1621
  const page = await (await browser.newContext()).newPage();
@@ -1502,13 +1626,7 @@ async function runAudit(projectRoot, workspaceRoot, options = {}) {
1502
1626
  const scan = async () => {
1503
1627
  const builder = new AxeBuilder({ page }).withTags(tags);
1504
1628
  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
- }));
1629
+ return (await builder.analyze()).violations.map(mapViolation);
1512
1630
  };
1513
1631
  const recordPage = async (index, title) => {
1514
1632
  const loadFailed = await page.evaluate(() => document.getElementById("tessera-app")?.dataset.tesseraPageError === "true");
@@ -1585,7 +1703,15 @@ function printSummary(report, reportPath) {
1585
1703
  }
1586
1704
  const mark = p.violations.some((v) => isFailing(v, thresholdRank)) ? "\x1B[31m ✗\x1B[0m" : "\x1B[33m ⚠\x1B[0m";
1587
1705
  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"})`);
1706
+ for (const v of p.violations) {
1707
+ console.log(` [${v.impact ?? "n/a"}] ${v.id} — ${v.help} (${v.nodes} node${v.nodes === 1 ? "" : "s"})`);
1708
+ for (const el of v.elements.slice(0, MAX_ELEMENTS_SHOWN)) {
1709
+ console.log(`\x1b[90m → ${el.target || "(unknown element)"}\x1b[0m`);
1710
+ if (el.summary) console.log(`\x1b[90m ${el.summary}\x1b[0m`);
1711
+ }
1712
+ const hidden = v.elements.length - MAX_ELEMENTS_SHOWN;
1713
+ if (hidden > 0) console.log(`\x1b[90m … and ${hidden} more — see a11y-report.json\x1b[0m`);
1714
+ }
1589
1715
  }
1590
1716
  console.log(`\n[tessera a11y] Report written to ${reportPath}`);
1591
1717
  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 +1730,4 @@ function printSummary(report, reportPath) {
1604
1730
  //#endregion
1605
1731
  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
1732
 
1607
- //# sourceMappingURL=audit-BNrvFHq_.js.map
1733
+ //# sourceMappingURL=audit-B9VHgVjk.js.map