tessera-learn 0.0.10 → 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.
Files changed (79) hide show
  1. package/README.md +1 -0
  2. package/dist/audit-BBJpQGqb.js +204 -0
  3. package/dist/audit-BBJpQGqb.js.map +1 -0
  4. package/dist/plugin/a11y-cli.d.ts +1 -0
  5. package/dist/plugin/a11y-cli.js +36 -0
  6. package/dist/plugin/a11y-cli.js.map +1 -0
  7. package/dist/plugin/cli.js +6 -3
  8. package/dist/plugin/cli.js.map +1 -1
  9. package/dist/plugin/index.d.ts +16 -1
  10. package/dist/plugin/index.d.ts.map +1 -1
  11. package/dist/plugin/index.js +171 -140
  12. package/dist/plugin/index.js.map +1 -1
  13. package/dist/{validation-BxWAMMnJ.js → validation-B-xTvM9B.js} +417 -81
  14. package/dist/validation-B-xTvM9B.js.map +1 -0
  15. package/package.json +17 -2
  16. package/src/components/Accordion.svelte +3 -1
  17. package/src/components/AccordionItem.svelte +1 -5
  18. package/src/components/Audio.svelte +22 -5
  19. package/src/components/Callout.svelte +5 -1
  20. package/src/components/Carousel.svelte +24 -8
  21. package/src/components/DefaultLayout.svelte +41 -12
  22. package/src/components/FillInTheBlank.svelte +75 -103
  23. package/src/components/Image.svelte +14 -10
  24. package/src/components/LockedBanner.svelte +5 -5
  25. package/src/components/Matching.svelte +48 -19
  26. package/src/components/MediaTracks.svelte +21 -0
  27. package/src/components/MultipleChoice.svelte +81 -102
  28. package/src/components/Quiz.svelte +63 -21
  29. package/src/components/ResultIcon.svelte +20 -4
  30. package/src/components/RevealModal.svelte +25 -22
  31. package/src/components/Sorting.svelte +61 -26
  32. package/src/components/Transcript.svelte +37 -0
  33. package/src/components/Video.svelte +25 -20
  34. package/src/components/util.ts +4 -1
  35. package/src/components/video-embed.ts +25 -0
  36. package/src/index.ts +2 -7
  37. package/src/plugin/a11y/audit.ts +299 -0
  38. package/src/plugin/a11y/contrast.ts +67 -0
  39. package/src/plugin/a11y-cli.ts +35 -0
  40. package/src/plugin/cli.ts +6 -8
  41. package/src/plugin/export.ts +60 -50
  42. package/src/plugin/index.ts +244 -101
  43. package/src/plugin/layout.ts +6 -51
  44. package/src/plugin/manifest.ts +90 -24
  45. package/src/plugin/override-plugin.ts +68 -0
  46. package/src/plugin/quiz.ts +9 -54
  47. package/src/plugin/validation.ts +768 -183
  48. package/src/runtime/App.svelte +128 -64
  49. package/src/runtime/LoadingBar.svelte +12 -3
  50. package/src/runtime/Sidebar.svelte +24 -8
  51. package/src/runtime/access.ts +15 -3
  52. package/src/runtime/adapters/cmi5.ts +68 -116
  53. package/src/runtime/adapters/format.ts +67 -0
  54. package/src/runtime/adapters/index.ts +45 -34
  55. package/src/runtime/adapters/retry.ts +25 -84
  56. package/src/runtime/adapters/scorm-base.ts +19 -15
  57. package/src/runtime/adapters/scorm12.ts +8 -9
  58. package/src/runtime/adapters/scorm2004.ts +22 -30
  59. package/src/runtime/adapters/web.ts +1 -1
  60. package/src/runtime/hooks.svelte.ts +152 -328
  61. package/src/runtime/interaction-format.ts +30 -12
  62. package/src/runtime/interaction.ts +44 -11
  63. package/src/runtime/navigation.svelte.ts +29 -40
  64. package/src/runtime/persistence.ts +2 -2
  65. package/src/runtime/progress.svelte.ts +22 -9
  66. package/src/runtime/quiz-engine.svelte.ts +361 -0
  67. package/src/runtime/quiz-policy.ts +28 -179
  68. package/src/runtime/types.ts +24 -2
  69. package/src/runtime/xapi/agent-rules.ts +11 -3
  70. package/src/runtime/xapi/client.ts +5 -5
  71. package/src/runtime/xapi/derive-actor.ts +2 -2
  72. package/src/runtime/xapi/publisher.ts +33 -40
  73. package/src/runtime/xapi/setup.ts +18 -15
  74. package/src/runtime/xapi/validation.ts +15 -6
  75. package/src/virtual.d.ts +4 -1
  76. package/styles/base.css +32 -11
  77. package/styles/layout.css +39 -18
  78. package/styles/theme.css +15 -3
  79. package/dist/validation-BxWAMMnJ.js.map +0 -1
@@ -1,11 +1,12 @@
1
- import { n as extractDefaultExportObjectLiteral, r as generateManifest, t as validateProject } from "../validation-BxWAMMnJ.js";
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 { cpSync, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
6
- import JSON5 from "json5";
5
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
6
+ import { cpSync, createWriteStream, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
7
7
  import { createHash } from "node:crypto";
8
8
  import { ZipArchive } from "archiver";
9
+ import { normalizePath } from "vite";
9
10
  //#region src/runtime/slugify.ts
10
11
  /**
11
12
  * Slugify a string for use as a URL-safe / filename-safe identifier.
@@ -151,77 +152,78 @@ function cleanOldZips(projectRoot, slug) {
151
152
  } catch {}
152
153
  } catch {}
153
154
  }
155
+ /** Packaged (zipped) export targets: which manifest file to write and how. */
156
+ const PACKAGED_EXPORTS = {
157
+ scorm12: {
158
+ manifestFile: "imsmanifest.xml",
159
+ label: "SCORM 1.2",
160
+ generate: generateSCORM12Manifest
161
+ },
162
+ scorm2004: {
163
+ manifestFile: "imsmanifest.xml",
164
+ label: "SCORM 2004",
165
+ generate: generateSCORM2004Manifest
166
+ },
167
+ cmi5: {
168
+ manifestFile: "cmi5.xml",
169
+ label: "CMI5",
170
+ generate: (config) => generateCMI5Xml(config)
171
+ }
172
+ };
154
173
  async function runExport(projectRoot, config) {
155
174
  const distDir = resolve(projectRoot, "dist");
156
175
  const standard = config.export?.standard || "web";
157
176
  const slug = slugify(config.title || "tessera-course") || "tessera-course";
158
177
  const zipName = `${slug}-${config.version || "1.0.0"}.zip`;
159
178
  const zipPath = resolve(projectRoot, zipName);
160
- switch (standard) {
161
- case "web": {
162
- const files = collectFiles(distDir);
163
- let totalSize = 0;
164
- for (const f of files) totalSize += statSync(resolve(distDir, f)).size;
165
- console.log(`✓ Web export: dist/ (${formatSize(totalSize)})`);
166
- break;
167
- }
168
- case "scorm12": {
169
- const manifest = generateSCORM12Manifest(config, distDir);
170
- writeFileSync(resolve(distDir, "imsmanifest.xml"), manifest, "utf-8");
171
- cleanOldZips(projectRoot, slug);
172
- const zipSize = await createZip(distDir, zipPath);
173
- console.log(`✓ SCORM 1.2 export: ${zipName} (${formatSize(zipSize)})`);
174
- break;
175
- }
176
- case "scorm2004": {
177
- const manifest = generateSCORM2004Manifest(config, distDir);
178
- writeFileSync(resolve(distDir, "imsmanifest.xml"), manifest, "utf-8");
179
- cleanOldZips(projectRoot, slug);
180
- const zipSize = await createZip(distDir, zipPath);
181
- console.log(`✓ SCORM 2004 export: ${zipName} (${formatSize(zipSize)})`);
182
- break;
183
- }
184
- case "cmi5": {
185
- const xml = generateCMI5Xml(config);
186
- writeFileSync(resolve(distDir, "cmi5.xml"), xml, "utf-8");
187
- cleanOldZips(projectRoot, slug);
188
- const zipSize = await createZip(distDir, zipPath);
189
- console.log(`✓ CMI5 export: ${zipName} (${formatSize(zipSize)})`);
190
- break;
191
- }
179
+ if (standard === "web") {
180
+ const files = collectFiles(distDir);
181
+ let totalSize = 0;
182
+ for (const f of files) totalSize += statSync(resolve(distDir, f)).size;
183
+ console.log(`✓ Web export: dist/ (${formatSize(totalSize)})`);
184
+ return;
192
185
  }
186
+ const spec = PACKAGED_EXPORTS[standard];
187
+ if (!spec) return;
188
+ writeFileSync(resolve(distDir, spec.manifestFile), spec.generate(config, distDir), "utf-8");
189
+ cleanOldZips(projectRoot, slug);
190
+ const zipSize = await createZip(distDir, zipPath);
191
+ console.log(`✓ ${spec.label} export: ${zipName} (${formatSize(zipSize)})`);
193
192
  }
194
193
  //#endregion
195
- //#region src/plugin/layout.ts
196
- const VIRTUAL_LAYOUT_ID = "virtual:tessera-layout";
197
- const RESOLVED_LAYOUT_ID = "\0" + VIRTUAL_LAYOUT_ID;
198
- function tesseraLayoutPlugin() {
199
- let projectRoot;
194
+ //#region src/plugin/override-plugin.ts
195
+ /**
196
+ * A virtual module that resolves to a project-root override file when present,
197
+ * and to the built-in (or a null export) otherwise. Shared by the layout and
198
+ * quiz plugins — they differ only in the virtual id, file name, and built-in.
199
+ */
200
+ function createOverridePlugin({ name, virtualId, projectFile, builtinFile }) {
201
+ const resolvedId = "\0" + virtualId;
202
+ const fallback = builtinFile ? `export { default } from '${normalizePath(builtinFile)}';` : "export default null;";
203
+ let filePath;
200
204
  return {
201
- name: "tessera:layout",
205
+ name,
202
206
  enforce: "pre",
203
207
  configResolved(config) {
204
- projectRoot = config.root;
208
+ filePath = resolve(config.root, projectFile);
205
209
  },
206
210
  resolveId(id) {
207
- if (id === VIRTUAL_LAYOUT_ID) return RESOLVED_LAYOUT_ID;
211
+ if (id === virtualId) return resolvedId;
208
212
  return null;
209
213
  },
210
214
  load(id) {
211
- if (id !== RESOLVED_LAYOUT_ID) return null;
212
- const layoutPath = resolve(projectRoot, "layout.svelte");
213
- if (existsSync(layoutPath)) {
214
- this.addWatchFile(layoutPath);
215
- return `export { default } from '${layoutPath.replace(/\\/g, "/")}';`;
215
+ if (id !== resolvedId) return null;
216
+ if (existsSync(filePath)) {
217
+ this.addWatchFile(filePath);
218
+ return `export { default } from '${normalizePath(filePath)}';`;
216
219
  }
217
- return `export default null;`;
220
+ return fallback;
218
221
  },
219
222
  configureServer(server) {
220
- const layoutPath = resolve(projectRoot, "layout.svelte");
221
- server.watcher.on("all", (event, filePath) => {
222
- if (filePath !== layoutPath) return;
223
+ server.watcher.on("all", (event, changed) => {
224
+ if (changed !== filePath) return;
223
225
  if (event !== "add" && event !== "unlink") return;
224
- const mod = server.moduleGraph.getModuleById(RESOLVED_LAYOUT_ID);
226
+ const mod = server.moduleGraph.getModuleById(resolvedId);
225
227
  if (mod) server.moduleGraph.invalidateModule(mod);
226
228
  server.ws.send({ type: "full-reload" });
227
229
  });
@@ -229,51 +231,30 @@ function tesseraLayoutPlugin() {
229
231
  };
230
232
  }
231
233
  //#endregion
234
+ //#region src/plugin/layout.ts
235
+ function tesseraLayoutPlugin() {
236
+ return createOverridePlugin({
237
+ name: "tessera:layout",
238
+ virtualId: "virtual:tessera-layout",
239
+ projectFile: "layout.svelte"
240
+ });
241
+ }
242
+ //#endregion
232
243
  //#region src/plugin/quiz.ts
233
- const VIRTUAL_QUIZ_ID = "virtual:tessera-quiz";
234
- const RESOLVED_QUIZ_ID = "\0" + VIRTUAL_QUIZ_ID;
235
244
  const __dirname$1 = dirname(fileURLToPath(import.meta.url));
236
- /**
237
- * Resolve the project's quiz shell.
238
- * `projectRoot/quiz.svelte` overrides the built-in `<Quiz>` if it exists,
239
- * otherwise the built-in is used. Mirrors `tesseraLayoutPlugin` (Phase 3A).
240
- */
241
245
  function tesseraQuizPlugin() {
242
- let projectRoot;
243
- const builtinQuiz = resolve(resolve(__dirname$1, "..", ".."), "src", "components", "Quiz.svelte");
244
- return {
246
+ return createOverridePlugin({
245
247
  name: "tessera:quiz",
246
- enforce: "pre",
247
- configResolved(config) {
248
- projectRoot = config.root;
249
- },
250
- resolveId(id) {
251
- if (id === VIRTUAL_QUIZ_ID) return RESOLVED_QUIZ_ID;
252
- return null;
253
- },
254
- load(id) {
255
- if (id !== RESOLVED_QUIZ_ID) return null;
256
- const userQuizPath = resolve(projectRoot, "quiz.svelte");
257
- if (existsSync(userQuizPath)) {
258
- this.addWatchFile(userQuizPath);
259
- return `export { default } from '${userQuizPath.replace(/\\/g, "/")}';`;
260
- }
261
- return `export { default } from '${builtinQuiz.replace(/\\/g, "/")}';`;
262
- },
263
- configureServer(server) {
264
- const userQuizPath = resolve(projectRoot, "quiz.svelte");
265
- server.watcher.on("all", (event, filePath) => {
266
- if (filePath !== userQuizPath) return;
267
- if (event !== "add" && event !== "unlink") return;
268
- const mod = server.moduleGraph.getModuleById(RESOLVED_QUIZ_ID);
269
- if (mod) server.moduleGraph.invalidateModule(mod);
270
- server.ws.send({ type: "full-reload" });
271
- });
272
- }
273
- };
248
+ virtualId: "virtual:tessera-quiz",
249
+ projectFile: "quiz.svelte",
250
+ builtinFile: resolve(resolve(__dirname$1, "..", ".."), "src", "components", "Quiz.svelte")
251
+ });
274
252
  }
275
253
  //#endregion
276
254
  //#region src/plugin/index.ts
255
+ function isAuditBuild() {
256
+ return process.env[AUDIT_ENV_FLAG] === "1";
257
+ }
277
258
  const __dirname = dirname(fileURLToPath(import.meta.url));
278
259
  function resolveRuntimeDir() {
279
260
  return resolve(resolve(__dirname, "..", ".."), "src", "runtime");
@@ -281,13 +262,44 @@ function resolveRuntimeDir() {
281
262
  function resolveStylesDir() {
282
263
  return resolve(resolve(__dirname, "..", ".."), "styles");
283
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
+ }
284
272
  function tesseraPlugin() {
285
273
  const manifestRef = {
286
274
  current: null,
287
275
  root: ""
288
276
  };
277
+ const a11y = {
278
+ warnings: [],
279
+ projectRoot: "",
280
+ isBuild: false,
281
+ settings: normalizeA11y(void 0)
282
+ };
289
283
  return [
290
- svelte({ compilerOptions: { css: "external" } }),
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),
291
303
  tesseraValidationPlugin(),
292
304
  tesseraEntryPlugin(),
293
305
  tesseraConfigPlugin(),
@@ -310,16 +322,18 @@ function tesseraEntryPlugin() {
310
322
  const stylesDir = resolveStylesDir();
311
323
  const appSveltePath = resolve(runtimeDir, "App.svelte");
312
324
  let projectRoot;
325
+ let outDir;
313
326
  let isBuild = false;
314
327
  return {
315
328
  name: "tessera:entry",
316
329
  enforce: "pre",
317
330
  configResolved(config) {
318
331
  projectRoot = config.root;
332
+ outDir = resolve(config.root, config.build.outDir);
319
333
  isBuild = config.command === "build";
320
334
  },
321
335
  buildStart() {
322
- if (isBuild) writeFileSync(resolve(projectRoot, "index.html"), generateIndexHtml(), "utf-8");
336
+ if (isBuild) writeFileSync(resolve(projectRoot, "index.html"), generateIndexHtml(readLanguage(projectRoot)), "utf-8");
323
337
  },
324
338
  closeBundle() {
325
339
  if (isBuild) {
@@ -328,7 +342,7 @@ function tesseraEntryPlugin() {
328
342
  unlinkSync(htmlPath);
329
343
  } catch {}
330
344
  const assetsDir = resolve(projectRoot, "assets");
331
- const distAssetsDir = resolve(projectRoot, "dist", "assets");
345
+ const distAssetsDir = resolve(outDir, "assets");
332
346
  if (existsSync(assetsDir)) {
333
347
  mkdirSync(distAssetsDir, { recursive: true });
334
348
  cpSync(assetsDir, distAssetsDir, { recursive: true });
@@ -339,7 +353,7 @@ function tesseraEntryPlugin() {
339
353
  return () => {
340
354
  server.middlewares.use(async (req, res, next) => {
341
355
  if (req.url === "/" || req.url === "/index.html") {
342
- const html = generateIndexHtml();
356
+ const html = generateIndexHtml(readLanguage(projectRoot));
343
357
  const transformed = await server.transformIndexHtml(req.url, html);
344
358
  res.setHeader("Content-Type", "text/html");
345
359
  res.statusCode = 200;
@@ -361,9 +375,14 @@ function tesseraEntryPlugin() {
361
375
  }
362
376
  };
363
377
  }
364
- function generateIndexHtml() {
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) {
365
384
  return `<!DOCTYPE html>
366
- <html lang="en">
385
+ <html lang="${lang}">
367
386
  <head>
368
387
  <meta charset="UTF-8" />
369
388
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -421,7 +440,8 @@ function tesseraConfigPlugin() {
421
440
  config(config) {
422
441
  return {
423
442
  base: "./",
424
- resolve: { alias: { "$assets": resolve(config.root || process.cwd(), "assets") } },
443
+ build: { assetsDir: "tessera" },
444
+ resolve: { alias: { $assets: resolve(config.root || process.cwd(), "assets") } },
425
445
  optimizeDeps: { exclude: ["tessera-learn"] }
426
446
  };
427
447
  },
@@ -435,14 +455,9 @@ function tesseraConfigPlugin() {
435
455
  load(id) {
436
456
  if (id === RESOLVED_CONFIG_ID) {
437
457
  const configPath = resolve(projectRoot, "course.config.js");
438
- let userConfig = {};
439
- if (existsSync(configPath)) {
440
- this.addWatchFile(configPath);
441
- const objectStr = extractDefaultExportObjectLiteral(readFileSync(configPath, "utf-8"));
442
- if (objectStr) try {
443
- userConfig = JSON5.parse(objectStr);
444
- } catch {}
445
- }
458
+ if (existsSync(configPath)) this.addWatchFile(configPath);
459
+ const read = readCourseConfig(projectRoot);
460
+ const userConfig = read.ok ? read.config : {};
446
461
  const { completion, passingScore } = completionDefaults(userConfig.completion?.mode);
447
462
  const merged = {
448
463
  title: userConfig.title || "Untitled Course",
@@ -516,13 +531,40 @@ function tesseraValidationPlugin() {
516
531
  }
517
532
  };
518
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
+ }
519
564
  function runValidation(projectRoot) {
520
- const { errors, warnings } = validateProject(projectRoot);
521
- for (const warning of warnings) console.warn(`\x1b[33m[tessera warning]\x1b[0m ${warning}`);
522
- if (errors.length > 0) {
523
- for (const error of errors) console.error(`\x1b[31m[tessera error]\x1b[0m ${error}`);
524
- throw new Error(`Tessera validation failed with ${errors.length} error(s). Fix the errors above to continue.`);
525
- }
565
+ const result = validateProject(projectRoot);
566
+ reportValidationIssues(result);
567
+ if (result.errors.length > 0) throw new Error(`Tessera validation failed with ${result.errors.length} error(s). Fix the errors above to continue.`);
526
568
  }
527
569
  function tesseraExportPlugin() {
528
570
  let projectRoot;
@@ -536,17 +578,14 @@ function tesseraExportPlugin() {
536
578
  },
537
579
  async closeBundle() {
538
580
  if (!isBuild) return;
539
- const configPath = resolve(projectRoot, "course.config.js");
540
- if (!existsSync(configPath)) throw new Error("[tessera:export] course.config.js not found at closeBundle. The file must exist for the export step to run.");
541
- const objectStr = extractDefaultExportObjectLiteral(readFileSync(configPath, "utf-8"));
542
- if (!objectStr) throw new Error("[tessera:export] course.config.js: could not locate `export default { ... }`. Cannot determine export.standard.");
543
- let config;
544
- try {
545
- config = JSON5.parse(objectStr);
546
- } catch (err) {
547
- throw new Error(`[tessera:export] course.config.js: failed to parse export-default object literal — ${err.message}`);
581
+ if (isAuditBuild()) return;
582
+ const read = readCourseConfig(projectRoot);
583
+ if (!read.ok) {
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.");
585
+ if (read.reason === "no-export") throw new Error("[tessera:export] course.config.js: could not locate `export default { ... }`. Cannot determine export.standard.");
586
+ throw new Error(`[tessera:export] course.config.js: failed to parse export-default object literal — ${read.error.message}`);
548
587
  }
549
- await runExport(projectRoot, config);
588
+ await runExport(projectRoot, read.config);
550
589
  }
551
590
  };
552
591
  }
@@ -620,14 +659,9 @@ function tesseraAdapterPlugin() {
620
659
  if (id !== RESOLVED_ADAPTER_ID) return null;
621
660
  if (!isBuild) return `export { createAdapter } from 'tessera-learn/runtime/adapters/index.js';`;
622
661
  let standard = "web";
623
- const configPath = resolve(projectRoot, "course.config.js");
624
- if (existsSync(configPath)) {
625
- const objectStr = extractDefaultExportObjectLiteral(readFileSync(configPath, "utf-8"));
626
- if (objectStr) try {
627
- const parsed = JSON5.parse(objectStr);
628
- if (typeof parsed?.export?.standard === "string") standard = parsed.export.standard;
629
- } catch {}
630
- }
662
+ const read = readCourseConfig(projectRoot);
663
+ if (read.ok && typeof read.config.export?.standard === "string") standard = read.config.export.standard;
664
+ if (isAuditBuild()) standard = "web";
631
665
  switch (standard) {
632
666
  case "scorm12": return `
633
667
  import { SCORM12Adapter } from 'tessera-learn/runtime/adapters/scorm12.js';
@@ -687,16 +721,13 @@ function tesseraXAPISetupPlugin() {
687
721
  load(id) {
688
722
  if (id !== RESOLVED_XAPI_SETUP_ID) return null;
689
723
  if (!isBuild) return `export { buildXAPIClient } from 'tessera-learn/runtime/xapi/setup.js';`;
724
+ if (isAuditBuild()) return `export async function buildXAPIClient() { return null; }`;
690
725
  let standard = "web";
691
726
  let hasXapi = false;
692
- const configPath = resolve(projectRoot, "course.config.js");
693
- if (existsSync(configPath)) {
694
- const objectStr = extractDefaultExportObjectLiteral(readFileSync(configPath, "utf-8"));
695
- if (objectStr) try {
696
- const parsed = JSON5.parse(objectStr);
697
- if (typeof parsed?.export?.standard === "string") standard = parsed.export.standard;
698
- hasXapi = parsed?.xapi != null;
699
- } catch {}
727
+ const read = readCourseConfig(projectRoot);
728
+ if (read.ok) {
729
+ if (typeof read.config.export?.standard === "string") standard = read.config.export.standard;
730
+ hasXapi = read.config.xapi != null;
700
731
  }
701
732
  if (hasXapi || standard === "cmi5") return `export { buildXAPIClient } from 'tessera-learn/runtime/xapi/setup.js';`;
702
733
  return `export async function buildXAPIClient() { return null; }`;
@@ -728,6 +759,6 @@ function tesseraFirstPagePreloadPlugin(manifestRef) {
728
759
  };
729
760
  }
730
761
  //#endregion
731
- export { tesseraPlugin };
762
+ export { runAudit, tesseraPlugin };
732
763
 
733
764
  //# sourceMappingURL=index.js.map