tessera-learn 0.0.13 → 0.2.0

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 (68) hide show
  1. package/AGENTS.md +1794 -0
  2. package/README.md +5 -5
  3. package/dist/{validation-B-xTvM9B.js → audit-BA5o0ick.js} +605 -269
  4. package/dist/audit-BA5o0ick.js.map +1 -0
  5. package/dist/build-commands-C0OnV-Vg.js +27 -0
  6. package/dist/build-commands-C0OnV-Vg.js.map +1 -0
  7. package/dist/inline-config-CroQ-_2Y.js +31 -0
  8. package/dist/inline-config-CroQ-_2Y.js.map +1 -0
  9. package/dist/plugin/cli.d.ts +9 -1
  10. package/dist/plugin/cli.d.ts.map +1 -0
  11. package/dist/plugin/cli.js +326 -17
  12. package/dist/plugin/cli.js.map +1 -1
  13. package/dist/plugin/index.d.ts +1 -1
  14. package/dist/plugin/index.d.ts.map +1 -1
  15. package/dist/plugin/index.js +2 -763
  16. package/dist/plugin-W_rk3Pit.js +731 -0
  17. package/dist/plugin-W_rk3Pit.js.map +1 -0
  18. package/package.json +21 -9
  19. package/src/components/FillInTheBlank.svelte +2 -2
  20. package/src/components/Matching.svelte +2 -2
  21. package/src/components/MultipleChoice.svelte +2 -2
  22. package/src/components/RevealModal.svelte +48 -103
  23. package/src/components/Sorting.svelte +2 -2
  24. package/src/components/util.ts +9 -0
  25. package/src/plugin/a11y/audit.ts +40 -8
  26. package/src/plugin/a11y-cli.ts +39 -22
  27. package/src/plugin/ast.ts +276 -0
  28. package/src/plugin/build-commands.ts +31 -0
  29. package/src/plugin/cli.ts +96 -21
  30. package/src/plugin/course-root.ts +98 -0
  31. package/src/plugin/duplicate-cli.ts +74 -0
  32. package/src/plugin/index.ts +87 -122
  33. package/src/plugin/inline-config.ts +54 -0
  34. package/src/plugin/manifest.ts +103 -136
  35. package/src/plugin/new-cli.ts +51 -0
  36. package/src/plugin/package-root.ts +24 -0
  37. package/src/plugin/project-name.ts +29 -0
  38. package/src/plugin/quiz.ts +8 -9
  39. package/src/plugin/template-copy.ts +43 -0
  40. package/src/plugin/validate-cli.ts +30 -0
  41. package/src/plugin/validation.ts +152 -244
  42. package/src/runtime/App.svelte +11 -97
  43. package/src/runtime/Sidebar.svelte +3 -1
  44. package/src/runtime/adapters/cmi5.ts +6 -10
  45. package/src/runtime/adapters/format.ts +6 -0
  46. package/src/runtime/adapters/retry.ts +1 -1
  47. package/src/runtime/adapters/scorm2004.ts +2 -4
  48. package/src/runtime/branding.ts +90 -0
  49. package/src/runtime/defaults.ts +3 -0
  50. package/src/runtime/hooks.svelte.ts +16 -53
  51. package/src/runtime/interaction-format.ts +3 -8
  52. package/src/runtime/progress.svelte.ts +47 -83
  53. package/src/runtime/xapi/derive-actor.ts +41 -48
  54. package/src/runtime/xapi/publisher.ts +14 -14
  55. package/src/runtime/xapi/setup.ts +39 -46
  56. package/templates/course/course.config.js +11 -0
  57. package/templates/course/layout.svelte +116 -0
  58. package/templates/course/pages/01-getting-started/01-welcome/_meta.js +1 -0
  59. package/templates/course/pages/01-getting-started/01-welcome/welcome.svelte +19 -0
  60. package/templates/course/pages/01-getting-started/_meta.js +1 -0
  61. package/templates/course/styles/custom.css +5 -0
  62. package/dist/audit-BBJpQGqb.js +0 -204
  63. package/dist/audit-BBJpQGqb.js.map +0 -1
  64. package/dist/plugin/a11y-cli.d.ts +0 -1
  65. package/dist/plugin/a11y-cli.js +0 -36
  66. package/dist/plugin/a11y-cli.js.map +0 -1
  67. package/dist/plugin/index.js.map +0 -1
  68. package/dist/validation-B-xTvM9B.js.map +0 -1
@@ -1,764 +1,3 @@
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";
3
- import { svelte } from "@sveltejs/vite-plugin-svelte";
4
- import { fileURLToPath } from "node:url";
5
- import { dirname, isAbsolute, relative, resolve } from "node:path";
6
- import { cpSync, createWriteStream, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
7
- import { createHash } from "node:crypto";
8
- import { ZipArchive } from "archiver";
9
- import { normalizePath } from "vite";
10
- //#region src/runtime/slugify.ts
11
- /**
12
- * Slugify a string for use as a URL-safe / filename-safe identifier.
13
- * "My Course Title" → "my-course-title"
14
- *
15
- * Shared by the runtime (`WebAdapter` localStorage key) and the build-time
16
- * exporter (`runExport` zip filename). Both want identical, deterministic
17
- * output so a course's storage key matches its package name.
18
- */
19
- function slugify(text) {
20
- return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
21
- }
22
- //#endregion
23
- //#region src/plugin/export.ts
24
- function escapeXml(str) {
25
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
26
- }
27
- /**
28
- * Recursively collect all file paths relative to a directory.
29
- */
30
- function collectFiles(dir, base = "") {
31
- const files = [];
32
- if (!existsSync(dir)) return files;
33
- for (const entry of readdirSync(dir)) {
34
- const fullPath = resolve(dir, entry);
35
- const relPath = base ? `${base}/${entry}` : entry;
36
- if (statSync(fullPath).isDirectory()) files.push(...collectFiles(fullPath, relPath));
37
- else files.push(relPath);
38
- }
39
- return files;
40
- }
41
- /**
42
- * Derive a stable URN IRI from a seed string. cmi5 §13.1 / xs:anyURI
43
- * require course / AU ids to be IRIs — bare hex or UUID-shaped strings
44
- * (without correct version/variant bits) aren't conformant URNs and may
45
- * be rejected by strict LMS importers.
46
- *
47
- * Hash the seed so the id survives rebuilds, then format as
48
- * `urn:tessera:<kind>:<hex>`. The same seed always produces the same
49
- * IRI, so existing LRS records are not orphaned by re-export.
50
- */
51
- function stableUrn(kind, seed) {
52
- return `urn:tessera:${kind}:${createHash("sha256").update(seed).digest("hex").slice(0, 32)}`;
53
- }
54
- function formatSize(bytes) {
55
- if (bytes < 1024) return `${bytes} B`;
56
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
57
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
58
- }
59
- const SCORM_DIALECTS = {
60
- "1.2": {
61
- rootNs: "http://www.imsproject.org/xsd/imscp_rootv1p1p2",
62
- adlcpNs: "http://www.adlnet.org/xsd/adlcp_rootv1p2",
63
- schemaversion: "1.2",
64
- scormTypeAttr: "scormtype",
65
- schemaLocation: "http://www.imsproject.org/xsd/imscp_rootv1p1p2 imscp_rootv1p1p2.xsd http://www.imsglobal.org/xsd/imsmd_rootv1p2p1 imsmd_rootv1p2p1.xsd http://www.adlnet.org/xsd/adlcp_rootv1p2 adlcp_rootv1p2.xsd"
66
- },
67
- "2004": {
68
- rootNs: "http://www.imsglobal.org/xsd/imscp_v1p1",
69
- adlcpNs: "http://www.adlnet.org/xsd/adlcp_v1p3",
70
- schemaversion: "2004 4th Edition",
71
- scormTypeAttr: "scormType",
72
- schemaLocation: "http://www.imsglobal.org/xsd/imscp_v1p1 imscp_v1p1.xsd http://www.adlnet.org/xsd/adlcp_v1p3 adlcp_v1p3.xsd"
73
- }
74
- };
75
- function generateScormManifest(version, config, distDir) {
76
- const dialect = SCORM_DIALECTS[version];
77
- const title = escapeXml(config.title || "Tessera Course");
78
- const fileElements = collectFiles(distDir).map((f) => ` <file href="${escapeXml(f)}" />`).join("\n");
79
- return `<?xml version="1.0" encoding="UTF-8"?>
80
- <manifest identifier="tessera-course" version="1.0"
81
- xmlns="${dialect.rootNs}"
82
- xmlns:adlcp="${dialect.adlcpNs}"
83
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
84
- xsi:schemaLocation="${dialect.schemaLocation}">
85
- <metadata>
86
- <schema>ADL SCORM</schema>
87
- <schemaversion>${dialect.schemaversion}</schemaversion>
88
- </metadata>
89
- <organizations default="org-1">
90
- <organization identifier="org-1">
91
- <title>${title}</title>
92
- <item identifier="item-1" identifierref="res-1">
93
- <title>${title}</title>
94
- </item>
95
- </organization>
96
- </organizations>
97
- <resources>
98
- <resource identifier="res-1" type="webcontent" adlcp:${dialect.scormTypeAttr}="sco" href="index.html">
99
- ${fileElements}
100
- </resource>
101
- </resources>
102
- </manifest>`;
103
- }
104
- function generateSCORM12Manifest(config, distDir) {
105
- return generateScormManifest("1.2", config, distDir);
106
- }
107
- function generateSCORM2004Manifest(config, distDir) {
108
- return generateScormManifest("2004", config, distDir);
109
- }
110
- function generateCMI5Xml(config) {
111
- const title = escapeXml(config.title || "Tessera Course");
112
- const description = escapeXml(config.description || "");
113
- const courseId = stableUrn("course", `tessera-course:${config.title || ""}`);
114
- const auId = stableUrn("au", `tessera-au:${config.title || ""}`);
115
- const masteryScore = Number(((config.scoring?.passingScore ?? 70) / 100).toFixed(4));
116
- return `<?xml version="1.0" encoding="UTF-8"?>
117
- <courseStructure xmlns="https://w3id.org/xapi/profiles/cmi5/v1/CourseStructure.xsd">
118
- <course id="${courseId}">
119
- <title><langstring lang="en-US">${title}</langstring></title>
120
- <description><langstring lang="en-US">${description}</langstring></description>
121
- </course>
122
- <au id="${auId}" launchMethod="AnyWindow" moveOn="${config.completion?.mode === "quiz" ? "CompletedAndPassed" : "Completed"}" masteryScore="${masteryScore}">
123
- <title><langstring lang="en-US">${title}</langstring></title>
124
- <description><langstring lang="en-US">${description}</langstring></description>
125
- <url>index.html</url>
126
- </au>
127
- </courseStructure>`;
128
- }
129
- async function createZip(distDir, outputPath) {
130
- return new Promise((res, reject) => {
131
- const output = createWriteStream(outputPath);
132
- const archive = new ZipArchive({ zlib: { level: 9 } });
133
- output.on("close", () => {
134
- res(archive.pointer());
135
- });
136
- output.on("error", reject);
137
- archive.on("error", reject);
138
- archive.pipe(output);
139
- archive.directory(distDir, false);
140
- archive.finalize();
141
- });
142
- }
143
- /**
144
- * Run the export process after Vite build completes.
145
- * Writes manifest XML into dist/, then packages into ZIP if needed.
146
- */
147
- /** Remove any previously built zips for this package to prevent accumulation. */
148
- function cleanOldZips(projectRoot, slug) {
149
- try {
150
- for (const f of readdirSync(projectRoot)) if (f.startsWith(`${slug}-`) && f.endsWith(".zip")) try {
151
- unlinkSync(resolve(projectRoot, f));
152
- } catch {}
153
- } catch {}
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
- };
173
- async function runExport(projectRoot, config) {
174
- const distDir = resolve(projectRoot, "dist");
175
- const standard = config.export?.standard || "web";
176
- const slug = slugify(config.title || "tessera-course") || "tessera-course";
177
- const zipName = `${slug}-${config.version || "1.0.0"}.zip`;
178
- const zipPath = resolve(projectRoot, zipName);
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;
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)})`);
192
- }
193
- //#endregion
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;
204
- return {
205
- name,
206
- enforce: "pre",
207
- configResolved(config) {
208
- filePath = resolve(config.root, projectFile);
209
- },
210
- resolveId(id) {
211
- if (id === virtualId) return resolvedId;
212
- return null;
213
- },
214
- load(id) {
215
- if (id !== resolvedId) return null;
216
- if (existsSync(filePath)) {
217
- this.addWatchFile(filePath);
218
- return `export { default } from '${normalizePath(filePath)}';`;
219
- }
220
- return fallback;
221
- },
222
- configureServer(server) {
223
- server.watcher.on("all", (event, changed) => {
224
- if (changed !== filePath) return;
225
- if (event !== "add" && event !== "unlink") return;
226
- const mod = server.moduleGraph.getModuleById(resolvedId);
227
- if (mod) server.moduleGraph.invalidateModule(mod);
228
- server.ws.send({ type: "full-reload" });
229
- });
230
- }
231
- };
232
- }
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
243
- //#region src/plugin/quiz.ts
244
- const __dirname$1 = dirname(fileURLToPath(import.meta.url));
245
- function tesseraQuizPlugin() {
246
- return createOverridePlugin({
247
- name: "tessera:quiz",
248
- virtualId: "virtual:tessera-quiz",
249
- projectFile: "quiz.svelte",
250
- builtinFile: resolve(resolve(__dirname$1, "..", ".."), "src", "components", "Quiz.svelte")
251
- });
252
- }
253
- //#endregion
254
- //#region src/plugin/index.ts
255
- function isAuditBuild() {
256
- return process.env[AUDIT_ENV_FLAG] === "1";
257
- }
258
- const __dirname = dirname(fileURLToPath(import.meta.url));
259
- function resolveRuntimeDir() {
260
- return resolve(resolve(__dirname, "..", ".."), "src", "runtime");
261
- }
262
- function resolveStylesDir() {
263
- return resolve(resolve(__dirname, "..", ".."), "styles");
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
- }
272
- function tesseraPlugin() {
273
- const manifestRef = {
274
- current: null,
275
- root: ""
276
- };
277
- const a11y = {
278
- warnings: [],
279
- projectRoot: "",
280
- isBuild: false,
281
- settings: normalizeA11y(void 0)
282
- };
283
- return [
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),
303
- tesseraValidationPlugin(),
304
- tesseraEntryPlugin(),
305
- tesseraConfigPlugin(),
306
- tesseraPagesPlugin(),
307
- tesseraManifestPlugin(manifestRef),
308
- tesseraLayoutPlugin(),
309
- tesseraQuizPlugin(),
310
- tesseraAdapterPlugin(),
311
- tesseraXAPISetupPlugin(),
312
- tesseraFirstPagePreloadPlugin(manifestRef),
313
- tesseraExportPlugin()
314
- ];
315
- }
316
- const VIRTUAL_ENTRY_ID = "virtual:tessera-entry";
317
- const RESOLVED_ENTRY_ID = "\0" + VIRTUAL_ENTRY_ID;
318
- const VIRTUAL_MAIN_ID = "/virtual:tessera-main";
319
- const RESOLVED_MAIN_ID = "\0virtual:tessera-main";
320
- function tesseraEntryPlugin() {
321
- const runtimeDir = resolveRuntimeDir();
322
- const stylesDir = resolveStylesDir();
323
- const appSveltePath = resolve(runtimeDir, "App.svelte");
324
- let projectRoot;
325
- let outDir;
326
- let isBuild = false;
327
- return {
328
- name: "tessera:entry",
329
- enforce: "pre",
330
- configResolved(config) {
331
- projectRoot = config.root;
332
- outDir = resolve(config.root, config.build.outDir);
333
- isBuild = config.command === "build";
334
- },
335
- buildStart() {
336
- if (isBuild) writeFileSync(resolve(projectRoot, "index.html"), generateIndexHtml(readLanguage(projectRoot)), "utf-8");
337
- },
338
- closeBundle() {
339
- if (isBuild) {
340
- const htmlPath = resolve(projectRoot, "index.html");
341
- if (existsSync(htmlPath)) try {
342
- unlinkSync(htmlPath);
343
- } catch {}
344
- const assetsDir = resolve(projectRoot, "assets");
345
- const distAssetsDir = resolve(outDir, "assets");
346
- if (existsSync(assetsDir)) {
347
- mkdirSync(distAssetsDir, { recursive: true });
348
- cpSync(assetsDir, distAssetsDir, { recursive: true });
349
- }
350
- }
351
- },
352
- configureServer(server) {
353
- return () => {
354
- server.middlewares.use(async (req, res, next) => {
355
- if (req.url === "/" || req.url === "/index.html") {
356
- const html = generateIndexHtml(readLanguage(projectRoot));
357
- const transformed = await server.transformIndexHtml(req.url, html);
358
- res.setHeader("Content-Type", "text/html");
359
- res.statusCode = 200;
360
- res.end(transformed);
361
- return;
362
- }
363
- next();
364
- });
365
- };
366
- },
367
- resolveId(id) {
368
- if (id === VIRTUAL_ENTRY_ID) return RESOLVED_ENTRY_ID;
369
- if (id === VIRTUAL_MAIN_ID || id === "virtual:tessera-main") return RESOLVED_MAIN_ID;
370
- return null;
371
- },
372
- load(id) {
373
- if (id === RESOLVED_ENTRY_ID || id === RESOLVED_MAIN_ID) return generateEntryScript(appSveltePath, stylesDir, projectRoot);
374
- return null;
375
- }
376
- };
377
- }
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) {
384
- return `<!DOCTYPE html>
385
- <html lang="${lang}">
386
- <head>
387
- <meta charset="UTF-8" />
388
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
389
- <title>Tessera Course</title>
390
- </head>
391
- <body>
392
- <div id="tessera-root"></div>
393
- <script type="module" src="/virtual:tessera-main"><\/script>
394
- </body>
395
- </html>`;
396
- }
397
- function generateEntryScript(appSveltePath, frameworkStylesDir, projectRoot) {
398
- const normalizedPath = appSveltePath.replace(/\\/g, "/");
399
- const frameworkImports = [
400
- "theme.css",
401
- "base.css",
402
- "layout.css"
403
- ].map((file) => resolve(frameworkStylesDir, file).replace(/\\/g, "/")).filter((path) => existsSync(path)).map((path) => `import '${path}';`).join("\n");
404
- const userStylesDir = resolve(projectRoot, "styles");
405
- let userImports = "";
406
- if (existsSync(userStylesDir)) userImports = readdirSync(userStylesDir).filter((f) => f.endsWith(".css")).sort().map((f) => resolve(userStylesDir, f).replace(/\\/g, "/")).map((path) => `import '${path}';`).join("\n");
407
- return `// Framework styles
408
- ${frameworkImports}
409
- // User styles
410
- ${userImports}
411
-
412
- import { mount } from 'svelte';
413
- import App from '${normalizedPath}';
414
-
415
- mount(App, {
416
- target: document.getElementById('tessera-root'),
417
- });
418
- `;
419
- }
420
- const VIRTUAL_CONFIG_ID = "virtual:tessera-config";
421
- const RESOLVED_CONFIG_ID = "\0" + VIRTUAL_CONFIG_ID;
422
- function completionDefaults(mode) {
423
- if (mode === "manual") return {
424
- completion: { mode: "manual" },
425
- passingScore: 0
426
- };
427
- return {
428
- completion: {
429
- mode: "percentage",
430
- percentageThreshold: 100
431
- },
432
- passingScore: 70
433
- };
434
- }
435
- function tesseraConfigPlugin() {
436
- let projectRoot;
437
- return {
438
- name: "tessera:config",
439
- enforce: "pre",
440
- config(config) {
441
- return {
442
- base: "./",
443
- build: { assetsDir: "tessera" },
444
- resolve: { alias: { $assets: resolve(config.root || process.cwd(), "assets") } },
445
- optimizeDeps: { exclude: ["tessera-learn"] }
446
- };
447
- },
448
- configResolved(config) {
449
- projectRoot = config.root;
450
- },
451
- resolveId(id) {
452
- if (id === VIRTUAL_CONFIG_ID) return RESOLVED_CONFIG_ID;
453
- return null;
454
- },
455
- load(id) {
456
- if (id === RESOLVED_CONFIG_ID) {
457
- const configPath = resolve(projectRoot, "course.config.js");
458
- if (existsSync(configPath)) this.addWatchFile(configPath);
459
- const read = readCourseConfig(projectRoot);
460
- const userConfig = read.ok ? read.config : {};
461
- const { completion, passingScore } = completionDefaults(userConfig.completion?.mode);
462
- const merged = {
463
- title: userConfig.title || "Untitled Course",
464
- ...userConfig,
465
- navigation: {
466
- mode: "free",
467
- ...userConfig.navigation
468
- },
469
- completion: {
470
- ...completion,
471
- ...userConfig.completion
472
- },
473
- scoring: {
474
- passingScore,
475
- ...userConfig.scoring
476
- },
477
- export: {
478
- standard: "web",
479
- ...userConfig.export
480
- }
481
- };
482
- return `export default ${JSON.stringify(merged)};`;
483
- }
484
- return null;
485
- }
486
- };
487
- }
488
- /** Register all _meta.js and .svelte files under pagesDir as watch files for build mode. */
489
- function addWatchFiles(ctx, dir) {
490
- if (!existsSync(dir)) return;
491
- for (const entry of readdirSync(dir)) {
492
- const full = resolve(dir, entry);
493
- if (statSync(full).isDirectory()) addWatchFiles(ctx, full);
494
- else if (entry.endsWith(".svelte") || entry === "_meta.js") ctx.addWatchFile(full);
495
- }
496
- }
497
- const VIRTUAL_PAGES_ID = "virtual:tessera-pages";
498
- const RESOLVED_PAGES_ID = "\0" + VIRTUAL_PAGES_ID;
499
- /**
500
- * Provides a virtual module that exports an import.meta.glob map for all .svelte
501
- * pages. This runs in the user's project context so the glob resolves against their
502
- * pages/ directory, and Vite can statically analyze it for code splitting.
503
- */
504
- function tesseraPagesPlugin() {
505
- return {
506
- name: "tessera:pages",
507
- enforce: "pre",
508
- resolveId(id) {
509
- if (id === VIRTUAL_PAGES_ID) return RESOLVED_PAGES_ID;
510
- return null;
511
- },
512
- load(id) {
513
- if (id === RESOLVED_PAGES_ID) return `export default import.meta.glob('/pages/**/*.svelte');`;
514
- return null;
515
- }
516
- };
517
- }
518
- function tesseraValidationPlugin() {
519
- let projectRoot;
520
- let isBuild = false;
521
- return {
522
- name: "tessera:validation",
523
- enforce: "pre",
524
- configResolved(config) {
525
- projectRoot = config.root;
526
- isBuild = config.command === "build";
527
- if (!isBuild) runValidation(projectRoot);
528
- },
529
- buildStart() {
530
- if (isBuild) runValidation(projectRoot);
531
- }
532
- };
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
- }
564
- function runValidation(projectRoot) {
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.`);
568
- }
569
- function tesseraExportPlugin() {
570
- let projectRoot;
571
- let isBuild = false;
572
- return {
573
- name: "tessera:export",
574
- enforce: "post",
575
- configResolved(config) {
576
- projectRoot = config.root;
577
- isBuild = config.command === "build";
578
- },
579
- async closeBundle() {
580
- if (!isBuild) return;
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}`);
587
- }
588
- await runExport(projectRoot, read.config);
589
- }
590
- };
591
- }
592
- const VIRTUAL_MANIFEST_ID = "virtual:tessera-manifest";
593
- const RESOLVED_MANIFEST_ID = "\0" + VIRTUAL_MANIFEST_ID;
594
- function tesseraManifestPlugin(manifestRef) {
595
- let projectRoot;
596
- let pagesDir;
597
- function buildManifest() {
598
- const m = generateManifest(pagesDir);
599
- manifestRef.current = m;
600
- return m;
601
- }
602
- return {
603
- name: "tessera:manifest",
604
- enforce: "pre",
605
- configResolved(config) {
606
- projectRoot = config.root;
607
- pagesDir = resolve(projectRoot, "pages");
608
- manifestRef.root = projectRoot;
609
- },
610
- configureServer(devServer) {
611
- devServer.watcher.on("all", (event, filePath) => {
612
- if (!filePath.startsWith(pagesDir)) return;
613
- if (filePath.endsWith(".svelte") || filePath.endsWith("_meta.js") || event === "addDir" || event === "unlinkDir") {
614
- manifestRef.current = null;
615
- const mod = devServer.moduleGraph.getModuleById(RESOLVED_MANIFEST_ID);
616
- if (mod) {
617
- devServer.moduleGraph.invalidateModule(mod);
618
- devServer.ws.send({ type: "full-reload" });
619
- }
620
- console.log(`[tessera] Manifest rebuilt (${event}: ${filePath.replace(projectRoot, "")})`);
621
- }
622
- });
623
- },
624
- buildStart() {
625
- buildManifest();
626
- },
627
- resolveId(id) {
628
- if (id === VIRTUAL_MANIFEST_ID) return RESOLVED_MANIFEST_ID;
629
- return null;
630
- },
631
- load(id) {
632
- if (id === RESOLVED_MANIFEST_ID) {
633
- if (!manifestRef.current) buildManifest();
634
- addWatchFiles(this, pagesDir);
635
- const json = JSON.stringify(manifestRef.current, (_key, value) => value === Infinity ? 1e9 : value);
636
- return `export default JSON.parse(atob("${Buffer.from(json).toString("base64")}"));`;
637
- }
638
- return null;
639
- }
640
- };
641
- }
642
- const VIRTUAL_ADAPTER_ID = "virtual:tessera-adapter";
643
- const RESOLVED_ADAPTER_ID = "\0" + VIRTUAL_ADAPTER_ID;
644
- function tesseraAdapterPlugin() {
645
- let projectRoot;
646
- let isBuild = false;
647
- return {
648
- name: "tessera:adapter",
649
- enforce: "pre",
650
- configResolved(config) {
651
- projectRoot = config.root;
652
- isBuild = config.command === "build";
653
- },
654
- resolveId(id) {
655
- if (id === VIRTUAL_ADAPTER_ID) return RESOLVED_ADAPTER_ID;
656
- return null;
657
- },
658
- load(id) {
659
- if (id !== RESOLVED_ADAPTER_ID) return null;
660
- if (!isBuild) return `export { createAdapter } from 'tessera-learn/runtime/adapters/index.js';`;
661
- let standard = "web";
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";
665
- switch (standard) {
666
- case "scorm12": return `
667
- import { SCORM12Adapter } from 'tessera-learn/runtime/adapters/scorm12.js';
668
- import { findSCORM12API } from 'tessera-learn/runtime/adapters/discovery.js';
669
- import { LMSAdapterError } from 'tessera-learn/runtime/adapters/index.js';
670
- export function createAdapter() {
671
- const api = findSCORM12API();
672
- 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.');
673
- return new SCORM12Adapter(api);
674
- }
675
- `;
676
- case "scorm2004": return `
677
- import { SCORM2004Adapter } from 'tessera-learn/runtime/adapters/scorm2004.js';
678
- import { findSCORM2004API } from 'tessera-learn/runtime/adapters/discovery.js';
679
- import { LMSAdapterError } from 'tessera-learn/runtime/adapters/index.js';
680
- export function createAdapter() {
681
- const api = findSCORM2004API();
682
- 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.');
683
- return new SCORM2004Adapter(api);
684
- }
685
- `;
686
- case "cmi5": return `
687
- import { CMI5Adapter } from 'tessera-learn/runtime/adapters/cmi5.js';
688
- import { hasCMI5LaunchParams } from 'tessera-learn/runtime/adapters/discovery.js';
689
- import { LMSAdapterError } from 'tessera-learn/runtime/adapters/index.js';
690
- export function createAdapter() {
691
- if (!hasCMI5LaunchParams()) throw new LMSAdapterError('cmi5', 'Tessera: cmi5 launch parameters not present on URL. Course must be launched from a cmi5-compliant LMS.');
692
- return new CMI5Adapter();
693
- }
694
- `;
695
- default: return `
696
- import { WebAdapter } from 'tessera-learn/runtime/adapters/web.js';
697
- export function createAdapter(config) {
698
- return new WebAdapter(config);
699
- }
700
- `;
701
- }
702
- }
703
- };
704
- }
705
- const VIRTUAL_XAPI_SETUP_ID = "virtual:tessera-xapi-setup";
706
- const RESOLVED_XAPI_SETUP_ID = "\0" + VIRTUAL_XAPI_SETUP_ID;
707
- function tesseraXAPISetupPlugin() {
708
- let projectRoot;
709
- let isBuild = false;
710
- return {
711
- name: "tessera:xapi-setup",
712
- enforce: "pre",
713
- configResolved(config) {
714
- projectRoot = config.root;
715
- isBuild = config.command === "build";
716
- },
717
- resolveId(id) {
718
- if (id === VIRTUAL_XAPI_SETUP_ID) return RESOLVED_XAPI_SETUP_ID;
719
- return null;
720
- },
721
- load(id) {
722
- if (id !== RESOLVED_XAPI_SETUP_ID) return null;
723
- if (!isBuild) return `export { buildXAPIClient } from 'tessera-learn/runtime/xapi/setup.js';`;
724
- if (isAuditBuild()) return `export async function buildXAPIClient() { return null; }`;
725
- let standard = "web";
726
- let hasXapi = false;
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;
731
- }
732
- if (hasXapi || standard === "cmi5") return `export { buildXAPIClient } from 'tessera-learn/runtime/xapi/setup.js';`;
733
- return `export async function buildXAPIClient() { return null; }`;
734
- }
735
- };
736
- }
737
- function tesseraFirstPagePreloadPlugin(manifestRef) {
738
- return {
739
- name: "tessera:first-page-preload",
740
- apply: "build",
741
- transformIndexHtml: {
742
- order: "post",
743
- handler(_html, ctx) {
744
- const firstPagePath = manifestRef.current?.pages[0]?.importPath;
745
- if (!firstPagePath || !ctx.bundle) return;
746
- const normalized = resolve(manifestRef.root, firstPagePath.replace(/^\//, "")).replace(/\\/g, "/");
747
- const chunk = Object.values(ctx.bundle).find((c) => c.type === "chunk" && !!c.facadeModuleId && c.facadeModuleId.replace(/\\/g, "/") === normalized);
748
- if (!chunk) return;
749
- return [{
750
- tag: "link",
751
- attrs: {
752
- rel: "modulepreload",
753
- href: `./${chunk.fileName}`
754
- },
755
- injectTo: "head"
756
- }];
757
- }
758
- }
759
- };
760
- }
761
- //#endregion
1
+ import { n as runAudit } from "../audit-BA5o0ick.js";
2
+ import { t as tesseraPlugin } from "../plugin-W_rk3Pit.js";
762
3
  export { runAudit, tesseraPlugin };
763
-
764
- //# sourceMappingURL=index.js.map