tessera-learn 0.2.3 → 0.4.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.
- package/AGENTS.md +50 -21
- package/README.md +2 -2
- package/dist/{audit--fSWIOgK.js → audit-DsYqXbqm.js} +282 -197
- package/dist/audit-DsYqXbqm.js.map +1 -0
- package/dist/{build-commands-Qyrlsp3n.js → build-commands-BFuiAxaR.js} +4 -4
- package/dist/build-commands-BFuiAxaR.js.map +1 -0
- package/dist/{inline-config-DqAKsCNl.js → inline-config-DVvOCKht.js} +6 -6
- package/dist/inline-config-DVvOCKht.js.map +1 -0
- package/dist/plugin/cli.d.ts +5 -1
- package/dist/plugin/cli.d.ts.map +1 -1
- package/dist/plugin/cli.js +91 -49
- package/dist/plugin/cli.js.map +1 -1
- package/dist/plugin/index.d.ts +287 -2
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +3 -3
- package/dist/{plugin-B-aiL9-V.js → plugin-BuMiDTmU.js} +145 -111
- package/dist/plugin-BuMiDTmU.js.map +1 -0
- package/package.json +7 -7
- package/src/components/DefaultLayout.svelte +2 -5
- package/src/components/MultipleChoice.svelte +1 -2
- package/src/components/Quiz.svelte +18 -26
- package/src/plugin/ast.ts +9 -2
- package/src/plugin/build-commands.ts +7 -4
- package/src/plugin/cli.ts +96 -46
- package/src/plugin/csp.ts +59 -0
- package/src/plugin/duplicate-cli.ts +37 -1
- package/src/plugin/export.ts +56 -27
- package/src/plugin/index.ts +138 -93
- package/src/plugin/inline-config.ts +4 -2
- package/src/plugin/manifest.ts +24 -23
- package/src/plugin/new-cli.ts +2 -0
- package/src/plugin/validate-cli.ts +5 -2
- package/src/plugin/validation.ts +255 -238
- package/src/runtime/App.svelte +14 -9
- package/src/runtime/Sidebar.svelte +3 -1
- package/src/runtime/adapters/cmi5.ts +59 -402
- package/src/runtime/adapters/discovery.ts +11 -0
- package/src/runtime/adapters/index.ts +27 -60
- package/src/runtime/adapters/lms-error.ts +61 -0
- package/src/runtime/adapters/scorm-base.ts +15 -14
- package/src/runtime/adapters/scorm12.ts +6 -25
- package/src/runtime/adapters/scorm2004.ts +12 -54
- package/src/runtime/adapters/web.ts +11 -4
- package/src/runtime/adapters/xapi-launch-base.ts +346 -0
- package/src/runtime/adapters/xapi.ts +26 -0
- package/src/runtime/fingerprint.ts +28 -0
- package/src/runtime/interaction-format.ts +0 -1
- package/src/runtime/persistence.ts +4 -0
- package/src/runtime/types.ts +22 -1
- package/src/runtime/xapi/publisher.ts +16 -15
- package/src/runtime/xapi/setup.ts +24 -15
- package/src/virtual.d.ts +4 -1
- package/templates/course/course.config.js +1 -0
- package/dist/audit--fSWIOgK.js.map +0 -1
- package/dist/build-commands-Qyrlsp3n.js.map +0 -1
- package/dist/inline-config-DqAKsCNl.js.map +0 -1
- package/dist/plugin-B-aiL9-V.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as isIgnored, c as reportValidationIssues, d as courseIdentity, f as generateManifest, l as validateProject, m as readResolvedConfig, o as isPlausibleLanguageTag, p as readCourseConfig, r as resolvePackageRoot, s as normalizeA11y, t as AUDIT_ENV_FLAG, u as buildCsp } from "./audit-DsYqXbqm.js";
|
|
2
2
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
|
3
3
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
4
4
|
import { cpSync, createWriteStream, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -19,6 +19,7 @@ function slugify(text) {
|
|
|
19
19
|
}
|
|
20
20
|
//#endregion
|
|
21
21
|
//#region src/plugin/export.ts
|
|
22
|
+
const UNTITLED_TITLE = "Untitled Course";
|
|
22
23
|
function escapeXml(str) {
|
|
23
24
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
24
25
|
}
|
|
@@ -28,10 +29,9 @@ function escapeXml(str) {
|
|
|
28
29
|
function collectFiles(dir, base = "") {
|
|
29
30
|
const files = [];
|
|
30
31
|
if (!existsSync(dir)) return files;
|
|
31
|
-
for (const entry of readdirSync(dir)) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
if (statSync(fullPath).isDirectory()) files.push(...collectFiles(fullPath, relPath));
|
|
32
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
33
|
+
const relPath = base ? `${base}/${entry.name}` : entry.name;
|
|
34
|
+
if (entry.isSymbolicLink() ? statSync(resolve(dir, entry.name)).isDirectory() : entry.isDirectory()) files.push(...collectFiles(resolve(dir, entry.name), relPath));
|
|
35
35
|
else files.push(relPath);
|
|
36
36
|
}
|
|
37
37
|
return files;
|
|
@@ -49,6 +49,10 @@ function collectFiles(dir, base = "") {
|
|
|
49
49
|
function stableUrn(kind, seed) {
|
|
50
50
|
return `urn:tessera:${kind}:${createHash("sha256").update(seed).digest("hex").slice(0, 32)}`;
|
|
51
51
|
}
|
|
52
|
+
function auIdFor(config) {
|
|
53
|
+
const id = courseIdentity(config);
|
|
54
|
+
return stableUrn("au", id ? `${id}#au` : "tessera-au");
|
|
55
|
+
}
|
|
52
56
|
function formatSize(bytes) {
|
|
53
57
|
if (bytes < 1024) return `${bytes} B`;
|
|
54
58
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -72,7 +76,7 @@ const SCORM_DIALECTS = {
|
|
|
72
76
|
};
|
|
73
77
|
function generateScormManifest(version, config, distDir) {
|
|
74
78
|
const dialect = SCORM_DIALECTS[version];
|
|
75
|
-
const title = escapeXml(config.title ||
|
|
79
|
+
const title = escapeXml(config.title || UNTITLED_TITLE);
|
|
76
80
|
const fileElements = collectFiles(distDir).map((f) => ` <file href="${escapeXml(f)}" />`).join("\n");
|
|
77
81
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
78
82
|
<manifest identifier="tessera-course" version="1.0"
|
|
@@ -99,17 +103,11 @@ ${fileElements}
|
|
|
99
103
|
</resources>
|
|
100
104
|
</manifest>`;
|
|
101
105
|
}
|
|
102
|
-
function generateSCORM12Manifest(config, distDir) {
|
|
103
|
-
return generateScormManifest("1.2", config, distDir);
|
|
104
|
-
}
|
|
105
|
-
function generateSCORM2004Manifest(config, distDir) {
|
|
106
|
-
return generateScormManifest("2004", config, distDir);
|
|
107
|
-
}
|
|
108
106
|
function generateCMI5Xml(config) {
|
|
109
|
-
const title = escapeXml(config.title ||
|
|
107
|
+
const title = escapeXml(config.title || UNTITLED_TITLE);
|
|
110
108
|
const description = escapeXml(config.description || "");
|
|
111
|
-
const courseId = stableUrn("course",
|
|
112
|
-
const auId =
|
|
109
|
+
const courseId = stableUrn("course", courseIdentity(config) || "tessera-course");
|
|
110
|
+
const auId = auIdFor(config);
|
|
113
111
|
const masteryScore = Number(((config.scoring?.passingScore ?? 70) / 100).toFixed(4));
|
|
114
112
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
115
113
|
<courseStructure xmlns="https://w3id.org/xapi/profiles/cmi5/v1/CourseStructure.xsd">
|
|
@@ -124,6 +122,20 @@ function generateCMI5Xml(config) {
|
|
|
124
122
|
</au>
|
|
125
123
|
</courseStructure>`;
|
|
126
124
|
}
|
|
125
|
+
function generateTincanXml(config) {
|
|
126
|
+
const title = escapeXml(config.title || UNTITLED_TITLE);
|
|
127
|
+
const description = escapeXml(config.description || "");
|
|
128
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
129
|
+
<tincan xmlns="http://projecttincan.com/tincan.xsd">
|
|
130
|
+
<activities>
|
|
131
|
+
<activity id="${auIdFor(config)}" type="http://adlnet.gov/expapi/activities/course">
|
|
132
|
+
<name>${title}</name>
|
|
133
|
+
<description lang="en-US">${description}</description>
|
|
134
|
+
<launch lang="en-US">index.html</launch>
|
|
135
|
+
</activity>
|
|
136
|
+
</activities>
|
|
137
|
+
</tincan>`;
|
|
138
|
+
}
|
|
127
139
|
async function createZip(distDir, outputPath) {
|
|
128
140
|
return new Promise((res, reject) => {
|
|
129
141
|
const output = createWriteStream(outputPath);
|
|
@@ -155,17 +167,22 @@ const PACKAGED_EXPORTS = {
|
|
|
155
167
|
scorm12: {
|
|
156
168
|
manifestFile: "imsmanifest.xml",
|
|
157
169
|
label: "SCORM 1.2",
|
|
158
|
-
generate:
|
|
170
|
+
generate: (config, distDir) => generateScormManifest("1.2", config, distDir)
|
|
159
171
|
},
|
|
160
172
|
scorm2004: {
|
|
161
173
|
manifestFile: "imsmanifest.xml",
|
|
162
174
|
label: "SCORM 2004",
|
|
163
|
-
generate:
|
|
175
|
+
generate: (config, distDir) => generateScormManifest("2004", config, distDir)
|
|
164
176
|
},
|
|
165
177
|
cmi5: {
|
|
166
178
|
manifestFile: "cmi5.xml",
|
|
167
179
|
label: "CMI5",
|
|
168
180
|
generate: (config) => generateCMI5Xml(config)
|
|
181
|
+
},
|
|
182
|
+
xapi: {
|
|
183
|
+
manifestFile: "tincan.xml",
|
|
184
|
+
label: "xAPI 1.0.3",
|
|
185
|
+
generate: (config) => generateTincanXml(config)
|
|
169
186
|
}
|
|
170
187
|
};
|
|
171
188
|
async function runExport(projectRoot, config) {
|
|
@@ -287,7 +304,8 @@ function virtualModule(name, virtualId, load) {
|
|
|
287
304
|
}
|
|
288
305
|
};
|
|
289
306
|
}
|
|
290
|
-
function tesseraPlugin() {
|
|
307
|
+
function tesseraPlugin(options = {}) {
|
|
308
|
+
const { standardOverride } = options;
|
|
291
309
|
const manifestRef = {
|
|
292
310
|
current: null,
|
|
293
311
|
root: ""
|
|
@@ -318,25 +336,25 @@ function tesseraPlugin() {
|
|
|
318
336
|
}
|
|
319
337
|
}),
|
|
320
338
|
tesseraA11yCompilerPlugin(a11y),
|
|
321
|
-
tesseraValidationPlugin(),
|
|
322
|
-
tesseraEntryPlugin(),
|
|
339
|
+
tesseraValidationPlugin(standardOverride),
|
|
340
|
+
tesseraEntryPlugin(standardOverride),
|
|
323
341
|
tesseraConfigDefaultsPlugin(),
|
|
324
|
-
tesseraConfigPlugin(),
|
|
342
|
+
tesseraConfigPlugin(standardOverride),
|
|
325
343
|
tesseraPagesPlugin(),
|
|
326
344
|
tesseraManifestPlugin(manifestRef),
|
|
327
345
|
tesseraLayoutPlugin(),
|
|
328
346
|
tesseraQuizPlugin(),
|
|
329
|
-
tesseraAdapterPlugin(),
|
|
330
|
-
tesseraXAPISetupPlugin(),
|
|
347
|
+
tesseraAdapterPlugin(standardOverride),
|
|
348
|
+
tesseraXAPISetupPlugin(standardOverride),
|
|
331
349
|
tesseraFirstPagePreloadPlugin(manifestRef),
|
|
332
|
-
tesseraExportPlugin()
|
|
350
|
+
tesseraExportPlugin(standardOverride)
|
|
333
351
|
];
|
|
334
352
|
}
|
|
335
353
|
const VIRTUAL_ENTRY_ID = "virtual:tessera-entry";
|
|
336
354
|
const RESOLVED_ENTRY_ID = "\0virtual:tessera-entry";
|
|
337
355
|
const VIRTUAL_MAIN_ID = "/virtual:tessera-main";
|
|
338
356
|
const RESOLVED_MAIN_ID = "\0virtual:tessera-main";
|
|
339
|
-
function tesseraEntryPlugin() {
|
|
357
|
+
function tesseraEntryPlugin(standardOverride) {
|
|
340
358
|
const runtimeDir = resolveRuntimeDir();
|
|
341
359
|
const stylesDir = resolveStylesDir();
|
|
342
360
|
const appSveltePath = resolve(runtimeDir, "App.svelte");
|
|
@@ -352,7 +370,10 @@ function tesseraEntryPlugin() {
|
|
|
352
370
|
isBuild = config.command === "build";
|
|
353
371
|
},
|
|
354
372
|
buildStart() {
|
|
355
|
-
if (isBuild)
|
|
373
|
+
if (isBuild) {
|
|
374
|
+
const read = readResolvedConfig(projectRoot, standardOverride);
|
|
375
|
+
writeFileSync(resolve(projectRoot, "index.html"), generateIndexHtml(readLanguage(read), cspMeta(read)), "utf-8");
|
|
376
|
+
}
|
|
356
377
|
},
|
|
357
378
|
closeBundle() {
|
|
358
379
|
if (isBuild) {
|
|
@@ -372,7 +393,7 @@ function tesseraEntryPlugin() {
|
|
|
372
393
|
return () => {
|
|
373
394
|
server.middlewares.use(async (req, res, next) => {
|
|
374
395
|
if (req.url === "/" || req.url === "/index.html") {
|
|
375
|
-
const html = generateIndexHtml(readLanguage(projectRoot));
|
|
396
|
+
const html = generateIndexHtml(readLanguage(readCourseConfig(projectRoot)));
|
|
376
397
|
const transformed = await server.transformIndexHtml(req.url, html);
|
|
377
398
|
res.setHeader("Content-Type", "text/html");
|
|
378
399
|
res.statusCode = 200;
|
|
@@ -394,17 +415,22 @@ function tesseraEntryPlugin() {
|
|
|
394
415
|
}
|
|
395
416
|
};
|
|
396
417
|
}
|
|
397
|
-
function readLanguage(
|
|
398
|
-
const read = readCourseConfig(projectRoot);
|
|
418
|
+
function readLanguage(read) {
|
|
399
419
|
const lang = read.ok ? read.config.language : void 0;
|
|
400
420
|
return isPlausibleLanguageTag(lang) ? lang : "en";
|
|
401
421
|
}
|
|
402
|
-
function
|
|
422
|
+
function cspMeta(read) {
|
|
423
|
+
if (read.standard !== "web") return "";
|
|
424
|
+
const csp = read.ok ? read.config.export?.csp : void 0;
|
|
425
|
+
if (csp === false) return "";
|
|
426
|
+
return `\n <meta http-equiv="Content-Security-Policy" content="${buildCsp(csp)}" />`;
|
|
427
|
+
}
|
|
428
|
+
function generateIndexHtml(lang, csp = "") {
|
|
403
429
|
return `<!DOCTYPE html>
|
|
404
430
|
<html lang="${lang}">
|
|
405
431
|
<head>
|
|
406
432
|
<meta charset="UTF-8" />
|
|
407
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"
|
|
433
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />${csp}
|
|
408
434
|
<title>Tessera Course</title>
|
|
409
435
|
</head>
|
|
410
436
|
<body>
|
|
@@ -442,6 +468,10 @@ function completionDefaults(mode) {
|
|
|
442
468
|
completion: { mode: "manual" },
|
|
443
469
|
passingScore: 0
|
|
444
470
|
};
|
|
471
|
+
if (mode === "quiz") return {
|
|
472
|
+
completion: { mode: "quiz" },
|
|
473
|
+
passingScore: 70
|
|
474
|
+
};
|
|
445
475
|
return {
|
|
446
476
|
completion: {
|
|
447
477
|
mode: "percentage",
|
|
@@ -464,34 +494,38 @@ function tesseraConfigDefaultsPlugin() {
|
|
|
464
494
|
}
|
|
465
495
|
};
|
|
466
496
|
}
|
|
467
|
-
|
|
497
|
+
/** Fill runtime defaults into a parsed course.config.js. Exported for tests. */
|
|
498
|
+
function mergeCourseConfig(userConfig) {
|
|
499
|
+
const { completion, passingScore } = completionDefaults(userConfig.completion?.mode);
|
|
500
|
+
return {
|
|
501
|
+
...userConfig,
|
|
502
|
+
title: userConfig.title || "Untitled Course",
|
|
503
|
+
resume: userConfig.resume ?? "auto",
|
|
504
|
+
navigation: {
|
|
505
|
+
mode: "free",
|
|
506
|
+
...userConfig.navigation
|
|
507
|
+
},
|
|
508
|
+
completion: {
|
|
509
|
+
...completion,
|
|
510
|
+
...userConfig.completion
|
|
511
|
+
},
|
|
512
|
+
scoring: {
|
|
513
|
+
passingScore,
|
|
514
|
+
...userConfig.scoring
|
|
515
|
+
},
|
|
516
|
+
export: {
|
|
517
|
+
standard: "web",
|
|
518
|
+
...userConfig.export
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function tesseraConfigPlugin(standardOverride) {
|
|
468
523
|
return virtualModule("tessera:config", VIRTUAL_CONFIG_ID, function({ projectRoot }) {
|
|
469
524
|
const configPath = resolve(projectRoot, "course.config.js");
|
|
470
525
|
if (existsSync(configPath)) this.addWatchFile(configPath);
|
|
471
|
-
const read =
|
|
526
|
+
const read = readResolvedConfig(projectRoot, standardOverride);
|
|
472
527
|
const userConfig = read.ok ? read.config : {};
|
|
473
|
-
|
|
474
|
-
const merged = {
|
|
475
|
-
title: userConfig.title || "Untitled Course",
|
|
476
|
-
...userConfig,
|
|
477
|
-
navigation: {
|
|
478
|
-
mode: "free",
|
|
479
|
-
...userConfig.navigation
|
|
480
|
-
},
|
|
481
|
-
completion: {
|
|
482
|
-
...completion,
|
|
483
|
-
...userConfig.completion
|
|
484
|
-
},
|
|
485
|
-
scoring: {
|
|
486
|
-
passingScore,
|
|
487
|
-
...userConfig.scoring
|
|
488
|
-
},
|
|
489
|
-
export: {
|
|
490
|
-
standard: "web",
|
|
491
|
-
...userConfig.export
|
|
492
|
-
}
|
|
493
|
-
};
|
|
494
|
-
return `export default ${JSON.stringify(merged)};`;
|
|
528
|
+
return `export default ${JSON.stringify(mergeCourseConfig(userConfig))};`;
|
|
495
529
|
});
|
|
496
530
|
}
|
|
497
531
|
/** Register all _meta.js and .svelte files under pagesDir as watch files for build mode. */
|
|
@@ -514,7 +548,7 @@ function tesseraPagesPlugin() {
|
|
|
514
548
|
return `export default import.meta.glob('/pages/**/*.svelte');`;
|
|
515
549
|
});
|
|
516
550
|
}
|
|
517
|
-
function tesseraValidationPlugin() {
|
|
551
|
+
function tesseraValidationPlugin(standardOverride) {
|
|
518
552
|
let projectRoot;
|
|
519
553
|
let isBuild = false;
|
|
520
554
|
return {
|
|
@@ -523,10 +557,10 @@ function tesseraValidationPlugin() {
|
|
|
523
557
|
configResolved(config) {
|
|
524
558
|
projectRoot = config.root;
|
|
525
559
|
isBuild = config.command === "build";
|
|
526
|
-
if (!isBuild) runValidation(projectRoot);
|
|
560
|
+
if (!isBuild) runValidation(projectRoot, standardOverride);
|
|
527
561
|
},
|
|
528
562
|
buildStart() {
|
|
529
|
-
if (isBuild) runValidation(projectRoot);
|
|
563
|
+
if (isBuild) runValidation(projectRoot, standardOverride);
|
|
530
564
|
}
|
|
531
565
|
};
|
|
532
566
|
}
|
|
@@ -560,12 +594,12 @@ function tesseraA11yCompilerPlugin(a11y) {
|
|
|
560
594
|
}
|
|
561
595
|
};
|
|
562
596
|
}
|
|
563
|
-
function runValidation(projectRoot) {
|
|
564
|
-
const result = validateProject(projectRoot);
|
|
597
|
+
function runValidation(projectRoot, standardOverride) {
|
|
598
|
+
const result = validateProject(projectRoot, standardOverride);
|
|
565
599
|
reportValidationIssues(result);
|
|
566
600
|
if (result.errors.length > 0) throw new Error(`Tessera validation failed with ${result.errors.length} error(s). Fix the errors above to continue.`);
|
|
567
601
|
}
|
|
568
|
-
function tesseraExportPlugin() {
|
|
602
|
+
function tesseraExportPlugin(standardOverride) {
|
|
569
603
|
let projectRoot;
|
|
570
604
|
let isBuild = false;
|
|
571
605
|
return {
|
|
@@ -578,7 +612,7 @@ function tesseraExportPlugin() {
|
|
|
578
612
|
async closeBundle() {
|
|
579
613
|
if (!isBuild) return;
|
|
580
614
|
if (isAuditBuild()) return;
|
|
581
|
-
const read =
|
|
615
|
+
const read = readResolvedConfig(projectRoot, standardOverride);
|
|
582
616
|
if (!read.ok) {
|
|
583
617
|
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.");
|
|
584
618
|
if (read.reason === "no-export") throw new Error("[tessera:export] course.config.js: could not locate `export default { ... }`. Cannot determine export.standard.");
|
|
@@ -632,72 +666,72 @@ function tesseraManifestPlugin(manifestRef) {
|
|
|
632
666
|
if (!manifestRef.current) buildManifest();
|
|
633
667
|
addWatchFiles(this, pagesDir);
|
|
634
668
|
const json = JSON.stringify(manifestRef.current, (_key, value) => value === Infinity ? 1e9 : value);
|
|
635
|
-
return `export default JSON.parse(atob("${Buffer.from(json).toString("base64")}"));`;
|
|
669
|
+
return `export default JSON.parse(new TextDecoder().decode(Uint8Array.from(atob("${Buffer.from(json).toString("base64")}"),(c)=>c.charCodeAt(0))));`;
|
|
636
670
|
}
|
|
637
671
|
return null;
|
|
638
672
|
}
|
|
639
673
|
};
|
|
640
674
|
}
|
|
641
675
|
const VIRTUAL_ADAPTER_ID = "virtual:tessera-adapter";
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
676
|
+
const LMS_ADAPTER_GEN = {
|
|
677
|
+
scorm12: {
|
|
678
|
+
adapter: "SCORM12Adapter",
|
|
679
|
+
module: "scorm12",
|
|
680
|
+
detect: "findSCORM12API",
|
|
681
|
+
takesApi: true
|
|
682
|
+
},
|
|
683
|
+
scorm2004: {
|
|
684
|
+
adapter: "SCORM2004Adapter",
|
|
685
|
+
module: "scorm2004",
|
|
686
|
+
detect: "findSCORM2004API",
|
|
687
|
+
takesApi: true
|
|
688
|
+
},
|
|
689
|
+
cmi5: {
|
|
690
|
+
adapter: "CMI5Adapter",
|
|
691
|
+
module: "cmi5",
|
|
692
|
+
detect: "hasCMI5LaunchParams",
|
|
693
|
+
takesApi: false
|
|
694
|
+
},
|
|
695
|
+
xapi: {
|
|
696
|
+
adapter: "XAPIAdapter",
|
|
697
|
+
module: "xapi",
|
|
698
|
+
detect: "hasXAPILaunchParams",
|
|
699
|
+
takesApi: false
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
function generateLmsAdapterModule(standard) {
|
|
703
|
+
const { adapter, module, detect, takesApi } = LMS_ADAPTER_GEN[standard];
|
|
704
|
+
return `
|
|
705
|
+
import { ${adapter} } from 'tessera-learn/runtime/adapters/${module}.js';
|
|
706
|
+
import { ${detect} } from 'tessera-learn/runtime/adapters/discovery.js';
|
|
707
|
+
import { missingApiError } from 'tessera-learn/runtime/adapters/lms-error.js';
|
|
664
708
|
export function createAdapter() {
|
|
665
|
-
const api =
|
|
666
|
-
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.');
|
|
667
|
-
return new SCORM2004Adapter(api);
|
|
709
|
+
${takesApi ? `const api = ${detect}();\n if (!api) throw missingApiError('${standard}');\n return new ${adapter}(api);` : `if (!${detect}()) throw missingApiError('${standard}');\n return new ${adapter}();`}
|
|
668
710
|
}
|
|
669
711
|
`;
|
|
670
|
-
case "cmi5": return `
|
|
671
|
-
import { CMI5Adapter } from 'tessera-learn/runtime/adapters/cmi5.js';
|
|
672
|
-
import { hasCMI5LaunchParams } from 'tessera-learn/runtime/adapters/discovery.js';
|
|
673
|
-
import { LMSAdapterError } from 'tessera-learn/runtime/adapters/index.js';
|
|
674
|
-
export function createAdapter() {
|
|
675
|
-
if (!hasCMI5LaunchParams()) throw new LMSAdapterError('cmi5', 'Tessera: cmi5 launch parameters not present on URL. Course must be launched from a cmi5-compliant LMS.');
|
|
676
|
-
return new CMI5Adapter();
|
|
677
712
|
}
|
|
678
|
-
|
|
679
|
-
|
|
713
|
+
function tesseraAdapterPlugin(standardOverride) {
|
|
714
|
+
return virtualModule("tessera:adapter", VIRTUAL_ADAPTER_ID, ({ projectRoot, isBuild }) => {
|
|
715
|
+
if (!isBuild) return `export { createAdapter } from 'tessera-learn/runtime/adapters/index.js';`;
|
|
716
|
+
let standard = readResolvedConfig(projectRoot, standardOverride).standard;
|
|
717
|
+
if (isAuditBuild()) standard = "web";
|
|
718
|
+
if (standard in LMS_ADAPTER_GEN) return generateLmsAdapterModule(standard);
|
|
719
|
+
return `
|
|
680
720
|
import { WebAdapter } from 'tessera-learn/runtime/adapters/web.js';
|
|
681
|
-
export function createAdapter(config) {
|
|
682
|
-
return new WebAdapter(config);
|
|
721
|
+
export function createAdapter(config, options) {
|
|
722
|
+
return new WebAdapter(config, options && options.manifest);
|
|
683
723
|
}
|
|
684
724
|
`;
|
|
685
|
-
}
|
|
686
725
|
});
|
|
687
726
|
}
|
|
688
727
|
const VIRTUAL_XAPI_SETUP_ID = "virtual:tessera-xapi-setup";
|
|
689
|
-
function tesseraXAPISetupPlugin() {
|
|
728
|
+
function tesseraXAPISetupPlugin(standardOverride) {
|
|
690
729
|
return virtualModule("tessera:xapi-setup", VIRTUAL_XAPI_SETUP_ID, ({ projectRoot, isBuild }) => {
|
|
691
730
|
if (!isBuild) return `export { buildXAPIClient } from 'tessera-learn/runtime/xapi/setup.js';`;
|
|
692
731
|
if (isAuditBuild()) return `export async function buildXAPIClient() { return null; }`;
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (read.ok) {
|
|
697
|
-
if (typeof read.config.export?.standard === "string") standard = read.config.export.standard;
|
|
698
|
-
hasXapi = read.config.xapi != null;
|
|
699
|
-
}
|
|
700
|
-
if (hasXapi || standard === "cmi5") return `export { buildXAPIClient } from 'tessera-learn/runtime/xapi/setup.js';`;
|
|
732
|
+
const read = readResolvedConfig(projectRoot, standardOverride);
|
|
733
|
+
const standard = read.standard;
|
|
734
|
+
if (read.ok && read.config.xapi != null || standard === "cmi5" || standard === "xapi") return `export { buildXAPIClient } from 'tessera-learn/runtime/xapi/setup.js';`;
|
|
701
735
|
return `export async function buildXAPIClient() { return null; }`;
|
|
702
736
|
});
|
|
703
737
|
}
|
|
@@ -726,6 +760,6 @@ function tesseraFirstPagePreloadPlugin(manifestRef) {
|
|
|
726
760
|
};
|
|
727
761
|
}
|
|
728
762
|
//#endregion
|
|
729
|
-
export { tesseraPlugin as t };
|
|
763
|
+
export { tesseraPlugin as n, mergeCourseConfig as t };
|
|
730
764
|
|
|
731
|
-
//# sourceMappingURL=plugin-
|
|
765
|
+
//# sourceMappingURL=plugin-BuMiDTmU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-BuMiDTmU.js","names":[],"sources":["../src/runtime/slugify.ts","../src/plugin/export.ts","../src/plugin/override-plugin.ts","../src/plugin/layout.ts","../src/plugin/quiz.ts","../src/plugin/index.ts"],"sourcesContent":["/**\n * Slugify a string for use as a URL-safe / filename-safe identifier.\n * \"My Course Title\" → \"my-course-title\"\n *\n * Shared by the runtime (`WebAdapter` localStorage key) and the build-time\n * exporter (`runExport` zip filename). Both want identical, deterministic\n * output so a course's storage key matches its package name.\n */\nexport function slugify(text: string): string {\n return text\n .toLowerCase()\n .trim()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/[\\s_]+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n}\n","import {\n existsSync,\n readdirSync,\n statSync,\n writeFileSync,\n unlinkSync,\n} from 'node:fs';\nimport { resolve } from 'node:path';\nimport { createWriteStream } from 'node:fs';\nimport { createHash } from 'node:crypto';\nimport { ZipArchive } from 'archiver';\nimport { slugify } from '../runtime/slugify.js';\nimport { courseIdentity } from '../runtime/types.js';\n\n// ---------- Types ----------\n\ninterface ExportConfig {\n title: string;\n id?: string;\n description?: string;\n version?: string;\n scoring?: { passingScore?: number };\n completion?: { mode?: 'quiz' | 'percentage' };\n export?: { standard?: string };\n}\n\n// ---------- Helpers ----------\n\nconst UNTITLED_TITLE = 'Untitled Course';\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Recursively collect all file paths relative to a directory.\n */\nfunction collectFiles(dir: string, base: string = ''): string[] {\n const files: string[] = [];\n if (!existsSync(dir)) return files;\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n const relPath = base ? `${base}/${entry.name}` : entry.name;\n // Dirent is lstat-based; stat symlinks so a symlinked dir still recurses.\n const isDir = entry.isSymbolicLink()\n ? statSync(resolve(dir, entry.name)).isDirectory()\n : entry.isDirectory();\n if (isDir) {\n files.push(...collectFiles(resolve(dir, entry.name), relPath));\n } else {\n files.push(relPath);\n }\n }\n return files;\n}\n\n/**\n * Derive a stable URN IRI from a seed string. cmi5 §13.1 / xs:anyURI\n * require course / AU ids to be IRIs — bare hex or UUID-shaped strings\n * (without correct version/variant bits) aren't conformant URNs and may\n * be rejected by strict LMS importers.\n *\n * Hash the seed so the id survives rebuilds, then format as\n * `urn:tessera:<kind>:<hex>`. The same seed always produces the same\n * IRI, so existing LRS records are not orphaned by re-export.\n */\nfunction stableUrn(kind: 'course' | 'au', seed: string): string {\n const h = createHash('sha256').update(seed).digest('hex');\n // 32 hex chars (128 bits of entropy) is plenty; trim to keep ids short.\n return `urn:tessera:${kind}:${h.slice(0, 32)}`;\n}\n\n// AU activity id, derived from the course id so re-exports don't orphan LRS\n// records. Shared by the cmi5 and tincan manifests.\nfunction auIdFor(config: ExportConfig): string {\n const id = courseIdentity(config);\n return stableUrn('au', id ? `${id}#au` : 'tessera-au');\n}\n\nfunction formatSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\n// ---------- Manifest Generators ----------\n\n/** Per-version XML differences in imsmanifest.xml between SCORM 1.2 and 2004. */\ninterface ScormManifestDialect {\n rootNs: string;\n adlcpNs: string;\n schemaversion: string;\n /** Attribute name on <resource>: SCORM 1.2 uses lowercase, 2004 uses camelCase. */\n scormTypeAttr: 'scormtype' | 'scormType';\n /** Whitespace-separated namespace+XSD pairs for xsi:schemaLocation. */\n schemaLocation: string;\n}\n\nconst SCORM_DIALECTS: Record<'1.2' | '2004', ScormManifestDialect> = {\n '1.2': {\n rootNs: 'http://www.imsproject.org/xsd/imscp_rootv1p1p2',\n adlcpNs: 'http://www.adlnet.org/xsd/adlcp_rootv1p2',\n schemaversion: '1.2',\n scormTypeAttr: 'scormtype',\n schemaLocation:\n 'http://www.imsproject.org/xsd/imscp_rootv1p1p2 imscp_rootv1p1p2.xsd ' +\n 'http://www.imsglobal.org/xsd/imsmd_rootv1p2p1 imsmd_rootv1p2p1.xsd ' +\n 'http://www.adlnet.org/xsd/adlcp_rootv1p2 adlcp_rootv1p2.xsd',\n },\n '2004': {\n rootNs: 'http://www.imsglobal.org/xsd/imscp_v1p1',\n adlcpNs: 'http://www.adlnet.org/xsd/adlcp_v1p3',\n schemaversion: '2004 4th Edition',\n scormTypeAttr: 'scormType',\n schemaLocation:\n 'http://www.imsglobal.org/xsd/imscp_v1p1 imscp_v1p1.xsd ' +\n 'http://www.adlnet.org/xsd/adlcp_v1p3 adlcp_v1p3.xsd',\n },\n};\n\nexport function generateScormManifest(\n version: '1.2' | '2004',\n config: ExportConfig,\n distDir: string,\n): string {\n const dialect = SCORM_DIALECTS[version];\n const title = escapeXml(config.title || UNTITLED_TITLE);\n const files = collectFiles(distDir);\n const fileElements = files\n .map((f) => ` <file href=\"${escapeXml(f)}\" />`)\n .join('\\n');\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<manifest identifier=\"tessera-course\" version=\"1.0\"\n xmlns=\"${dialect.rootNs}\"\n xmlns:adlcp=\"${dialect.adlcpNs}\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:schemaLocation=\"${dialect.schemaLocation}\">\n <metadata>\n <schema>ADL SCORM</schema>\n <schemaversion>${dialect.schemaversion}</schemaversion>\n </metadata>\n <organizations default=\"org-1\">\n <organization identifier=\"org-1\">\n <title>${title}</title>\n <item identifier=\"item-1\" identifierref=\"res-1\">\n <title>${title}</title>\n </item>\n </organization>\n </organizations>\n <resources>\n <resource identifier=\"res-1\" type=\"webcontent\" adlcp:${dialect.scormTypeAttr}=\"sco\" href=\"index.html\">\n${fileElements}\n </resource>\n </resources>\n</manifest>`;\n}\n\nexport function generateCMI5Xml(config: ExportConfig): string {\n const title = escapeXml(config.title || UNTITLED_TITLE);\n const description = escapeXml(config.description || '');\n // Derive stable IDs from the course id so they survive rebuilds without\n // orphaning existing learner records in the LRS.\n const courseId = stableUrn(\n 'course',\n courseIdentity(config) || 'tessera-course',\n );\n const auId = auIdFor(config);\n // cmi5 §10.2.4 caps masteryScore at 4 decimals; avoid float drift like 0.7000000000000001.\n const masteryScore = Number(\n ((config.scoring?.passingScore ?? 70) / 100).toFixed(4),\n );\n // cmi5 §13.1.4 — `moveOn` decides which verb(s) the LMS treats as\n // satisfying the AU. For graded courses (completion gated on a quiz)\n // a learner who completes without passing should NOT receive credit, so\n // the LMS needs both a Completed AND a Passed before satisfaction.\n // Percentage-mode courses don't surface pass/fail, so completion alone\n // is the right signal.\n const moveOn =\n config.completion?.mode === 'quiz' ? 'CompletedAndPassed' : 'Completed';\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<courseStructure xmlns=\"https://w3id.org/xapi/profiles/cmi5/v1/CourseStructure.xsd\">\n <course id=\"${courseId}\">\n <title><langstring lang=\"en-US\">${title}</langstring></title>\n <description><langstring lang=\"en-US\">${description}</langstring></description>\n </course>\n <au id=\"${auId}\" launchMethod=\"AnyWindow\" moveOn=\"${moveOn}\" masteryScore=\"${masteryScore}\">\n <title><langstring lang=\"en-US\">${title}</langstring></title>\n <description><langstring lang=\"en-US\">${description}</langstring></description>\n <url>index.html</url>\n </au>\n</courseStructure>`;\n}\n\nexport function generateTincanXml(config: ExportConfig): string {\n const title = escapeXml(config.title || UNTITLED_TITLE);\n const description = escapeXml(config.description || '');\n // Reuse the cmi5/SCORM stable-id scheme so re-exports don't orphan LRS records.\n const auId = auIdFor(config);\n // tincan.xml carries NO xAPI version — the version is set at runtime by the\n // adapter's X-Experience-API-Version header, not declared in the manifest.\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<tincan xmlns=\"http://projecttincan.com/tincan.xsd\">\n <activities>\n <activity id=\"${auId}\" type=\"http://adlnet.gov/expapi/activities/course\">\n <name>${title}</name>\n <description lang=\"en-US\">${description}</description>\n <launch lang=\"en-US\">index.html</launch>\n </activity>\n </activities>\n</tincan>`;\n}\n\n// ---------- ZIP Packaging ----------\n\nexport async function createZip(\n distDir: string,\n outputPath: string,\n): Promise<number> {\n return new Promise((res, reject) => {\n const output = createWriteStream(outputPath);\n const archive = new ZipArchive({ zlib: { level: 9 } });\n\n output.on('close', () => {\n res(archive.pointer());\n });\n output.on('error', reject);\n archive.on('error', reject);\n\n archive.pipe(output);\n archive.directory(distDir, false);\n void archive.finalize();\n });\n}\n\n// ---------- Main Export ----------\n\n/**\n * Run the export process after Vite build completes.\n * Writes manifest XML into dist/, then packages into ZIP if needed.\n */\n/** Remove any previously built zips for this package to prevent accumulation. */\nfunction cleanOldZips(projectRoot: string, slug: string): void {\n try {\n for (const f of readdirSync(projectRoot)) {\n if (f.startsWith(`${slug}-`) && f.endsWith('.zip')) {\n try {\n unlinkSync(resolve(projectRoot, f));\n } catch {}\n }\n }\n } catch {}\n}\n\n/** Packaged (zipped) export targets: which manifest file to write and how. */\nconst PACKAGED_EXPORTS: Record<\n 'scorm12' | 'scorm2004' | 'cmi5' | 'xapi',\n {\n manifestFile: string;\n label: string;\n generate: (config: ExportConfig, distDir: string) => string;\n }\n> = {\n scorm12: {\n manifestFile: 'imsmanifest.xml',\n label: 'SCORM 1.2',\n generate: (config, distDir) =>\n generateScormManifest('1.2', config, distDir),\n },\n scorm2004: {\n manifestFile: 'imsmanifest.xml',\n label: 'SCORM 2004',\n generate: (config, distDir) =>\n generateScormManifest('2004', config, distDir),\n },\n cmi5: {\n manifestFile: 'cmi5.xml',\n label: 'CMI5',\n generate: (config) => generateCMI5Xml(config),\n },\n xapi: {\n manifestFile: 'tincan.xml',\n label: 'xAPI 1.0.3',\n generate: (config) => generateTincanXml(config),\n },\n};\n\nexport async function runExport(\n projectRoot: string,\n config: ExportConfig,\n): Promise<void> {\n const distDir = resolve(projectRoot, 'dist');\n const standard = config.export?.standard || 'web';\n const slug = slugify(config.title || 'tessera-course') || 'tessera-course';\n const version = config.version || '1.0.0';\n const zipName = `${slug}-${version}.zip`;\n const zipPath = resolve(projectRoot, zipName);\n\n if (standard === 'web') {\n const files = collectFiles(distDir);\n let totalSize = 0;\n for (const f of files) totalSize += statSync(resolve(distDir, f)).size;\n console.log(`✓ Web export: dist/ (${formatSize(totalSize)})`);\n return;\n }\n\n const spec = PACKAGED_EXPORTS[standard as keyof typeof PACKAGED_EXPORTS];\n if (!spec) return; // unknown standard — the validator rejects these upstream\n\n writeFileSync(\n resolve(distDir, spec.manifestFile),\n spec.generate(config, distDir),\n 'utf-8',\n );\n cleanOldZips(projectRoot, slug);\n const zipSize = await createZip(distDir, zipPath);\n console.log(`✓ ${spec.label} export: ${zipName} (${formatSize(zipSize)})`);\n}\n","import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite';\nimport { normalizePath } from 'vite';\nimport { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nexport interface OverridePluginOptions {\n name: string;\n virtualId: string;\n projectFile: string;\n /** Built-in re-exported when the project file is absent; null export otherwise. */\n builtinFile?: string;\n}\n\n/**\n * A virtual module that resolves to a project-root override file when present,\n * and to the built-in (or a null export) otherwise. Shared by the layout and\n * quiz plugins — they differ only in the virtual id, file name, and built-in.\n */\nexport function createOverridePlugin({\n name,\n virtualId,\n projectFile,\n builtinFile,\n}: OverridePluginOptions): Plugin {\n const resolvedId = '\\0' + virtualId;\n const fallback = builtinFile\n ? `export { default } from '${normalizePath(builtinFile)}';`\n : 'export default null;';\n let filePath: string;\n\n return {\n name,\n enforce: 'pre',\n\n configResolved(config: ResolvedConfig) {\n filePath = resolve(config.root, projectFile);\n },\n\n resolveId(id) {\n if (id === virtualId) return resolvedId;\n return null;\n },\n\n load(id) {\n if (id !== resolvedId) return null;\n if (existsSync(filePath)) {\n // Only watch when it exists — addWatchFile on a missing path makes\n // Vite's importAnalysis try to resolve it as a real import.\n this.addWatchFile(filePath);\n return `export { default } from '${normalizePath(filePath)}';`;\n }\n return fallback;\n },\n\n configureServer(server: ViteDevServer) {\n // Only add/unlink flips load()'s output between the override and the\n // fallback; a `change` leaves it identical and Svelte's own HMR handles\n // the underlying file.\n server.watcher.on('all', (event, changed) => {\n if (changed !== filePath) return;\n if (event !== 'add' && event !== 'unlink') return;\n const mod = server.moduleGraph.getModuleById(resolvedId);\n if (mod) server.moduleGraph.invalidateModule(mod);\n server.ws.send({ type: 'full-reload' });\n });\n },\n };\n}\n","import type { Plugin } from 'vite';\nimport { createOverridePlugin } from './override-plugin.js';\n\nexport function tesseraLayoutPlugin(): Plugin {\n return createOverridePlugin({\n name: 'tessera:layout',\n virtualId: 'virtual:tessera-layout',\n projectFile: 'layout.svelte',\n });\n}\n","import type { Plugin } from 'vite';\nimport { resolve } from 'node:path';\nimport { createOverridePlugin } from './override-plugin.js';\nimport { resolvePackageRoot } from './package-root.js';\n\nexport function tesseraQuizPlugin(): Plugin {\n const builtinQuiz = resolve(\n resolvePackageRoot(),\n 'src',\n 'components',\n 'Quiz.svelte',\n );\n return createOverridePlugin({\n name: 'tessera:quiz',\n virtualId: 'virtual:tessera-quiz',\n projectFile: 'quiz.svelte',\n builtinFile: builtinQuiz,\n });\n}\n","import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite';\nimport { svelte } from '@sveltejs/vite-plugin-svelte';\nimport { resolve, relative, isAbsolute } from 'node:path';\nimport {\n existsSync,\n readdirSync,\n statSync,\n writeFileSync,\n unlinkSync,\n cpSync,\n mkdirSync,\n} from 'node:fs';\nimport {\n generateManifest,\n readCourseConfig,\n readResolvedConfig,\n type CourseConfigRead,\n} from './manifest.js';\nimport type { Manifest } from './manifest.js';\nimport type { CourseConfig } from '../runtime/types.js';\nimport {\n DEFAULT_PASSING_SCORE,\n DEFAULT_PERCENTAGE_THRESHOLD,\n} from '../runtime/defaults.js';\nimport {\n validateProject,\n reportValidationIssues,\n normalizeA11y,\n isPlausibleLanguageTag,\n isIgnored,\n type A11ySettings,\n} from './validation.js';\nimport { buildCsp } from './csp.js';\nimport { runExport } from './export.js';\nimport { tesseraLayoutPlugin } from './layout.js';\nimport { tesseraQuizPlugin } from './quiz.js';\nimport { resolvePackageRoot } from './package-root.js';\n\nimport { AUDIT_ENV_FLAG } from './a11y/audit.js';\n\nexport { runAudit } from './a11y/audit.js';\nexport type { AuditOptions, ImpactLevel } from './a11y/audit.js';\n\nfunction isAuditBuild(): boolean {\n return process.env[AUDIT_ENV_FLAG] === '1';\n}\n\n// Resolve the runtime directory where App.svelte lives\nfunction resolveRuntimeDir(): string {\n return resolve(resolvePackageRoot(), 'src', 'runtime');\n}\n\n// Resolve the framework styles directory\nfunction resolveStylesDir(): string {\n return resolve(resolvePackageRoot(), 'styles');\n}\n\n// Tier-1a state shared between the svelte() onwarn handler and the sibling\n// gate plugin. onwarn fires during transform (after the Tier-1b buildStart\n// gate), so a11y warnings are collected here and flushed/gated at buildEnd.\ninterface A11yCompilerState {\n warnings: string[];\n projectRoot: string;\n isBuild: boolean;\n settings: A11ySettings;\n}\n\n// Svelte's onwarn filename is relative to the vite root (e.g. `pages/x.svelte`)\n// in build and may be absolute or a virtual id elsewhere. Return the\n// project-relative path for a real author file, or null to skip framework /\n// node_modules / virtual modules — Tier 0 owns the framework's own warnings.\nfunction projectFileRel(\n filename: string | undefined,\n projectRoot: string,\n): string | null {\n if (!filename || !projectRoot) return null;\n if (\n filename.startsWith('\\0') ||\n filename.includes('virtual:') ||\n filename.includes('node_modules')\n ) {\n return null;\n }\n const abs = isAbsolute(filename) ? filename : resolve(projectRoot, filename);\n const rel = relative(projectRoot, abs);\n if (rel.startsWith('..') || isAbsolute(rel) || rel.includes('node_modules')) {\n return null;\n }\n return rel;\n}\n\ntype VirtualLoadCtx = { projectRoot: string; isBuild: boolean };\n\nfunction virtualModule(\n name: string,\n virtualId: string,\n load: (\n this: import('vite').Rollup.PluginContext,\n ctx: VirtualLoadCtx,\n ) => string | null,\n): Plugin {\n const resolvedId = '\\0' + virtualId;\n let projectRoot = '';\n let isBuild = false;\n return {\n name,\n enforce: 'pre',\n configResolved(config: ResolvedConfig) {\n projectRoot = config.root;\n isBuild = config.command === 'build';\n },\n resolveId(id) {\n return id === virtualId ? resolvedId : null;\n },\n load(id) {\n return id === resolvedId\n ? load.call(this, { projectRoot, isBuild })\n : null;\n },\n };\n}\n\nexport function tesseraPlugin(options: { standardOverride?: string } = {}) {\n const { standardOverride } = options;\n const manifestRef: { current: Manifest | null; root: string } = {\n current: null,\n root: '',\n };\n const a11y: A11yCompilerState = {\n warnings: [],\n projectRoot: '',\n isBuild: false,\n settings: normalizeA11y(undefined),\n };\n return [\n svelte({\n compilerOptions: { css: 'external' },\n onwarn(warning, defaultHandler) {\n if (warning.code?.startsWith('a11y')) {\n const rel = projectFileRel(warning.filename, a11y.projectRoot);\n if (rel !== null) {\n const msg = `[${warning.code}] ${rel}: ${warning.message}`;\n if (a11y.isBuild) {\n a11y.warnings.push(msg);\n } else if (!a11y.settings.ignore.includes(warning.code)) {\n reportValidationIssues({ errors: [], warnings: [msg] });\n }\n }\n return; // suppress the raw Vite print; we re-emit via the reporter\n }\n defaultHandler?.(warning);\n },\n }),\n tesseraA11yCompilerPlugin(a11y),\n tesseraValidationPlugin(standardOverride),\n tesseraEntryPlugin(standardOverride),\n tesseraConfigDefaultsPlugin(),\n tesseraConfigPlugin(standardOverride),\n tesseraPagesPlugin(),\n tesseraManifestPlugin(manifestRef),\n tesseraLayoutPlugin(),\n tesseraQuizPlugin(),\n tesseraAdapterPlugin(standardOverride),\n tesseraXAPISetupPlugin(standardOverride),\n tesseraFirstPagePreloadPlugin(manifestRef),\n tesseraExportPlugin(standardOverride),\n ];\n}\n\n// ---------- Entry Plugin ----------\n\nconst VIRTUAL_ENTRY_ID = 'virtual:tessera-entry';\nconst RESOLVED_ENTRY_ID = '\\0' + VIRTUAL_ENTRY_ID;\nconst VIRTUAL_MAIN_ID = '/virtual:tessera-main';\nconst RESOLVED_MAIN_ID = '\\0virtual:tessera-main';\n\nfunction tesseraEntryPlugin(standardOverride?: string): Plugin {\n const runtimeDir = resolveRuntimeDir();\n const stylesDir = resolveStylesDir();\n const appSveltePath = resolve(runtimeDir, 'App.svelte');\n let projectRoot: string;\n let outDir: string;\n let isBuild = false;\n\n return {\n name: 'tessera:entry',\n enforce: 'pre',\n\n configResolved(config: ResolvedConfig) {\n projectRoot = config.root;\n outDir = resolve(config.root, config.build.outDir);\n isBuild = config.command === 'build';\n },\n\n // For build mode: write index.html so Rollup can find it\n buildStart() {\n if (isBuild) {\n const read = readResolvedConfig(projectRoot, standardOverride);\n writeFileSync(\n resolve(projectRoot, 'index.html'),\n generateIndexHtml(readLanguage(read), cspMeta(read)),\n 'utf-8',\n );\n }\n },\n\n // For build mode: clean up temporary index.html and copy assets\n closeBundle() {\n if (isBuild) {\n const htmlPath = resolve(projectRoot, 'index.html');\n if (existsSync(htmlPath)) {\n try {\n unlinkSync(htmlPath);\n } catch {}\n }\n\n // Copy assets/ into the build's assets/ so $assets/ references resolve\n const assetsDir = resolve(projectRoot, 'assets');\n const distAssetsDir = resolve(outDir, 'assets');\n if (existsSync(assetsDir)) {\n mkdirSync(distAssetsDir, { recursive: true });\n cpSync(assetsDir, distAssetsDir, { recursive: true });\n }\n }\n },\n\n // Serve index.html for the dev server\n configureServer(server: ViteDevServer) {\n return () => {\n server.middlewares.use(async (req, res, next) => {\n if (req.url === '/' || req.url === '/index.html') {\n const html = generateIndexHtml(\n readLanguage(readCourseConfig(projectRoot)),\n );\n const transformed = await server.transformIndexHtml(req.url, html);\n res.setHeader('Content-Type', 'text/html');\n res.statusCode = 200;\n res.end(transformed);\n return;\n }\n next();\n });\n };\n },\n\n resolveId(id) {\n if (id === VIRTUAL_ENTRY_ID) return RESOLVED_ENTRY_ID;\n if (id === VIRTUAL_MAIN_ID || id === 'virtual:tessera-main')\n return RESOLVED_MAIN_ID;\n return null;\n },\n\n load(id) {\n if (id === RESOLVED_ENTRY_ID || id === RESOLVED_MAIN_ID) {\n return generateEntryScript(appSveltePath, stylesDir, projectRoot);\n }\n return null;\n },\n };\n}\n\n// 'en' fallback applied here: the config default-merge runs later than buildStart.\n// Only a validated BCP-47 tag is interpolated into <html lang>, so a malformed\n// value (caught separately as a warning) can't ship a broken attribute.\nfunction readLanguage(read: CourseConfigRead): string {\n const lang = read.ok ? read.config.language : undefined;\n return isPlausibleLanguageTag(lang) ? lang : 'en';\n}\n\n// Web export only — never on LMS packages (whose iframe JS bridges a meta CSP\n// could break) and never on the dev server (a meta connect-src would block\n// Vite's HMR websocket). `export.csp` extends the baseline per-directive, or\n// `false` drops the meta for deployments that set a CSP header themselves.\nfunction cspMeta(read: CourseConfigRead & { standard: string }): string {\n if (read.standard !== 'web') return '';\n const csp = read.ok ? read.config.export?.csp : undefined;\n if (csp === false) return '';\n return `\\n <meta http-equiv=\"Content-Security-Policy\" content=\"${buildCsp(csp)}\" />`;\n}\n\nfunction generateIndexHtml(lang: string, csp = ''): string {\n return `<!DOCTYPE html>\n<html lang=\"${lang}\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />${csp}\n <title>Tessera Course</title>\n</head>\n<body>\n <div id=\"tessera-root\"></div>\n <script type=\"module\" src=\"/virtual:tessera-main\"></script>\n</body>\n</html>`;\n}\n\nfunction generateEntryScript(\n appSveltePath: string,\n frameworkStylesDir: string,\n projectRoot: string,\n): string {\n const normalizedPath = appSveltePath.replace(/\\\\/g, '/');\n\n // Framework CSS imports (theme → base → layout)\n const frameworkCssOrder = ['theme.css', 'base.css', 'layout.css'];\n const frameworkImports = frameworkCssOrder\n .map((file) => resolve(frameworkStylesDir, file).replace(/\\\\/g, '/'))\n .filter((path) => existsSync(path))\n .map((path) => `import '${path}';`)\n .join('\\n');\n\n // User CSS imports from project's styles/ directory\n const userStylesDir = resolve(projectRoot, 'styles');\n let userImports = '';\n if (existsSync(userStylesDir)) {\n const userCssFiles = readdirSync(userStylesDir)\n .filter((f) => f.endsWith('.css'))\n .sort();\n userImports = userCssFiles\n .map((f) => resolve(userStylesDir, f).replace(/\\\\/g, '/'))\n .map((path) => `import '${path}';`)\n .join('\\n');\n }\n\n return `// Framework styles\n${frameworkImports}\n// User styles\n${userImports}\n\nimport { mount } from 'svelte';\nimport App from '${normalizedPath}';\n\nmount(App, {\n target: document.getElementById('tessera-root'),\n});\n`;\n}\n\n// ---------- Config Plugin ----------\n\nconst VIRTUAL_CONFIG_ID = 'virtual:tessera-config';\n\nfunction completionDefaults(mode: string | undefined): {\n completion: Record<string, unknown>;\n passingScore: number;\n} {\n if (mode === 'manual') {\n return { completion: { mode: 'manual' }, passingScore: 0 };\n }\n if (mode === 'quiz') {\n return {\n completion: { mode: 'quiz' },\n passingScore: DEFAULT_PASSING_SCORE,\n };\n }\n return {\n completion: {\n mode: 'percentage',\n percentageThreshold: DEFAULT_PERCENTAGE_THRESHOLD,\n },\n passingScore: DEFAULT_PASSING_SCORE,\n };\n}\n\nfunction tesseraConfigDefaultsPlugin(): Plugin {\n return {\n name: 'tessera:config-defaults',\n enforce: 'pre',\n config(config) {\n const root = config.root || process.cwd();\n return {\n base: './',\n build: { assetsDir: 'tessera' },\n resolve: { alias: { $assets: resolve(root, 'assets') } },\n // tessera-learn ships .ts/.svelte.ts source; Vite's dep optimizer\n // doesn't run vite-plugin-svelte's preprocessor, so skip pre-bundling.\n optimizeDeps: { exclude: ['tessera-learn'] },\n };\n },\n };\n}\n\n/** Fill runtime defaults into a parsed course.config.js. Exported for tests. */\nexport function mergeCourseConfig(userConfig: Partial<CourseConfig>) {\n const { completion, passingScore } = completionDefaults(\n userConfig.completion?.mode,\n );\n return {\n ...userConfig,\n title: userConfig.title || 'Untitled Course',\n resume: userConfig.resume ?? 'auto',\n navigation: { mode: 'free', ...userConfig.navigation },\n completion: { ...completion, ...userConfig.completion },\n scoring: { passingScore, ...userConfig.scoring },\n export: { standard: 'web', ...userConfig.export },\n };\n}\n\nfunction tesseraConfigPlugin(standardOverride?: string): Plugin {\n return virtualModule(\n 'tessera:config',\n VIRTUAL_CONFIG_ID,\n function ({ projectRoot }) {\n const configPath = resolve(projectRoot, 'course.config.js');\n if (existsSync(configPath)) this.addWatchFile(configPath);\n // The runtime reads export.standard too, so readResolvedConfig must apply\n // the override here — the bundled config, not just the manifest/adapter.\n const read = readResolvedConfig(projectRoot, standardOverride);\n const userConfig: Partial<CourseConfig> = read.ok ? read.config : {};\n return `export default ${JSON.stringify(mergeCourseConfig(userConfig))};`;\n },\n );\n}\n\n// ---------- Manifest Watch Helpers ----------\n\n/** Register all _meta.js and .svelte files under pagesDir as watch files for build mode. */\nfunction addWatchFiles(\n ctx: { addWatchFile(id: string): void },\n dir: string,\n): void {\n if (!existsSync(dir)) return;\n for (const entry of readdirSync(dir)) {\n const full = resolve(dir, entry);\n if (statSync(full).isDirectory()) {\n addWatchFiles(ctx, full);\n } else if (entry.endsWith('.svelte') || entry === '_meta.js') {\n ctx.addWatchFile(full);\n }\n }\n}\n\n// ---------- Pages Plugin ----------\n\nconst VIRTUAL_PAGES_ID = 'virtual:tessera-pages';\n\n/**\n * Provides a virtual module that exports an import.meta.glob map for all .svelte\n * pages. This runs in the user's project context so the glob resolves against their\n * pages/ directory, and Vite can statically analyze it for code splitting.\n */\nfunction tesseraPagesPlugin(): Plugin {\n return virtualModule('tessera:pages', VIRTUAL_PAGES_ID, () => {\n return `export default import.meta.glob('/pages/**/*.svelte');`;\n });\n}\n\n// ---------- Validation Plugin ----------\n\nfunction tesseraValidationPlugin(standardOverride?: string): Plugin {\n let projectRoot: string;\n let isBuild = false;\n\n return {\n name: 'tessera:validation',\n enforce: 'pre',\n\n configResolved(config: ResolvedConfig) {\n projectRoot = config.root;\n isBuild = config.command === 'build';\n // Run validation during dev (configResolved fires before server starts)\n if (!isBuild) {\n runValidation(projectRoot, standardOverride);\n }\n },\n\n buildStart() {\n // Run validation during build (buildStart fires once before bundling)\n if (isBuild) {\n runValidation(projectRoot, standardOverride);\n }\n },\n };\n}\n\n// Tier 1a: flush + gate the Svelte compiler's a11y warnings at buildEnd, after\n// every module is transformed. svelte() accepts `onwarn` but not arbitrary\n// Rollup hooks, so the gate lives here and shares the onwarn closure.\nfunction tesseraA11yCompilerPlugin(a11y: A11yCompilerState): Plugin {\n return {\n name: 'tessera:a11y-compiler',\n enforce: 'pre',\n\n configResolved(config: ResolvedConfig) {\n a11y.projectRoot = config.root;\n a11y.isBuild = config.command === 'build';\n const read = readCourseConfig(config.root);\n a11y.settings = normalizeA11y(read.ok ? read.config.a11y : undefined);\n },\n\n buildEnd() {\n if (!a11y.isBuild || a11y.warnings.length === 0) return;\n const ignored = new Set(a11y.settings.ignore);\n const warnings = a11y.warnings.filter((msg) => !isIgnored(msg, ignored));\n a11y.warnings = [];\n if (warnings.length === 0) return;\n if (a11y.settings.level === 'error') {\n reportValidationIssues({ errors: warnings, warnings: [] });\n throw new Error(\n `Tessera: ${warnings.length} a11y issue(s) with a11y.level: 'error'. Fix the errors above to continue.`,\n );\n }\n reportValidationIssues({ errors: [], warnings });\n },\n };\n}\n\nfunction runValidation(projectRoot: string, standardOverride?: string): void {\n const result = validateProject(projectRoot, standardOverride);\n reportValidationIssues(result);\n if (result.errors.length > 0) {\n throw new Error(\n `Tessera validation failed with ${result.errors.length} error(s). Fix the errors above to continue.`,\n );\n }\n}\n\n// ---------- Export Plugin ----------\n\nfunction tesseraExportPlugin(standardOverride?: string): Plugin {\n let projectRoot: string;\n let isBuild = false;\n\n return {\n name: 'tessera:export',\n enforce: 'post',\n\n configResolved(config: ResolvedConfig) {\n projectRoot = config.root;\n isBuild = config.command === 'build';\n },\n\n async closeBundle() {\n if (!isBuild) return;\n if (isAuditBuild()) return;\n\n const read = readResolvedConfig(projectRoot, standardOverride);\n if (!read.ok) {\n // Validation already required a parseable course.config.js — getting\n // here means it vanished or broke mid-build. Surface that loudly\n // rather than shipping a bundle with no LMS export silently.\n if (read.reason === 'missing') {\n throw new Error(\n '[tessera:export] course.config.js not found at closeBundle. The file must exist for the export step to run.',\n );\n }\n if (read.reason === 'no-export') {\n throw new Error(\n '[tessera:export] course.config.js: could not locate `export default { ... }`. Cannot determine export.standard.',\n );\n }\n throw new Error(\n `[tessera:export] course.config.js: failed to parse export-default object literal — ${(read.error as Error).message}`,\n );\n }\n\n await runExport(\n projectRoot,\n read.config as Parameters<typeof runExport>[1],\n );\n },\n };\n}\n\n// ---------- Manifest Plugin ----------\n\nconst VIRTUAL_MANIFEST_ID = 'virtual:tessera-manifest';\nconst RESOLVED_MANIFEST_ID = '\\0' + VIRTUAL_MANIFEST_ID;\n\nfunction tesseraManifestPlugin(manifestRef: {\n current: Manifest | null;\n root: string;\n}): Plugin {\n let projectRoot: string;\n let pagesDir: string;\n\n function buildManifest(): Manifest {\n const m = generateManifest(pagesDir);\n manifestRef.current = m;\n return m;\n }\n\n return {\n name: 'tessera:manifest',\n enforce: 'pre',\n\n configResolved(config: ResolvedConfig) {\n projectRoot = config.root;\n pagesDir = resolve(projectRoot, 'pages');\n manifestRef.root = projectRoot;\n },\n\n configureServer(devServer: ViteDevServer) {\n // Watch the pages directory for changes\n devServer.watcher.on('all', (event, filePath) => {\n if (!filePath.startsWith(pagesDir)) return;\n\n // Rebuild manifest on relevant file changes\n const isRelevant =\n filePath.endsWith('.svelte') ||\n filePath.endsWith('_meta.js') ||\n event === 'addDir' ||\n event === 'unlinkDir';\n\n if (isRelevant) {\n manifestRef.current = null; // invalidate cache\n\n // Invalidate the virtual module to trigger HMR\n const mod = devServer.moduleGraph.getModuleById(RESOLVED_MANIFEST_ID);\n if (mod) {\n devServer.moduleGraph.invalidateModule(mod);\n devServer.ws.send({ type: 'full-reload' });\n }\n\n console.log(\n `[tessera] Manifest rebuilt (${event}: ${filePath.replace(projectRoot, '')})`,\n );\n }\n });\n },\n\n buildStart() {\n buildManifest();\n },\n\n resolveId(id) {\n if (id === VIRTUAL_MANIFEST_ID) return RESOLVED_MANIFEST_ID;\n return null;\n },\n\n load(id) {\n if (id === RESOLVED_MANIFEST_ID) {\n if (!manifestRef.current) {\n buildManifest();\n }\n\n // Register watch files so Vite's built-in watcher (used in build --watch)\n // knows to re-trigger when pages/ content changes.\n addWatchFiles(this, pagesDir);\n\n // Encode as base64 to prevent Vite's import analysis from\n // scanning .svelte importPath strings as module imports.\n // Replace Infinity with 1e9 since JSON.stringify drops it.\n const json = JSON.stringify(manifestRef.current, (_key, value) =>\n value === Infinity ? 1e9 : value,\n );\n const b64 = Buffer.from(json).toString('base64');\n // atob yields Latin1 bytes; decode through UTF-8 or non-ASCII titles ship as mojibake.\n return `export default JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(\"${b64}\"),(c)=>c.charCodeAt(0))));`;\n }\n return null;\n },\n };\n}\n\nconst VIRTUAL_ADAPTER_ID = 'virtual:tessera-adapter';\n\n// `takesApi`: SCORM detectors return the API object the constructor needs;\n// cmi5/xAPI ones return a boolean.\nconst LMS_ADAPTER_GEN: Record<\n 'scorm12' | 'scorm2004' | 'cmi5' | 'xapi',\n { adapter: string; module: string; detect: string; takesApi: boolean }\n> = {\n scorm12: {\n adapter: 'SCORM12Adapter',\n module: 'scorm12',\n detect: 'findSCORM12API',\n takesApi: true,\n },\n scorm2004: {\n adapter: 'SCORM2004Adapter',\n module: 'scorm2004',\n detect: 'findSCORM2004API',\n takesApi: true,\n },\n cmi5: {\n adapter: 'CMI5Adapter',\n module: 'cmi5',\n detect: 'hasCMI5LaunchParams',\n takesApi: false,\n },\n xapi: {\n adapter: 'XAPIAdapter',\n module: 'xapi',\n detect: 'hasXAPILaunchParams',\n takesApi: false,\n },\n};\n\nfunction generateLmsAdapterModule(\n standard: keyof typeof LMS_ADAPTER_GEN,\n): string {\n const { adapter, module, detect, takesApi } = LMS_ADAPTER_GEN[standard];\n const guard = takesApi\n ? `const api = ${detect}();\\n if (!api) throw missingApiError('${standard}');\\n return new ${adapter}(api);`\n : `if (!${detect}()) throw missingApiError('${standard}');\\n return new ${adapter}();`;\n return `\nimport { ${adapter} } from 'tessera-learn/runtime/adapters/${module}.js';\nimport { ${detect} } from 'tessera-learn/runtime/adapters/discovery.js';\nimport { missingApiError } from 'tessera-learn/runtime/adapters/lms-error.js';\nexport function createAdapter() {\n ${guard}\n}\n`;\n}\n\nfunction tesseraAdapterPlugin(standardOverride?: string): Plugin {\n return virtualModule(\n 'tessera:adapter',\n VIRTUAL_ADAPTER_ID,\n ({ projectRoot, isBuild }) => {\n // In dev, defer to the runtime selector so its WebAdapter fallback\n // for unreachable LMS APIs keeps working.\n if (!isBuild) {\n return `export { createAdapter } from 'tessera-learn/runtime/adapters/index.js';`;\n }\n\n let standard = readResolvedConfig(projectRoot, standardOverride).standard;\n\n // The audit renders headless with no LMS in the frame chain; the SCORM/\n // cmi5 adapters throw when their API is absent, so render with WebAdapter.\n if (isAuditBuild()) standard = 'web';\n\n if (standard in LMS_ADAPTER_GEN) {\n return generateLmsAdapterModule(\n standard as keyof typeof LMS_ADAPTER_GEN,\n );\n }\n return `\nimport { WebAdapter } from 'tessera-learn/runtime/adapters/web.js';\nexport function createAdapter(config, options) {\n return new WebAdapter(config, options && options.manifest);\n}\n`;\n },\n );\n}\n\nconst VIRTUAL_XAPI_SETUP_ID = 'virtual:tessera-xapi-setup';\n\nfunction tesseraXAPISetupPlugin(standardOverride?: string): Plugin {\n return virtualModule(\n 'tessera:xapi-setup',\n VIRTUAL_XAPI_SETUP_ID,\n ({ projectRoot, isBuild }) => {\n if (!isBuild) {\n return `export { buildXAPIClient } from 'tessera-learn/runtime/xapi/setup.js';`;\n }\n\n // The audit runs offline — don't wire real LRS destinations into it.\n if (isAuditBuild()) {\n return `export async function buildXAPIClient() { return null; }`;\n }\n\n const read = readResolvedConfig(projectRoot, standardOverride);\n const standard = read.standard;\n const hasXapi = read.ok && read.config.xapi != null;\n\n // The launch standards (cmi5, plain xAPI) own a publisher the runtime\n // can share for `endpoint: 'lms'`, so wire the client regardless of\n // explicit xapi config.\n if (hasXapi || standard === 'cmi5' || standard === 'xapi') {\n return `export { buildXAPIClient } from 'tessera-learn/runtime/xapi/setup.js';`;\n }\n\n return `export async function buildXAPIClient() { return null; }`;\n },\n );\n}\n\nfunction tesseraFirstPagePreloadPlugin(manifestRef: {\n current: Manifest | null;\n root: string;\n}): Plugin {\n return {\n name: 'tessera:first-page-preload',\n apply: 'build',\n transformIndexHtml: {\n order: 'post',\n handler(_html, ctx) {\n const firstPagePath = manifestRef.current?.pages[0]?.importPath;\n if (!firstPagePath || !ctx.bundle) return;\n const normalized = resolve(\n manifestRef.root,\n firstPagePath.replace(/^\\//, ''),\n ).replace(/\\\\/g, '/');\n const chunk = Object.values(ctx.bundle).find(\n (c): c is import('vite').Rollup.OutputChunk =>\n c.type === 'chunk' &&\n !!c.facadeModuleId &&\n c.facadeModuleId.replace(/\\\\/g, '/') === normalized,\n );\n if (!chunk) return;\n return [\n {\n tag: 'link',\n attrs: { rel: 'modulepreload', href: `./${chunk.fileName}` },\n injectTo: 'head',\n },\n ];\n },\n },\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tessera-learn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "LMS tracking runtime for interactive learning content. One adapter layer (SCORM 1.2, SCORM 2004 4th Edition, cmi5, static Web), your choice of components.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -76,10 +76,10 @@
|
|
|
76
76
|
"dependencies": {
|
|
77
77
|
"@sveltejs/acorn-typescript": "^1.0.10",
|
|
78
78
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
79
|
-
"acorn": "^8.
|
|
79
|
+
"acorn": "^8.17.0",
|
|
80
80
|
"archiver": "^8.0.0",
|
|
81
81
|
"json5": "^2.0.0",
|
|
82
|
-
"svelte": "^5.56.
|
|
82
|
+
"svelte": "^5.56.3",
|
|
83
83
|
"vite": "^8.0.16"
|
|
84
84
|
},
|
|
85
85
|
"peerDependencies": {
|
|
@@ -95,14 +95,14 @@
|
|
|
95
95
|
}
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
98
|
-
"@types/node": "^
|
|
99
|
-
"@vitest/coverage-v8": "^4.1.
|
|
98
|
+
"@types/node": "^26.0.0",
|
|
99
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
100
100
|
"jsdom": "^29.0.1",
|
|
101
101
|
"scorm-again": "3.0.5",
|
|
102
102
|
"svelte-check": "^4.6.0",
|
|
103
|
-
"tsdown": "^0.22.
|
|
103
|
+
"tsdown": "^0.22.3",
|
|
104
104
|
"typescript": "^6.0.3",
|
|
105
|
-
"vitest": "^4.1.
|
|
105
|
+
"vitest": "^4.1.9"
|
|
106
106
|
},
|
|
107
107
|
"scripts": {
|
|
108
108
|
"build": "tsdown",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { onMount
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
3
|
import Sidebar from '../runtime/Sidebar.svelte';
|
|
4
4
|
import { requireNavContext } from '../runtime/contexts.js';
|
|
5
5
|
|
|
@@ -49,10 +49,7 @@
|
|
|
49
49
|
|
|
50
50
|
onMount(() => {
|
|
51
51
|
window.addEventListener('keydown', handleKeyNav);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
onDestroy(() => {
|
|
55
|
-
window.removeEventListener('keydown', handleKeyNav);
|
|
52
|
+
return () => window.removeEventListener('keydown', handleKeyNav);
|
|
56
53
|
});
|
|
57
54
|
</script>
|
|
58
55
|
|