@lessonkit/lxpack 1.0.2 → 1.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.
- package/dist/bridge.cjs +21 -0
- package/dist/bridge.d.cts +0 -1
- package/dist/bridge.d.ts +0 -1
- package/dist/bridge.js +21 -0
- package/dist/index.cjs +376 -173
- package/dist/index.d.cts +42 -7
- package/dist/index.d.ts +42 -7
- package/dist/index.js +372 -169
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -2,8 +2,187 @@ import {
|
|
|
2
2
|
telemetryEventToLessonkit
|
|
3
3
|
} from "./chunk-DYQI222N.js";
|
|
4
4
|
|
|
5
|
-
// src/
|
|
5
|
+
// src/descriptor/normalize.ts
|
|
6
6
|
import { validateId } from "@lessonkit/core";
|
|
7
|
+
function normalizeDescriptor(input) {
|
|
8
|
+
const course = validateId(input.courseId, "courseId");
|
|
9
|
+
if (!course.ok) throw new Error("normalizeDescriptor called with invalid courseId");
|
|
10
|
+
return {
|
|
11
|
+
...input,
|
|
12
|
+
courseId: course.id,
|
|
13
|
+
title: input.title.trim(),
|
|
14
|
+
version: input.version?.trim() || void 0,
|
|
15
|
+
spaLessonId: input.spaLessonId?.trim() || void 0,
|
|
16
|
+
lessons: input.lessons.map((lesson) => {
|
|
17
|
+
const idResult = validateId(lesson.id, "lessonId");
|
|
18
|
+
if (!idResult.ok) throw new Error("normalizeDescriptor called with invalid lesson id");
|
|
19
|
+
return {
|
|
20
|
+
...lesson,
|
|
21
|
+
id: idResult.id,
|
|
22
|
+
title: lesson.title.trim(),
|
|
23
|
+
spaPath: lesson.spaPath?.trim() || void 0
|
|
24
|
+
};
|
|
25
|
+
}),
|
|
26
|
+
assessments: input.assessments?.map((assessment) => {
|
|
27
|
+
const check = validateId(assessment.checkId, "checkId");
|
|
28
|
+
if (!check.ok) throw new Error("normalizeDescriptor called with invalid checkId");
|
|
29
|
+
const question = assessment.question.trim();
|
|
30
|
+
if (assessment.kind === "trueFalse") {
|
|
31
|
+
return { ...assessment, checkId: check.id, question };
|
|
32
|
+
}
|
|
33
|
+
if (assessment.kind === "fillInBlanks") {
|
|
34
|
+
return {
|
|
35
|
+
...assessment,
|
|
36
|
+
checkId: check.id,
|
|
37
|
+
question,
|
|
38
|
+
template: assessment.template.trim(),
|
|
39
|
+
blanks: assessment.blanks?.map((b) => ({
|
|
40
|
+
id: b.id.trim(),
|
|
41
|
+
answer: b.answer.trim()
|
|
42
|
+
}))
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (assessment.kind === "findHotspot") {
|
|
46
|
+
return {
|
|
47
|
+
...assessment,
|
|
48
|
+
checkId: check.id,
|
|
49
|
+
question,
|
|
50
|
+
src: assessment.src.trim(),
|
|
51
|
+
alt: assessment.alt.trim(),
|
|
52
|
+
correctTargetId: assessment.correctTargetId.trim()
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (assessment.kind === "findMultipleHotspots") {
|
|
56
|
+
return {
|
|
57
|
+
...assessment,
|
|
58
|
+
checkId: check.id,
|
|
59
|
+
question,
|
|
60
|
+
src: assessment.src.trim(),
|
|
61
|
+
alt: assessment.alt.trim(),
|
|
62
|
+
correctTargetIds: assessment.correctTargetIds.map((id) => id.trim()).filter((id) => id.length > 0)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const mcq = assessment;
|
|
66
|
+
return {
|
|
67
|
+
...mcq,
|
|
68
|
+
checkId: check.id,
|
|
69
|
+
question,
|
|
70
|
+
choices: mcq.choices.map((c) => c.trim()).filter((c) => c.length > 0),
|
|
71
|
+
answer: mcq.answer.trim()
|
|
72
|
+
};
|
|
73
|
+
})
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/descriptor/parseInput.ts
|
|
78
|
+
function isRecord(value) {
|
|
79
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
80
|
+
}
|
|
81
|
+
function parseLessonDescriptor(raw) {
|
|
82
|
+
if (!isRecord(raw)) {
|
|
83
|
+
return { id: "", title: "" };
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
id: typeof raw.id === "string" ? raw.id : "",
|
|
87
|
+
title: typeof raw.title === "string" ? raw.title : "",
|
|
88
|
+
spaPath: typeof raw.spaPath === "string" ? raw.spaPath : void 0
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function parseAssessmentDescriptor(raw) {
|
|
92
|
+
if (!isRecord(raw)) {
|
|
93
|
+
return { checkId: "", question: "", choices: [], answer: "" };
|
|
94
|
+
}
|
|
95
|
+
const base = {
|
|
96
|
+
checkId: typeof raw.checkId === "string" ? raw.checkId : "",
|
|
97
|
+
question: typeof raw.question === "string" ? raw.question : "",
|
|
98
|
+
passingScore: typeof raw.passingScore === "number" ? raw.passingScore : void 0
|
|
99
|
+
};
|
|
100
|
+
const kind = raw.kind;
|
|
101
|
+
if (kind === "trueFalse") {
|
|
102
|
+
return {
|
|
103
|
+
kind: "trueFalse",
|
|
104
|
+
...base,
|
|
105
|
+
answer: typeof raw.answer === "boolean" ? raw.answer : raw.answer === "true"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (kind === "fillInBlanks") {
|
|
109
|
+
return {
|
|
110
|
+
kind: "fillInBlanks",
|
|
111
|
+
...base,
|
|
112
|
+
template: typeof raw.template === "string" ? raw.template : "",
|
|
113
|
+
blanks: Array.isArray(raw.blanks) ? raw.blanks.filter((b) => isRecord(b)).map((b) => ({
|
|
114
|
+
id: typeof b.id === "string" ? b.id : "",
|
|
115
|
+
answer: typeof b.answer === "string" ? b.answer : ""
|
|
116
|
+
})) : void 0
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (kind === "findHotspot") {
|
|
120
|
+
return {
|
|
121
|
+
kind: "findHotspot",
|
|
122
|
+
...base,
|
|
123
|
+
src: typeof raw.src === "string" ? raw.src : "",
|
|
124
|
+
alt: typeof raw.alt === "string" ? raw.alt : "",
|
|
125
|
+
correctTargetId: typeof raw.correctTargetId === "string" ? raw.correctTargetId : ""
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (kind === "findMultipleHotspots") {
|
|
129
|
+
return {
|
|
130
|
+
kind: "findMultipleHotspots",
|
|
131
|
+
...base,
|
|
132
|
+
src: typeof raw.src === "string" ? raw.src : "",
|
|
133
|
+
alt: typeof raw.alt === "string" ? raw.alt : "",
|
|
134
|
+
correctTargetIds: Array.isArray(raw.correctTargetIds) ? raw.correctTargetIds.filter((id) => typeof id === "string") : []
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
kind: kind === "mcq" ? "mcq" : void 0,
|
|
139
|
+
...base,
|
|
140
|
+
choices: Array.isArray(raw.choices) ? raw.choices.filter((c) => typeof c === "string") : [],
|
|
141
|
+
answer: typeof raw.answer === "string" ? raw.answer : ""
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function parseCourseDescriptorInput(input) {
|
|
145
|
+
if (!isRecord(input)) return null;
|
|
146
|
+
const trackingRaw = input.tracking;
|
|
147
|
+
let tracking;
|
|
148
|
+
if (isRecord(trackingRaw)) {
|
|
149
|
+
const completionRaw = trackingRaw.completion;
|
|
150
|
+
const xapiRaw = trackingRaw.xapi;
|
|
151
|
+
tracking = {
|
|
152
|
+
completion: isRecord(completionRaw) ? {
|
|
153
|
+
threshold: typeof completionRaw.threshold === "number" ? completionRaw.threshold : void 0
|
|
154
|
+
} : void 0,
|
|
155
|
+
xapi: isRecord(xapiRaw) ? {
|
|
156
|
+
activityIri: typeof xapiRaw.activityIri === "string" ? xapiRaw.activityIri : void 0
|
|
157
|
+
} : void 0
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const themeRaw = input.theme;
|
|
161
|
+
let theme;
|
|
162
|
+
if (isRecord(themeRaw)) {
|
|
163
|
+
theme = {
|
|
164
|
+
preset: typeof themeRaw.preset === "string" ? themeRaw.preset : void 0
|
|
165
|
+
};
|
|
166
|
+
if (isRecord(themeRaw.theme)) {
|
|
167
|
+
theme.theme = themeRaw.theme;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
courseId: typeof input.courseId === "string" ? input.courseId : "",
|
|
172
|
+
title: typeof input.title === "string" ? input.title : "",
|
|
173
|
+
version: typeof input.version === "string" ? input.version : void 0,
|
|
174
|
+
layout: typeof input.layout === "string" ? input.layout : void 0,
|
|
175
|
+
lessons: Array.isArray(input.lessons) ? input.lessons.map(parseLessonDescriptor) : [],
|
|
176
|
+
assessments: Array.isArray(input.assessments) ? input.assessments.map(parseAssessmentDescriptor) : void 0,
|
|
177
|
+
theme,
|
|
178
|
+
tracking,
|
|
179
|
+
spaDistDir: typeof input.spaDistDir === "string" ? input.spaDistDir : void 0,
|
|
180
|
+
spaLessonId: typeof input.spaLessonId === "string" ? input.spaLessonId : void 0
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/descriptor/validateCourse.ts
|
|
185
|
+
import { validateId as validateId3 } from "@lessonkit/core";
|
|
7
186
|
|
|
8
187
|
// src/spaPath.ts
|
|
9
188
|
import { realpathSync } from "fs";
|
|
@@ -28,7 +207,8 @@ function assertResolvedPathUnderRoot(root, target) {
|
|
|
28
207
|
const targetResolved = resolveComparablePath(target);
|
|
29
208
|
const prefix = rootResolved.endsWith(sep) ? rootResolved : rootResolved + sep;
|
|
30
209
|
const win32Prefix = rootResolved.endsWith(win32.sep) ? rootResolved : rootResolved + win32.sep;
|
|
31
|
-
if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix) &&
|
|
210
|
+
if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix) && /* v8 ignore next */
|
|
211
|
+
!targetResolved.startsWith(win32Prefix)) {
|
|
32
212
|
throw new Error(`unsafe path escapes project root: ${target}`);
|
|
33
213
|
}
|
|
34
214
|
}
|
|
@@ -89,133 +269,69 @@ function themeToLxpackRuntime(input) {
|
|
|
89
269
|
};
|
|
90
270
|
}
|
|
91
271
|
|
|
92
|
-
// src/
|
|
93
|
-
|
|
94
|
-
var
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
function parseLessonDescriptor(raw) {
|
|
99
|
-
if (!isRecord(raw)) {
|
|
100
|
-
return { id: "", title: "" };
|
|
272
|
+
// src/descriptor/validateAssessments.ts
|
|
273
|
+
import { validateId as validateId2 } from "@lessonkit/core";
|
|
274
|
+
var validateMcqLike = (assessment, path, issues) => {
|
|
275
|
+
if (!("choices" in assessment) || !("answer" in assessment) || typeof assessment.answer !== "string") {
|
|
276
|
+
return;
|
|
101
277
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
spaPath: typeof raw.spaPath === "string" ? raw.spaPath : void 0
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
function parseAssessmentDescriptor(raw) {
|
|
109
|
-
if (!isRecord(raw)) {
|
|
110
|
-
return { checkId: "", question: "", choices: [], answer: "" };
|
|
278
|
+
const trimmedChoices = assessment.choices.map((c) => c.trim()).filter((c) => c.length > 0);
|
|
279
|
+
if (!trimmedChoices.length) {
|
|
280
|
+
issues.push({ path: `${path}.choices`, message: "at least one non-empty choice is required" });
|
|
111
281
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
answer: typeof raw.answer === "string" ? raw.answer : "",
|
|
117
|
-
passingScore: typeof raw.passingScore === "number" ? raw.passingScore : void 0
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
function parseCourseDescriptorInput(input) {
|
|
121
|
-
if (!isRecord(input)) return null;
|
|
122
|
-
const trackingRaw = input.tracking;
|
|
123
|
-
let tracking;
|
|
124
|
-
if (isRecord(trackingRaw)) {
|
|
125
|
-
const completionRaw = trackingRaw.completion;
|
|
126
|
-
const xapiRaw = trackingRaw.xapi;
|
|
127
|
-
tracking = {
|
|
128
|
-
completion: isRecord(completionRaw) ? {
|
|
129
|
-
threshold: typeof completionRaw.threshold === "number" ? completionRaw.threshold : void 0
|
|
130
|
-
} : void 0,
|
|
131
|
-
xapi: isRecord(xapiRaw) ? {
|
|
132
|
-
activityIri: typeof xapiRaw.activityIri === "string" ? xapiRaw.activityIri : void 0
|
|
133
|
-
} : void 0
|
|
134
|
-
};
|
|
282
|
+
if (!assessment.answer.trim()) {
|
|
283
|
+
issues.push({ path: `${path}.answer`, message: "answer is required" });
|
|
284
|
+
} else if (trimmedChoices.length && !trimmedChoices.includes(assessment.answer.trim())) {
|
|
285
|
+
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
135
286
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (isRecord(themeRaw.theme)) {
|
|
143
|
-
theme.theme = themeRaw.theme;
|
|
287
|
+
};
|
|
288
|
+
var ASSESSMENT_VALIDATORS = {
|
|
289
|
+
mcq: validateMcqLike,
|
|
290
|
+
trueFalse: (assessment, path, issues) => {
|
|
291
|
+
if (assessment.kind === "trueFalse" && typeof assessment.answer !== "boolean") {
|
|
292
|
+
issues.push({ path: `${path}.answer`, message: "answer must be a boolean for trueFalse" });
|
|
144
293
|
}
|
|
294
|
+
},
|
|
295
|
+
fillInBlanks: (assessment, path, issues) => {
|
|
296
|
+
if (assessment.kind === "fillInBlanks" && !assessment.template?.trim()) {
|
|
297
|
+
issues.push({ path: `${path}.template`, message: "template is required for fillInBlanks" });
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
findHotspot: () => {
|
|
301
|
+
},
|
|
302
|
+
findMultipleHotspots: () => {
|
|
145
303
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
lessons: input.lessons.map((lesson) => {
|
|
169
|
-
const idResult = validateId(lesson.id, "lessonId");
|
|
170
|
-
if (!idResult.ok) throw new Error("normalizeDescriptor called with invalid lesson id");
|
|
171
|
-
return {
|
|
172
|
-
...lesson,
|
|
173
|
-
id: idResult.id,
|
|
174
|
-
title: lesson.title.trim(),
|
|
175
|
-
spaPath: lesson.spaPath?.trim() || void 0
|
|
176
|
-
};
|
|
177
|
-
}),
|
|
178
|
-
assessments: input.assessments?.map((assessment) => {
|
|
179
|
-
const check = validateId(assessment.checkId, "checkId");
|
|
180
|
-
if (!check.ok) throw new Error("normalizeDescriptor called with invalid checkId");
|
|
181
|
-
return {
|
|
182
|
-
...assessment,
|
|
183
|
-
checkId: check.id,
|
|
184
|
-
question: assessment.question.trim(),
|
|
185
|
-
choices: assessment.choices.map((c) => c.trim()).filter((c) => c.length > 0),
|
|
186
|
-
answer: assessment.answer.trim()
|
|
187
|
-
};
|
|
188
|
-
})
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
function validateDescriptor(input) {
|
|
192
|
-
const parsed = parseCourseDescriptorInput(input);
|
|
193
|
-
if (parsed === null) {
|
|
194
|
-
return { ok: false, issues: [{ path: "course", message: "must be an object" }] };
|
|
195
|
-
}
|
|
196
|
-
return validateDescriptorParsed(parsed);
|
|
197
|
-
}
|
|
198
|
-
function validateDescriptorForTarget(input, target) {
|
|
199
|
-
const result = validateDescriptor(input);
|
|
200
|
-
if (!result.ok || !target) return result;
|
|
201
|
-
if (target !== "xapi" && target !== "cmi5") return result;
|
|
202
|
-
const activityIri = result.descriptor.tracking?.xapi?.activityIri?.trim();
|
|
203
|
-
if (!activityIri) {
|
|
204
|
-
return {
|
|
205
|
-
ok: false,
|
|
206
|
-
issues: [
|
|
207
|
-
{
|
|
208
|
-
path: "course.tracking.xapi.activityIri",
|
|
209
|
-
message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
|
|
210
|
-
}
|
|
211
|
-
]
|
|
212
|
-
};
|
|
304
|
+
};
|
|
305
|
+
function validateAssessmentEntry(assessment, index, issues, checkIds) {
|
|
306
|
+
const path = `assessments[${index}]`;
|
|
307
|
+
const check = validateId2(assessment.checkId, `${path}.checkId`);
|
|
308
|
+
if (!check.ok) {
|
|
309
|
+
issues.push(...check.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
310
|
+
} else if (checkIds.has(check.id)) {
|
|
311
|
+
issues.push({ path: `${path}.checkId`, message: "duplicate checkId" });
|
|
312
|
+
} else {
|
|
313
|
+
checkIds.add(check.id);
|
|
314
|
+
}
|
|
315
|
+
if (!assessment.question?.trim()) {
|
|
316
|
+
issues.push({ path: `${path}.question`, message: "question is required" });
|
|
317
|
+
}
|
|
318
|
+
const kind = assessment.kind ?? "mcq";
|
|
319
|
+
ASSESSMENT_VALIDATORS[kind](assessment, path, issues);
|
|
320
|
+
const passingScore = assessment.passingScore;
|
|
321
|
+
if (passingScore !== void 0 && !(Number.isFinite(passingScore) && passingScore > 0)) {
|
|
322
|
+
issues.push({
|
|
323
|
+
path: `${path}.passingScore`,
|
|
324
|
+
message: "passingScore must be greater than 0 (absolute point threshold)"
|
|
325
|
+
});
|
|
213
326
|
}
|
|
214
|
-
return result;
|
|
215
327
|
}
|
|
216
|
-
|
|
328
|
+
|
|
329
|
+
// src/descriptor/validateCourse.ts
|
|
330
|
+
var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
|
|
331
|
+
var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
|
|
332
|
+
function validateCourseDescriptor(input) {
|
|
217
333
|
const issues = [];
|
|
218
|
-
const course =
|
|
334
|
+
const course = validateId3(input.courseId, "courseId");
|
|
219
335
|
if (!course.ok) issues.push(...course.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
220
336
|
if (!input.title?.trim()) {
|
|
221
337
|
issues.push({ path: "title", message: "title is required" });
|
|
@@ -268,7 +384,7 @@ function validateDescriptorParsed(input) {
|
|
|
268
384
|
const spaPaths = /* @__PURE__ */ new Set();
|
|
269
385
|
for (const [index, lesson] of (input.lessons ?? []).entries()) {
|
|
270
386
|
const path = `lessons[${index}]`;
|
|
271
|
-
const lessonResult =
|
|
387
|
+
const lessonResult = validateId3(lesson.id, `${path}.id`);
|
|
272
388
|
if (!lessonResult.ok) {
|
|
273
389
|
issues.push(...lessonResult.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
274
390
|
} else if (lessonIds.has(lessonResult.id)) {
|
|
@@ -300,7 +416,7 @@ function validateDescriptorParsed(input) {
|
|
|
300
416
|
}
|
|
301
417
|
if (layout === "single-spa" && input.spaLessonId?.trim()) {
|
|
302
418
|
const spaId = input.spaLessonId.trim();
|
|
303
|
-
const spaResult =
|
|
419
|
+
const spaResult = validateId3(spaId, "spaLessonId");
|
|
304
420
|
if (!spaResult.ok) {
|
|
305
421
|
issues.push(...spaResult.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
306
422
|
} else if (!lessonIds.has(spaResult.id)) {
|
|
@@ -312,41 +428,48 @@ function validateDescriptorParsed(input) {
|
|
|
312
428
|
}
|
|
313
429
|
const checkIds = /* @__PURE__ */ new Set();
|
|
314
430
|
for (const [index, assessment] of (input.assessments ?? []).entries()) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
message: "at least one non-empty choice is required"
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
if (!assessment.answer?.trim()) {
|
|
335
|
-
issues.push({ path: `${path}.answer`, message: "answer is required" });
|
|
336
|
-
} else if (trimmedChoices.length && !trimmedChoices.includes(assessment.answer.trim())) {
|
|
337
|
-
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
338
|
-
}
|
|
339
|
-
const passingScore = assessment.passingScore;
|
|
340
|
-
if (passingScore !== void 0 && !(Number.isFinite(passingScore) && passingScore > 0)) {
|
|
341
|
-
issues.push({
|
|
342
|
-
path: `${path}.passingScore`,
|
|
343
|
-
message: "passingScore must be greater than 0 (absolute point threshold)"
|
|
344
|
-
});
|
|
345
|
-
}
|
|
431
|
+
validateAssessmentEntry(assessment, index, issues, checkIds);
|
|
432
|
+
}
|
|
433
|
+
return issues;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/descriptor/validateForTarget.ts
|
|
437
|
+
function validateDescriptorForExportTarget(descriptor, target) {
|
|
438
|
+
if (target !== "xapi" && target !== "cmi5") return [];
|
|
439
|
+
const activityIri = descriptor.tracking?.xapi?.activityIri?.trim();
|
|
440
|
+
if (!activityIri) {
|
|
441
|
+
return [
|
|
442
|
+
{
|
|
443
|
+
path: "course.tracking.xapi.activityIri",
|
|
444
|
+
message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
|
|
445
|
+
}
|
|
446
|
+
];
|
|
346
447
|
}
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/validateDescriptor.ts
|
|
452
|
+
function validateDescriptorParsed(input) {
|
|
453
|
+
const issues = validateCourseDescriptor(input);
|
|
347
454
|
if (issues.length) return { ok: false, issues };
|
|
348
455
|
return { ok: true, descriptor: normalizeDescriptor(input) };
|
|
349
456
|
}
|
|
457
|
+
function validateDescriptor(input) {
|
|
458
|
+
const parsed = parseCourseDescriptorInput(input);
|
|
459
|
+
if (parsed === null) {
|
|
460
|
+
return { ok: false, issues: [{ path: "course", message: "must be an object" }] };
|
|
461
|
+
}
|
|
462
|
+
return validateDescriptorParsed(parsed);
|
|
463
|
+
}
|
|
464
|
+
function validateDescriptorForTarget(input, target) {
|
|
465
|
+
const result = validateDescriptor(input);
|
|
466
|
+
if (!result.ok || !target) return result;
|
|
467
|
+
const targetIssues = validateDescriptorForExportTarget(result.descriptor, target);
|
|
468
|
+
if (targetIssues.length) {
|
|
469
|
+
return { ok: false, issues: targetIssues };
|
|
470
|
+
}
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
350
473
|
|
|
351
474
|
// src/validateProjectPaths.ts
|
|
352
475
|
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
@@ -417,7 +540,7 @@ function slugChoiceId(text, index) {
|
|
|
417
540
|
const stem = base.length ? base : "choice";
|
|
418
541
|
return `${stem}-${index + 1}`;
|
|
419
542
|
}
|
|
420
|
-
function
|
|
543
|
+
function mcqToLxpack(assessment) {
|
|
421
544
|
const choices = assessment.choices.map((text, index) => {
|
|
422
545
|
const id = slugChoiceId(text, index);
|
|
423
546
|
return {
|
|
@@ -438,8 +561,43 @@ function assessmentDescriptorToLxpack(assessment) {
|
|
|
438
561
|
]
|
|
439
562
|
};
|
|
440
563
|
}
|
|
564
|
+
function assessmentDescriptorToLxpack(assessment) {
|
|
565
|
+
const kind = assessment.kind ?? "mcq";
|
|
566
|
+
if (kind === "trueFalse" && assessment.kind === "trueFalse") {
|
|
567
|
+
const choices = ["True", "False"];
|
|
568
|
+
const answerText = assessment.answer ? "True" : "False";
|
|
569
|
+
return mcqToLxpack({
|
|
570
|
+
kind: "mcq",
|
|
571
|
+
checkId: assessment.checkId,
|
|
572
|
+
question: assessment.question,
|
|
573
|
+
choices,
|
|
574
|
+
answer: answerText,
|
|
575
|
+
passingScore: assessment.passingScore
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
if (kind === "fillInBlanks") {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
if (kind === "findHotspot" && assessment.kind === "findHotspot") {
|
|
582
|
+
return mcqToLxpack({
|
|
583
|
+
kind: "mcq",
|
|
584
|
+
checkId: assessment.checkId,
|
|
585
|
+
question: assessment.question,
|
|
586
|
+
choices: [assessment.correctTargetId, "other"],
|
|
587
|
+
answer: assessment.correctTargetId,
|
|
588
|
+
passingScore: assessment.passingScore
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
if (kind === "findMultipleHotspots") {
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
if ("choices" in assessment && "answer" in assessment && typeof assessment.answer === "string") {
|
|
595
|
+
return mcqToLxpack(assessment);
|
|
596
|
+
}
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
441
599
|
function extractAssessments(descriptor) {
|
|
442
|
-
return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack);
|
|
600
|
+
return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack).filter((a) => a !== null);
|
|
443
601
|
}
|
|
444
602
|
|
|
445
603
|
// src/interchange.ts
|
|
@@ -512,7 +670,8 @@ async function resolveSpaDirs(options) {
|
|
|
512
670
|
const { descriptor, spaDistDir, lessonSpaDirs, projectRoot } = options;
|
|
513
671
|
const spaLessons = resolveSpaLessons(descriptor);
|
|
514
672
|
if (descriptor.layout === "single-spa") {
|
|
515
|
-
const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ??
|
|
673
|
+
const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? /* v8 ignore next */
|
|
674
|
+
"dist";
|
|
516
675
|
const srcDist = projectRoot ? resolve3(projectRoot, spaDistRelative) : resolve3(spaDistRelative);
|
|
517
676
|
if (projectRoot) {
|
|
518
677
|
assertRealPathUnderRoot(resolve3(projectRoot), srcDist);
|
|
@@ -527,7 +686,8 @@ async function resolveSpaDirs(options) {
|
|
|
527
686
|
} catch {
|
|
528
687
|
throw new Error(`spaDistDir must contain index.html: ${join(srcDist, "index.html")}`);
|
|
529
688
|
}
|
|
530
|
-
const lessonId = spaLessons[0]?.id ??
|
|
689
|
+
const lessonId = spaLessons[0]?.id ?? /* v8 ignore next */
|
|
690
|
+
"main";
|
|
531
691
|
return { [lessonId]: srcDist };
|
|
532
692
|
}
|
|
533
693
|
const dirs = {};
|
|
@@ -614,7 +774,15 @@ function validatePackageInputs(options) {
|
|
|
614
774
|
ok: false,
|
|
615
775
|
courseDir: outDir,
|
|
616
776
|
target,
|
|
617
|
-
issues: [
|
|
777
|
+
issues: [
|
|
778
|
+
{
|
|
779
|
+
path: "outDir",
|
|
780
|
+
message: (
|
|
781
|
+
/* v8 ignore next */
|
|
782
|
+
err instanceof Error ? err.message : String(err)
|
|
783
|
+
)
|
|
784
|
+
}
|
|
785
|
+
]
|
|
618
786
|
};
|
|
619
787
|
}
|
|
620
788
|
}
|
|
@@ -646,7 +814,10 @@ function validatePackageInputs(options) {
|
|
|
646
814
|
issues: [
|
|
647
815
|
{
|
|
648
816
|
path: "outputBaseDir",
|
|
649
|
-
message:
|
|
817
|
+
message: (
|
|
818
|
+
/* v8 ignore next */
|
|
819
|
+
err instanceof Error ? err.message : String(err)
|
|
820
|
+
)
|
|
650
821
|
}
|
|
651
822
|
]
|
|
652
823
|
};
|
|
@@ -661,7 +832,15 @@ function validatePackageInputs(options) {
|
|
|
661
832
|
ok: false,
|
|
662
833
|
courseDir: outDir,
|
|
663
834
|
target,
|
|
664
|
-
issues: [
|
|
835
|
+
issues: [
|
|
836
|
+
{
|
|
837
|
+
path: "output",
|
|
838
|
+
message: (
|
|
839
|
+
/* v8 ignore next */
|
|
840
|
+
err instanceof Error ? err.message : String(err)
|
|
841
|
+
)
|
|
842
|
+
}
|
|
843
|
+
]
|
|
665
844
|
};
|
|
666
845
|
}
|
|
667
846
|
}
|
|
@@ -750,7 +929,10 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
750
929
|
try {
|
|
751
930
|
await renameOrCopy(tmpPromote, failedPromote2);
|
|
752
931
|
} catch {
|
|
753
|
-
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
932
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
933
|
+
/* v8 ignore next */
|
|
934
|
+
() => void 0
|
|
935
|
+
);
|
|
754
936
|
}
|
|
755
937
|
const promoteMsg = promoteError instanceof Error ? promoteError.message : String(promoteError);
|
|
756
938
|
const restoreMsg = restoreError instanceof Error ? restoreError.message : String(restoreError);
|
|
@@ -766,7 +948,10 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
766
948
|
`[lessonkit/lxpack] failed to restore ${stagingDir} after promote error:`,
|
|
767
949
|
restoreError instanceof Error ? restoreError.message : restoreError
|
|
768
950
|
);
|
|
769
|
-
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
951
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
952
|
+
/* v8 ignore next */
|
|
953
|
+
() => void 0
|
|
954
|
+
);
|
|
770
955
|
}
|
|
771
956
|
throw promoteError;
|
|
772
957
|
}
|
|
@@ -774,12 +959,18 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
774
959
|
try {
|
|
775
960
|
await renameOrCopy(tmpPromote, failedPromote);
|
|
776
961
|
} catch {
|
|
777
|
-
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
962
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
963
|
+
/* v8 ignore next */
|
|
964
|
+
() => void 0
|
|
965
|
+
);
|
|
778
966
|
}
|
|
779
967
|
throw promoteError;
|
|
780
968
|
}
|
|
781
969
|
if (backup) {
|
|
782
|
-
await fsp.rm(backup, { recursive: true, force: true }).catch(
|
|
970
|
+
await fsp.rm(backup, { recursive: true, force: true }).catch(
|
|
971
|
+
/* v8 ignore next */
|
|
972
|
+
() => void 0
|
|
973
|
+
);
|
|
783
974
|
}
|
|
784
975
|
}
|
|
785
976
|
|
|
@@ -842,7 +1033,10 @@ async function buildStagingPackage(options) {
|
|
|
842
1033
|
outputDir: "outputDir" in build ? build.outputDir : void 0
|
|
843
1034
|
};
|
|
844
1035
|
} catch (err) {
|
|
845
|
-
await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1036
|
+
await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1037
|
+
/* v8 ignore next */
|
|
1038
|
+
() => void 0
|
|
1039
|
+
);
|
|
846
1040
|
throw err;
|
|
847
1041
|
}
|
|
848
1042
|
}
|
|
@@ -908,7 +1102,10 @@ async function packageLessonkitCourse(options) {
|
|
|
908
1102
|
outputBaseDir
|
|
909
1103
|
});
|
|
910
1104
|
if (!staged.ok) {
|
|
911
|
-
await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(
|
|
1105
|
+
await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(
|
|
1106
|
+
/* v8 ignore next */
|
|
1107
|
+
() => void 0
|
|
1108
|
+
);
|
|
912
1109
|
const validation2 = staged.build ? { ok: false, issues: staged.build.issues } : void 0;
|
|
913
1110
|
return {
|
|
914
1111
|
ok: false,
|
|
@@ -926,7 +1123,10 @@ async function packageLessonkitCourse(options) {
|
|
|
926
1123
|
validateArtifactInStaging(stagingRoot, staged.outputDir, "outputDir")
|
|
927
1124
|
].filter((issue) => issue != null);
|
|
928
1125
|
if (artifactIssues.length > 0) {
|
|
929
|
-
await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1126
|
+
await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1127
|
+
/* v8 ignore next */
|
|
1128
|
+
() => void 0
|
|
1129
|
+
);
|
|
930
1130
|
return {
|
|
931
1131
|
ok: false,
|
|
932
1132
|
courseDir: outDir,
|
|
@@ -1028,7 +1228,10 @@ function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
|
|
|
1028
1228
|
if (!validation.ok) {
|
|
1029
1229
|
for (const i of validation.issues) {
|
|
1030
1230
|
issues.push({
|
|
1031
|
-
path:
|
|
1231
|
+
path: (
|
|
1232
|
+
/* v8 ignore next */
|
|
1233
|
+
i.path.startsWith("course.") ? i.path : `course.${i.path}`
|
|
1234
|
+
),
|
|
1032
1235
|
message: i.message
|
|
1033
1236
|
});
|
|
1034
1237
|
}
|