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