tessera-learn 0.0.9 → 0.0.11

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 (45) hide show
  1. package/dist/plugin/cli.js +5 -3
  2. package/dist/plugin/cli.js.map +1 -1
  3. package/dist/plugin/index.d.ts.map +1 -1
  4. package/dist/plugin/index.js +215 -124
  5. package/dist/plugin/index.js.map +1 -1
  6. package/dist/{validation-BxWAMMnJ.js → validation-D9DXlqNP.js} +77 -65
  7. package/dist/validation-D9DXlqNP.js.map +1 -0
  8. package/package.json +9 -6
  9. package/src/components/Audio.svelte +5 -2
  10. package/src/components/DefaultLayout.svelte +2 -0
  11. package/src/components/FillInTheBlank.svelte +60 -98
  12. package/src/components/Image.svelte +3 -8
  13. package/src/components/LockedBanner.svelte +3 -4
  14. package/src/components/MultipleChoice.svelte +53 -94
  15. package/src/components/Quiz.svelte +2 -1
  16. package/src/components/Video.svelte +4 -2
  17. package/src/components/util.ts +1 -0
  18. package/src/plugin/cli.ts +2 -7
  19. package/src/plugin/export.ts +23 -41
  20. package/src/plugin/index.ts +197 -56
  21. package/src/plugin/layout.ts +6 -51
  22. package/src/plugin/manifest.ts +31 -5
  23. package/src/plugin/override-plugin.ts +68 -0
  24. package/src/plugin/quiz.ts +9 -54
  25. package/src/plugin/validation.ts +38 -67
  26. package/src/runtime/App.svelte +48 -36
  27. package/src/runtime/LoadingBar.svelte +47 -0
  28. package/src/runtime/Sidebar.svelte +2 -0
  29. package/src/runtime/adapters/cmi5.ts +13 -83
  30. package/src/runtime/adapters/format.ts +67 -0
  31. package/src/runtime/adapters/index.ts +28 -29
  32. package/src/runtime/adapters/retry.ts +0 -64
  33. package/src/runtime/adapters/scorm12.ts +1 -1
  34. package/src/runtime/adapters/scorm2004.ts +11 -16
  35. package/src/runtime/hooks.svelte.ts +14 -16
  36. package/src/runtime/navigation.svelte.ts +51 -45
  37. package/src/runtime/progress.svelte.ts +25 -10
  38. package/src/runtime/quiz-policy.ts +21 -178
  39. package/src/runtime/xapi/agent-rules.ts +7 -2
  40. package/src/runtime/xapi/publisher.ts +1 -11
  41. package/src/runtime/xapi/validation.ts +1 -1
  42. package/src/virtual.d.ts +13 -0
  43. package/styles/layout.css +34 -24
  44. package/dist/validation-BxWAMMnJ.js.map +0 -1
  45. package/src/runtime/LoadingSkeleton.svelte +0 -26
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { t as validateProject } from "../validation-BxWAMMnJ.js";
2
+ import { n as validateProject, t as reportValidationIssues } from "../validation-D9DXlqNP.js";
3
3
  //#region src/plugin/cli.ts
4
4
  const { errors, warnings } = validateProject(process.cwd());
5
- for (const warning of warnings) console.warn(`\x1b[33m[tessera warning]\x1b[0m ${warning}`);
6
- for (const error of errors) console.error(`\x1b[31m[tessera error]\x1b[0m ${error}`);
5
+ reportValidationIssues({
6
+ errors,
7
+ warnings
8
+ });
7
9
  if (errors.length > 0) {
8
10
  const summary = `Validation failed with ${errors.length} error(s)` + (warnings.length > 0 ? ` and ${warnings.length} warning(s)` : "") + ".";
9
11
  console.error(`\n\x1b[31m${summary}\x1b[0m`);
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../../src/plugin/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { validateProject } from './validation.js';\n\nconst projectRoot = process.cwd();\nconst { errors, warnings } = validateProject(projectRoot);\n\nfor (const warning of warnings) {\n console.warn(`\\x1b[33m[tessera warning]\\x1b[0m ${warning}`);\n}\nfor (const error of errors) {\n console.error(`\\x1b[31m[tessera error]\\x1b[0m ${error}`);\n}\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}\nprocess.exit(0);\n"],"mappings":";;;AAIA,MAAM,EAAE,QAAQ,aAAa,gBADT,QAAQ,KAC4B,CAAC;AAEzD,KAAK,MAAM,WAAW,UACpB,QAAQ,KAAK,oCAAoC,UAAU;AAE7D,KAAK,MAAM,SAAS,QAClB,QAAQ,MAAM,kCAAkC,QAAQ;AAG1D,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,KAAK,EAAE"}
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}\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,KAAK,EAAE"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/plugin/index.ts"],"mappings":";;;iBA4BgB,aAAA,CAAA,IAAa,MAAA,QAAA,MAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/plugin/index.ts"],"mappings":";;;iBA2BgB,aAAA,CAAA,IAAa,MAAA,QAAA,MAAA"}
@@ -1,11 +1,11 @@
1
- import { n as extractDefaultExportObjectLiteral, r as generateManifest, t as validateProject } from "../validation-BxWAMMnJ.js";
1
+ import { i as readCourseConfig, n as validateProject, r as generateManifest, t as reportValidationIssues } from "../validation-D9DXlqNP.js";
2
2
  import { svelte } from "@sveltejs/vite-plugin-svelte";
3
3
  import { fileURLToPath } from "node:url";
4
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 { cpSync, createWriteStream, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
7
6
  import { createHash } from "node:crypto";
8
7
  import { ZipArchive } from "archiver";
8
+ import { normalizePath } from "vite";
9
9
  //#region src/runtime/slugify.ts
10
10
  /**
11
11
  * Slugify a string for use as a URL-safe / filename-safe identifier.
@@ -151,77 +151,78 @@ function cleanOldZips(projectRoot, slug) {
151
151
  } catch {}
152
152
  } catch {}
153
153
  }
154
+ /** Packaged (zipped) export targets: which manifest file to write and how. */
155
+ const PACKAGED_EXPORTS = {
156
+ scorm12: {
157
+ manifestFile: "imsmanifest.xml",
158
+ label: "SCORM 1.2",
159
+ generate: generateSCORM12Manifest
160
+ },
161
+ scorm2004: {
162
+ manifestFile: "imsmanifest.xml",
163
+ label: "SCORM 2004",
164
+ generate: generateSCORM2004Manifest
165
+ },
166
+ cmi5: {
167
+ manifestFile: "cmi5.xml",
168
+ label: "CMI5",
169
+ generate: (config) => generateCMI5Xml(config)
170
+ }
171
+ };
154
172
  async function runExport(projectRoot, config) {
155
173
  const distDir = resolve(projectRoot, "dist");
156
174
  const standard = config.export?.standard || "web";
157
175
  const slug = slugify(config.title || "tessera-course") || "tessera-course";
158
176
  const zipName = `${slug}-${config.version || "1.0.0"}.zip`;
159
177
  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
- }
178
+ if (standard === "web") {
179
+ const files = collectFiles(distDir);
180
+ let totalSize = 0;
181
+ for (const f of files) totalSize += statSync(resolve(distDir, f)).size;
182
+ console.log(`✓ Web export: dist/ (${formatSize(totalSize)})`);
183
+ return;
192
184
  }
185
+ const spec = PACKAGED_EXPORTS[standard];
186
+ if (!spec) return;
187
+ writeFileSync(resolve(distDir, spec.manifestFile), spec.generate(config, distDir), "utf-8");
188
+ cleanOldZips(projectRoot, slug);
189
+ const zipSize = await createZip(distDir, zipPath);
190
+ console.log(`✓ ${spec.label} export: ${zipName} (${formatSize(zipSize)})`);
193
191
  }
194
192
  //#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;
193
+ //#region src/plugin/override-plugin.ts
194
+ /**
195
+ * A virtual module that resolves to a project-root override file when present,
196
+ * and to the built-in (or a null export) otherwise. Shared by the layout and
197
+ * quiz plugins — they differ only in the virtual id, file name, and built-in.
198
+ */
199
+ function createOverridePlugin({ name, virtualId, projectFile, builtinFile }) {
200
+ const resolvedId = "\0" + virtualId;
201
+ const fallback = builtinFile ? `export { default } from '${normalizePath(builtinFile)}';` : "export default null;";
202
+ let filePath;
200
203
  return {
201
- name: "tessera:layout",
204
+ name,
202
205
  enforce: "pre",
203
206
  configResolved(config) {
204
- projectRoot = config.root;
207
+ filePath = resolve(config.root, projectFile);
205
208
  },
206
209
  resolveId(id) {
207
- if (id === VIRTUAL_LAYOUT_ID) return RESOLVED_LAYOUT_ID;
210
+ if (id === virtualId) return resolvedId;
208
211
  return null;
209
212
  },
210
213
  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, "/")}';`;
214
+ if (id !== resolvedId) return null;
215
+ if (existsSync(filePath)) {
216
+ this.addWatchFile(filePath);
217
+ return `export { default } from '${normalizePath(filePath)}';`;
216
218
  }
217
- return `export default null;`;
219
+ return fallback;
218
220
  },
219
221
  configureServer(server) {
220
- const layoutPath = resolve(projectRoot, "layout.svelte");
221
- server.watcher.on("all", (event, filePath) => {
222
- if (filePath !== layoutPath) return;
222
+ server.watcher.on("all", (event, changed) => {
223
+ if (changed !== filePath) return;
223
224
  if (event !== "add" && event !== "unlink") return;
224
- const mod = server.moduleGraph.getModuleById(RESOLVED_LAYOUT_ID);
225
+ const mod = server.moduleGraph.getModuleById(resolvedId);
225
226
  if (mod) server.moduleGraph.invalidateModule(mod);
226
227
  server.ws.send({ type: "full-reload" });
227
228
  });
@@ -229,48 +230,24 @@ function tesseraLayoutPlugin() {
229
230
  };
230
231
  }
231
232
  //#endregion
233
+ //#region src/plugin/layout.ts
234
+ function tesseraLayoutPlugin() {
235
+ return createOverridePlugin({
236
+ name: "tessera:layout",
237
+ virtualId: "virtual:tessera-layout",
238
+ projectFile: "layout.svelte"
239
+ });
240
+ }
241
+ //#endregion
232
242
  //#region src/plugin/quiz.ts
233
- const VIRTUAL_QUIZ_ID = "virtual:tessera-quiz";
234
- const RESOLVED_QUIZ_ID = "\0" + VIRTUAL_QUIZ_ID;
235
243
  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
244
  function tesseraQuizPlugin() {
242
- let projectRoot;
243
- const builtinQuiz = resolve(resolve(__dirname$1, "..", ".."), "src", "components", "Quiz.svelte");
244
- return {
245
+ return createOverridePlugin({
245
246
  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
- };
247
+ virtualId: "virtual:tessera-quiz",
248
+ projectFile: "quiz.svelte",
249
+ builtinFile: resolve(resolve(__dirname$1, "..", ".."), "src", "components", "Quiz.svelte")
250
+ });
274
251
  }
275
252
  //#endregion
276
253
  //#region src/plugin/index.ts
@@ -282,15 +259,22 @@ function resolveStylesDir() {
282
259
  return resolve(resolve(__dirname, "..", ".."), "styles");
283
260
  }
284
261
  function tesseraPlugin() {
262
+ const manifestRef = {
263
+ current: null,
264
+ root: ""
265
+ };
285
266
  return [
286
- svelte({ compilerOptions: { css: "injected" } }),
267
+ svelte({ compilerOptions: { css: "external" } }),
287
268
  tesseraValidationPlugin(),
288
269
  tesseraEntryPlugin(),
289
270
  tesseraConfigPlugin(),
290
271
  tesseraPagesPlugin(),
291
- tesseraManifestPlugin(),
272
+ tesseraManifestPlugin(manifestRef),
292
273
  tesseraLayoutPlugin(),
293
274
  tesseraQuizPlugin(),
275
+ tesseraAdapterPlugin(),
276
+ tesseraXAPISetupPlugin(),
277
+ tesseraFirstPagePreloadPlugin(manifestRef),
294
278
  tesseraExportPlugin()
295
279
  ];
296
280
  }
@@ -414,6 +398,7 @@ function tesseraConfigPlugin() {
414
398
  config(config) {
415
399
  return {
416
400
  base: "./",
401
+ build: { assetsDir: "tessera" },
417
402
  resolve: { alias: { "$assets": resolve(config.root || process.cwd(), "assets") } },
418
403
  optimizeDeps: { exclude: ["tessera-learn"] }
419
404
  };
@@ -428,14 +413,9 @@ function tesseraConfigPlugin() {
428
413
  load(id) {
429
414
  if (id === RESOLVED_CONFIG_ID) {
430
415
  const configPath = resolve(projectRoot, "course.config.js");
431
- let userConfig = {};
432
- if (existsSync(configPath)) {
433
- this.addWatchFile(configPath);
434
- const objectStr = extractDefaultExportObjectLiteral(readFileSync(configPath, "utf-8"));
435
- if (objectStr) try {
436
- userConfig = JSON5.parse(objectStr);
437
- } catch {}
438
- }
416
+ if (existsSync(configPath)) this.addWatchFile(configPath);
417
+ const read = readCourseConfig(projectRoot);
418
+ const userConfig = read.ok ? read.config : {};
439
419
  const { completion, passingScore } = completionDefaults(userConfig.completion?.mode);
440
420
  const merged = {
441
421
  title: userConfig.title || "Untitled Course",
@@ -510,12 +490,9 @@ function tesseraValidationPlugin() {
510
490
  };
511
491
  }
512
492
  function runValidation(projectRoot) {
513
- const { errors, warnings } = validateProject(projectRoot);
514
- for (const warning of warnings) console.warn(`\x1b[33m[tessera warning]\x1b[0m ${warning}`);
515
- if (errors.length > 0) {
516
- for (const error of errors) console.error(`\x1b[31m[tessera error]\x1b[0m ${error}`);
517
- throw new Error(`Tessera validation failed with ${errors.length} error(s). Fix the errors above to continue.`);
518
- }
493
+ const result = validateProject(projectRoot);
494
+ reportValidationIssues(result);
495
+ if (result.errors.length > 0) throw new Error(`Tessera validation failed with ${result.errors.length} error(s). Fix the errors above to continue.`);
519
496
  }
520
497
  function tesseraExportPlugin() {
521
498
  let projectRoot;
@@ -529,29 +506,25 @@ function tesseraExportPlugin() {
529
506
  },
530
507
  async closeBundle() {
531
508
  if (!isBuild) return;
532
- const configPath = resolve(projectRoot, "course.config.js");
533
- 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.");
534
- const objectStr = extractDefaultExportObjectLiteral(readFileSync(configPath, "utf-8"));
535
- if (!objectStr) throw new Error("[tessera:export] course.config.js: could not locate `export default { ... }`. Cannot determine export.standard.");
536
- let config;
537
- try {
538
- config = JSON5.parse(objectStr);
539
- } catch (err) {
540
- throw new Error(`[tessera:export] course.config.js: failed to parse export-default object literal — ${err.message}`);
509
+ const read = readCourseConfig(projectRoot);
510
+ if (!read.ok) {
511
+ 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.");
512
+ if (read.reason === "no-export") throw new Error("[tessera:export] course.config.js: could not locate `export default { ... }`. Cannot determine export.standard.");
513
+ throw new Error(`[tessera:export] course.config.js: failed to parse export-default object literal — ${read.error.message}`);
541
514
  }
542
- await runExport(projectRoot, config);
515
+ await runExport(projectRoot, read.config);
543
516
  }
544
517
  };
545
518
  }
546
519
  const VIRTUAL_MANIFEST_ID = "virtual:tessera-manifest";
547
520
  const RESOLVED_MANIFEST_ID = "\0" + VIRTUAL_MANIFEST_ID;
548
- function tesseraManifestPlugin() {
521
+ function tesseraManifestPlugin(manifestRef) {
549
522
  let projectRoot;
550
523
  let pagesDir;
551
- let currentManifest = null;
552
524
  function buildManifest() {
553
- currentManifest = generateManifest(pagesDir);
554
- return currentManifest;
525
+ const m = generateManifest(pagesDir);
526
+ manifestRef.current = m;
527
+ return m;
555
528
  }
556
529
  return {
557
530
  name: "tessera:manifest",
@@ -559,12 +532,13 @@ function tesseraManifestPlugin() {
559
532
  configResolved(config) {
560
533
  projectRoot = config.root;
561
534
  pagesDir = resolve(projectRoot, "pages");
535
+ manifestRef.root = projectRoot;
562
536
  },
563
537
  configureServer(devServer) {
564
538
  devServer.watcher.on("all", (event, filePath) => {
565
539
  if (!filePath.startsWith(pagesDir)) return;
566
540
  if (filePath.endsWith(".svelte") || filePath.endsWith("_meta.js") || event === "addDir" || event === "unlinkDir") {
567
- currentManifest = null;
541
+ manifestRef.current = null;
568
542
  const mod = devServer.moduleGraph.getModuleById(RESOLVED_MANIFEST_ID);
569
543
  if (mod) {
570
544
  devServer.moduleGraph.invalidateModule(mod);
@@ -583,15 +557,132 @@ function tesseraManifestPlugin() {
583
557
  },
584
558
  load(id) {
585
559
  if (id === RESOLVED_MANIFEST_ID) {
586
- if (!currentManifest) buildManifest();
560
+ if (!manifestRef.current) buildManifest();
587
561
  addWatchFiles(this, pagesDir);
588
- const json = JSON.stringify(currentManifest, (_key, value) => value === Infinity ? 1e9 : value);
562
+ const json = JSON.stringify(manifestRef.current, (_key, value) => value === Infinity ? 1e9 : value);
589
563
  return `export default JSON.parse(atob("${Buffer.from(json).toString("base64")}"));`;
590
564
  }
591
565
  return null;
592
566
  }
593
567
  };
594
568
  }
569
+ const VIRTUAL_ADAPTER_ID = "virtual:tessera-adapter";
570
+ const RESOLVED_ADAPTER_ID = "\0" + VIRTUAL_ADAPTER_ID;
571
+ function tesseraAdapterPlugin() {
572
+ let projectRoot;
573
+ let isBuild = false;
574
+ return {
575
+ name: "tessera:adapter",
576
+ enforce: "pre",
577
+ configResolved(config) {
578
+ projectRoot = config.root;
579
+ isBuild = config.command === "build";
580
+ },
581
+ resolveId(id) {
582
+ if (id === VIRTUAL_ADAPTER_ID) return RESOLVED_ADAPTER_ID;
583
+ return null;
584
+ },
585
+ load(id) {
586
+ if (id !== RESOLVED_ADAPTER_ID) return null;
587
+ if (!isBuild) return `export { createAdapter } from 'tessera-learn/runtime/adapters/index.js';`;
588
+ let standard = "web";
589
+ const read = readCourseConfig(projectRoot);
590
+ if (read.ok && typeof read.config.export?.standard === "string") standard = read.config.export.standard;
591
+ switch (standard) {
592
+ case "scorm12": return `
593
+ import { SCORM12Adapter } from 'tessera-learn/runtime/adapters/scorm12.js';
594
+ import { findSCORM12API } from 'tessera-learn/runtime/adapters/discovery.js';
595
+ import { LMSAdapterError } from 'tessera-learn/runtime/adapters/index.js';
596
+ export function createAdapter() {
597
+ const api = findSCORM12API();
598
+ if (!api) throw new LMSAdapterError('scorm12', 'Tessera: SCORM 1.2 API not found in window.parent/opener chain. Course must be launched from a SCORM 1.2 LMS.');
599
+ return new SCORM12Adapter(api);
600
+ }
601
+ `;
602
+ case "scorm2004": return `
603
+ import { SCORM2004Adapter } from 'tessera-learn/runtime/adapters/scorm2004.js';
604
+ import { findSCORM2004API } from 'tessera-learn/runtime/adapters/discovery.js';
605
+ import { LMSAdapterError } from 'tessera-learn/runtime/adapters/index.js';
606
+ export function createAdapter() {
607
+ const api = findSCORM2004API();
608
+ if (!api) throw new LMSAdapterError('scorm2004', 'Tessera: SCORM 2004 API not found in window.parent/opener chain. Course must be launched from a SCORM 2004 LMS.');
609
+ return new SCORM2004Adapter(api);
610
+ }
611
+ `;
612
+ case "cmi5": return `
613
+ import { CMI5Adapter } from 'tessera-learn/runtime/adapters/cmi5.js';
614
+ import { hasCMI5LaunchParams } from 'tessera-learn/runtime/adapters/discovery.js';
615
+ import { LMSAdapterError } from 'tessera-learn/runtime/adapters/index.js';
616
+ export function createAdapter() {
617
+ if (!hasCMI5LaunchParams()) throw new LMSAdapterError('cmi5', 'Tessera: cmi5 launch parameters not present on URL. Course must be launched from a cmi5-compliant LMS.');
618
+ return new CMI5Adapter();
619
+ }
620
+ `;
621
+ default: return `
622
+ import { WebAdapter } from 'tessera-learn/runtime/adapters/web.js';
623
+ export function createAdapter(config) {
624
+ return new WebAdapter(config);
625
+ }
626
+ `;
627
+ }
628
+ }
629
+ };
630
+ }
631
+ const VIRTUAL_XAPI_SETUP_ID = "virtual:tessera-xapi-setup";
632
+ const RESOLVED_XAPI_SETUP_ID = "\0" + VIRTUAL_XAPI_SETUP_ID;
633
+ function tesseraXAPISetupPlugin() {
634
+ let projectRoot;
635
+ let isBuild = false;
636
+ return {
637
+ name: "tessera:xapi-setup",
638
+ enforce: "pre",
639
+ configResolved(config) {
640
+ projectRoot = config.root;
641
+ isBuild = config.command === "build";
642
+ },
643
+ resolveId(id) {
644
+ if (id === VIRTUAL_XAPI_SETUP_ID) return RESOLVED_XAPI_SETUP_ID;
645
+ return null;
646
+ },
647
+ load(id) {
648
+ if (id !== RESOLVED_XAPI_SETUP_ID) return null;
649
+ if (!isBuild) return `export { buildXAPIClient } from 'tessera-learn/runtime/xapi/setup.js';`;
650
+ let standard = "web";
651
+ let hasXapi = false;
652
+ const read = readCourseConfig(projectRoot);
653
+ if (read.ok) {
654
+ if (typeof read.config.export?.standard === "string") standard = read.config.export.standard;
655
+ hasXapi = read.config.xapi != null;
656
+ }
657
+ if (hasXapi || standard === "cmi5") return `export { buildXAPIClient } from 'tessera-learn/runtime/xapi/setup.js';`;
658
+ return `export async function buildXAPIClient() { return null; }`;
659
+ }
660
+ };
661
+ }
662
+ function tesseraFirstPagePreloadPlugin(manifestRef) {
663
+ return {
664
+ name: "tessera:first-page-preload",
665
+ apply: "build",
666
+ transformIndexHtml: {
667
+ order: "post",
668
+ handler(_html, ctx) {
669
+ const firstPagePath = manifestRef.current?.pages[0]?.importPath;
670
+ if (!firstPagePath || !ctx.bundle) return;
671
+ const normalized = resolve(manifestRef.root, firstPagePath.replace(/^\//, "")).replace(/\\/g, "/");
672
+ const chunk = Object.values(ctx.bundle).find((c) => c.type === "chunk" && !!c.facadeModuleId && c.facadeModuleId.replace(/\\/g, "/") === normalized);
673
+ if (!chunk) return;
674
+ return [{
675
+ tag: "link",
676
+ attrs: {
677
+ rel: "modulepreload",
678
+ href: `./${chunk.fileName}`
679
+ },
680
+ injectTo: "head"
681
+ }];
682
+ }
683
+ }
684
+ };
685
+ }
595
686
  //#endregion
596
687
  export { tesseraPlugin };
597
688