@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.cjs
CHANGED
|
@@ -61,8 +61,187 @@ __export(index_exports, {
|
|
|
61
61
|
});
|
|
62
62
|
module.exports = __toCommonJS(index_exports);
|
|
63
63
|
|
|
64
|
-
// src/
|
|
64
|
+
// src/descriptor/normalize.ts
|
|
65
65
|
var import_core = require("@lessonkit/core");
|
|
66
|
+
function normalizeDescriptor(input) {
|
|
67
|
+
const course = (0, import_core.validateId)(input.courseId, "courseId");
|
|
68
|
+
if (!course.ok) throw new Error("normalizeDescriptor called with invalid courseId");
|
|
69
|
+
return {
|
|
70
|
+
...input,
|
|
71
|
+
courseId: course.id,
|
|
72
|
+
title: input.title.trim(),
|
|
73
|
+
version: input.version?.trim() || void 0,
|
|
74
|
+
spaLessonId: input.spaLessonId?.trim() || void 0,
|
|
75
|
+
lessons: input.lessons.map((lesson) => {
|
|
76
|
+
const idResult = (0, import_core.validateId)(lesson.id, "lessonId");
|
|
77
|
+
if (!idResult.ok) throw new Error("normalizeDescriptor called with invalid lesson id");
|
|
78
|
+
return {
|
|
79
|
+
...lesson,
|
|
80
|
+
id: idResult.id,
|
|
81
|
+
title: lesson.title.trim(),
|
|
82
|
+
spaPath: lesson.spaPath?.trim() || void 0
|
|
83
|
+
};
|
|
84
|
+
}),
|
|
85
|
+
assessments: input.assessments?.map((assessment) => {
|
|
86
|
+
const check = (0, import_core.validateId)(assessment.checkId, "checkId");
|
|
87
|
+
if (!check.ok) throw new Error("normalizeDescriptor called with invalid checkId");
|
|
88
|
+
const question = assessment.question.trim();
|
|
89
|
+
if (assessment.kind === "trueFalse") {
|
|
90
|
+
return { ...assessment, checkId: check.id, question };
|
|
91
|
+
}
|
|
92
|
+
if (assessment.kind === "fillInBlanks") {
|
|
93
|
+
return {
|
|
94
|
+
...assessment,
|
|
95
|
+
checkId: check.id,
|
|
96
|
+
question,
|
|
97
|
+
template: assessment.template.trim(),
|
|
98
|
+
blanks: assessment.blanks?.map((b) => ({
|
|
99
|
+
id: b.id.trim(),
|
|
100
|
+
answer: b.answer.trim()
|
|
101
|
+
}))
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (assessment.kind === "findHotspot") {
|
|
105
|
+
return {
|
|
106
|
+
...assessment,
|
|
107
|
+
checkId: check.id,
|
|
108
|
+
question,
|
|
109
|
+
src: assessment.src.trim(),
|
|
110
|
+
alt: assessment.alt.trim(),
|
|
111
|
+
correctTargetId: assessment.correctTargetId.trim()
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (assessment.kind === "findMultipleHotspots") {
|
|
115
|
+
return {
|
|
116
|
+
...assessment,
|
|
117
|
+
checkId: check.id,
|
|
118
|
+
question,
|
|
119
|
+
src: assessment.src.trim(),
|
|
120
|
+
alt: assessment.alt.trim(),
|
|
121
|
+
correctTargetIds: assessment.correctTargetIds.map((id) => id.trim()).filter((id) => id.length > 0)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const mcq = assessment;
|
|
125
|
+
return {
|
|
126
|
+
...mcq,
|
|
127
|
+
checkId: check.id,
|
|
128
|
+
question,
|
|
129
|
+
choices: mcq.choices.map((c) => c.trim()).filter((c) => c.length > 0),
|
|
130
|
+
answer: mcq.answer.trim()
|
|
131
|
+
};
|
|
132
|
+
})
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/descriptor/parseInput.ts
|
|
137
|
+
function isRecord(value) {
|
|
138
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
139
|
+
}
|
|
140
|
+
function parseLessonDescriptor(raw) {
|
|
141
|
+
if (!isRecord(raw)) {
|
|
142
|
+
return { id: "", title: "" };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
id: typeof raw.id === "string" ? raw.id : "",
|
|
146
|
+
title: typeof raw.title === "string" ? raw.title : "",
|
|
147
|
+
spaPath: typeof raw.spaPath === "string" ? raw.spaPath : void 0
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function parseAssessmentDescriptor(raw) {
|
|
151
|
+
if (!isRecord(raw)) {
|
|
152
|
+
return { checkId: "", question: "", choices: [], answer: "" };
|
|
153
|
+
}
|
|
154
|
+
const base = {
|
|
155
|
+
checkId: typeof raw.checkId === "string" ? raw.checkId : "",
|
|
156
|
+
question: typeof raw.question === "string" ? raw.question : "",
|
|
157
|
+
passingScore: typeof raw.passingScore === "number" ? raw.passingScore : void 0
|
|
158
|
+
};
|
|
159
|
+
const kind = raw.kind;
|
|
160
|
+
if (kind === "trueFalse") {
|
|
161
|
+
return {
|
|
162
|
+
kind: "trueFalse",
|
|
163
|
+
...base,
|
|
164
|
+
answer: typeof raw.answer === "boolean" ? raw.answer : raw.answer === "true"
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (kind === "fillInBlanks") {
|
|
168
|
+
return {
|
|
169
|
+
kind: "fillInBlanks",
|
|
170
|
+
...base,
|
|
171
|
+
template: typeof raw.template === "string" ? raw.template : "",
|
|
172
|
+
blanks: Array.isArray(raw.blanks) ? raw.blanks.filter((b) => isRecord(b)).map((b) => ({
|
|
173
|
+
id: typeof b.id === "string" ? b.id : "",
|
|
174
|
+
answer: typeof b.answer === "string" ? b.answer : ""
|
|
175
|
+
})) : void 0
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (kind === "findHotspot") {
|
|
179
|
+
return {
|
|
180
|
+
kind: "findHotspot",
|
|
181
|
+
...base,
|
|
182
|
+
src: typeof raw.src === "string" ? raw.src : "",
|
|
183
|
+
alt: typeof raw.alt === "string" ? raw.alt : "",
|
|
184
|
+
correctTargetId: typeof raw.correctTargetId === "string" ? raw.correctTargetId : ""
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (kind === "findMultipleHotspots") {
|
|
188
|
+
return {
|
|
189
|
+
kind: "findMultipleHotspots",
|
|
190
|
+
...base,
|
|
191
|
+
src: typeof raw.src === "string" ? raw.src : "",
|
|
192
|
+
alt: typeof raw.alt === "string" ? raw.alt : "",
|
|
193
|
+
correctTargetIds: Array.isArray(raw.correctTargetIds) ? raw.correctTargetIds.filter((id) => typeof id === "string") : []
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
kind: kind === "mcq" ? "mcq" : void 0,
|
|
198
|
+
...base,
|
|
199
|
+
choices: Array.isArray(raw.choices) ? raw.choices.filter((c) => typeof c === "string") : [],
|
|
200
|
+
answer: typeof raw.answer === "string" ? raw.answer : ""
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function parseCourseDescriptorInput(input) {
|
|
204
|
+
if (!isRecord(input)) return null;
|
|
205
|
+
const trackingRaw = input.tracking;
|
|
206
|
+
let tracking;
|
|
207
|
+
if (isRecord(trackingRaw)) {
|
|
208
|
+
const completionRaw = trackingRaw.completion;
|
|
209
|
+
const xapiRaw = trackingRaw.xapi;
|
|
210
|
+
tracking = {
|
|
211
|
+
completion: isRecord(completionRaw) ? {
|
|
212
|
+
threshold: typeof completionRaw.threshold === "number" ? completionRaw.threshold : void 0
|
|
213
|
+
} : void 0,
|
|
214
|
+
xapi: isRecord(xapiRaw) ? {
|
|
215
|
+
activityIri: typeof xapiRaw.activityIri === "string" ? xapiRaw.activityIri : void 0
|
|
216
|
+
} : void 0
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const themeRaw = input.theme;
|
|
220
|
+
let theme;
|
|
221
|
+
if (isRecord(themeRaw)) {
|
|
222
|
+
theme = {
|
|
223
|
+
preset: typeof themeRaw.preset === "string" ? themeRaw.preset : void 0
|
|
224
|
+
};
|
|
225
|
+
if (isRecord(themeRaw.theme)) {
|
|
226
|
+
theme.theme = themeRaw.theme;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
courseId: typeof input.courseId === "string" ? input.courseId : "",
|
|
231
|
+
title: typeof input.title === "string" ? input.title : "",
|
|
232
|
+
version: typeof input.version === "string" ? input.version : void 0,
|
|
233
|
+
layout: typeof input.layout === "string" ? input.layout : void 0,
|
|
234
|
+
lessons: Array.isArray(input.lessons) ? input.lessons.map(parseLessonDescriptor) : [],
|
|
235
|
+
assessments: Array.isArray(input.assessments) ? input.assessments.map(parseAssessmentDescriptor) : void 0,
|
|
236
|
+
theme,
|
|
237
|
+
tracking,
|
|
238
|
+
spaDistDir: typeof input.spaDistDir === "string" ? input.spaDistDir : void 0,
|
|
239
|
+
spaLessonId: typeof input.spaLessonId === "string" ? input.spaLessonId : void 0
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/descriptor/validateCourse.ts
|
|
244
|
+
var import_core3 = require("@lessonkit/core");
|
|
66
245
|
|
|
67
246
|
// src/spaPath.ts
|
|
68
247
|
var import_node_fs = require("fs");
|
|
@@ -87,7 +266,8 @@ function assertResolvedPathUnderRoot(root, target) {
|
|
|
87
266
|
const targetResolved = resolveComparablePath(target);
|
|
88
267
|
const prefix = rootResolved.endsWith(import_node_path.sep) ? rootResolved : rootResolved + import_node_path.sep;
|
|
89
268
|
const win32Prefix = rootResolved.endsWith(import_node_path.win32.sep) ? rootResolved : rootResolved + import_node_path.win32.sep;
|
|
90
|
-
if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix) &&
|
|
269
|
+
if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix) && /* v8 ignore next */
|
|
270
|
+
!targetResolved.startsWith(win32Prefix)) {
|
|
91
271
|
throw new Error(`unsafe path escapes project root: ${target}`);
|
|
92
272
|
}
|
|
93
273
|
}
|
|
@@ -148,133 +328,69 @@ function themeToLxpackRuntime(input) {
|
|
|
148
328
|
};
|
|
149
329
|
}
|
|
150
330
|
|
|
151
|
-
// src/
|
|
152
|
-
var
|
|
153
|
-
var
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
function parseLessonDescriptor(raw) {
|
|
158
|
-
if (!isRecord(raw)) {
|
|
159
|
-
return { id: "", title: "" };
|
|
331
|
+
// src/descriptor/validateAssessments.ts
|
|
332
|
+
var import_core2 = require("@lessonkit/core");
|
|
333
|
+
var validateMcqLike = (assessment, path, issues) => {
|
|
334
|
+
if (!("choices" in assessment) || !("answer" in assessment) || typeof assessment.answer !== "string") {
|
|
335
|
+
return;
|
|
160
336
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
spaPath: typeof raw.spaPath === "string" ? raw.spaPath : void 0
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
function parseAssessmentDescriptor(raw) {
|
|
168
|
-
if (!isRecord(raw)) {
|
|
169
|
-
return { checkId: "", question: "", choices: [], answer: "" };
|
|
337
|
+
const trimmedChoices = assessment.choices.map((c) => c.trim()).filter((c) => c.length > 0);
|
|
338
|
+
if (!trimmedChoices.length) {
|
|
339
|
+
issues.push({ path: `${path}.choices`, message: "at least one non-empty choice is required" });
|
|
170
340
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
answer: typeof raw.answer === "string" ? raw.answer : "",
|
|
176
|
-
passingScore: typeof raw.passingScore === "number" ? raw.passingScore : void 0
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
function parseCourseDescriptorInput(input) {
|
|
180
|
-
if (!isRecord(input)) return null;
|
|
181
|
-
const trackingRaw = input.tracking;
|
|
182
|
-
let tracking;
|
|
183
|
-
if (isRecord(trackingRaw)) {
|
|
184
|
-
const completionRaw = trackingRaw.completion;
|
|
185
|
-
const xapiRaw = trackingRaw.xapi;
|
|
186
|
-
tracking = {
|
|
187
|
-
completion: isRecord(completionRaw) ? {
|
|
188
|
-
threshold: typeof completionRaw.threshold === "number" ? completionRaw.threshold : void 0
|
|
189
|
-
} : void 0,
|
|
190
|
-
xapi: isRecord(xapiRaw) ? {
|
|
191
|
-
activityIri: typeof xapiRaw.activityIri === "string" ? xapiRaw.activityIri : void 0
|
|
192
|
-
} : void 0
|
|
193
|
-
};
|
|
341
|
+
if (!assessment.answer.trim()) {
|
|
342
|
+
issues.push({ path: `${path}.answer`, message: "answer is required" });
|
|
343
|
+
} else if (trimmedChoices.length && !trimmedChoices.includes(assessment.answer.trim())) {
|
|
344
|
+
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
194
345
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (isRecord(themeRaw.theme)) {
|
|
202
|
-
theme.theme = themeRaw.theme;
|
|
346
|
+
};
|
|
347
|
+
var ASSESSMENT_VALIDATORS = {
|
|
348
|
+
mcq: validateMcqLike,
|
|
349
|
+
trueFalse: (assessment, path, issues) => {
|
|
350
|
+
if (assessment.kind === "trueFalse" && typeof assessment.answer !== "boolean") {
|
|
351
|
+
issues.push({ path: `${path}.answer`, message: "answer must be a boolean for trueFalse" });
|
|
203
352
|
}
|
|
353
|
+
},
|
|
354
|
+
fillInBlanks: (assessment, path, issues) => {
|
|
355
|
+
if (assessment.kind === "fillInBlanks" && !assessment.template?.trim()) {
|
|
356
|
+
issues.push({ path: `${path}.template`, message: "template is required for fillInBlanks" });
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
findHotspot: () => {
|
|
360
|
+
},
|
|
361
|
+
findMultipleHotspots: () => {
|
|
204
362
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
lessons: input.lessons.map((lesson) => {
|
|
228
|
-
const idResult = (0, import_core.validateId)(lesson.id, "lessonId");
|
|
229
|
-
if (!idResult.ok) throw new Error("normalizeDescriptor called with invalid lesson id");
|
|
230
|
-
return {
|
|
231
|
-
...lesson,
|
|
232
|
-
id: idResult.id,
|
|
233
|
-
title: lesson.title.trim(),
|
|
234
|
-
spaPath: lesson.spaPath?.trim() || void 0
|
|
235
|
-
};
|
|
236
|
-
}),
|
|
237
|
-
assessments: input.assessments?.map((assessment) => {
|
|
238
|
-
const check = (0, import_core.validateId)(assessment.checkId, "checkId");
|
|
239
|
-
if (!check.ok) throw new Error("normalizeDescriptor called with invalid checkId");
|
|
240
|
-
return {
|
|
241
|
-
...assessment,
|
|
242
|
-
checkId: check.id,
|
|
243
|
-
question: assessment.question.trim(),
|
|
244
|
-
choices: assessment.choices.map((c) => c.trim()).filter((c) => c.length > 0),
|
|
245
|
-
answer: assessment.answer.trim()
|
|
246
|
-
};
|
|
247
|
-
})
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
function validateDescriptor(input) {
|
|
251
|
-
const parsed = parseCourseDescriptorInput(input);
|
|
252
|
-
if (parsed === null) {
|
|
253
|
-
return { ok: false, issues: [{ path: "course", message: "must be an object" }] };
|
|
254
|
-
}
|
|
255
|
-
return validateDescriptorParsed(parsed);
|
|
256
|
-
}
|
|
257
|
-
function validateDescriptorForTarget(input, target) {
|
|
258
|
-
const result = validateDescriptor(input);
|
|
259
|
-
if (!result.ok || !target) return result;
|
|
260
|
-
if (target !== "xapi" && target !== "cmi5") return result;
|
|
261
|
-
const activityIri = result.descriptor.tracking?.xapi?.activityIri?.trim();
|
|
262
|
-
if (!activityIri) {
|
|
263
|
-
return {
|
|
264
|
-
ok: false,
|
|
265
|
-
issues: [
|
|
266
|
-
{
|
|
267
|
-
path: "course.tracking.xapi.activityIri",
|
|
268
|
-
message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
|
|
269
|
-
}
|
|
270
|
-
]
|
|
271
|
-
};
|
|
363
|
+
};
|
|
364
|
+
function validateAssessmentEntry(assessment, index, issues, checkIds) {
|
|
365
|
+
const path = `assessments[${index}]`;
|
|
366
|
+
const check = (0, import_core2.validateId)(assessment.checkId, `${path}.checkId`);
|
|
367
|
+
if (!check.ok) {
|
|
368
|
+
issues.push(...check.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
369
|
+
} else if (checkIds.has(check.id)) {
|
|
370
|
+
issues.push({ path: `${path}.checkId`, message: "duplicate checkId" });
|
|
371
|
+
} else {
|
|
372
|
+
checkIds.add(check.id);
|
|
373
|
+
}
|
|
374
|
+
if (!assessment.question?.trim()) {
|
|
375
|
+
issues.push({ path: `${path}.question`, message: "question is required" });
|
|
376
|
+
}
|
|
377
|
+
const kind = assessment.kind ?? "mcq";
|
|
378
|
+
ASSESSMENT_VALIDATORS[kind](assessment, path, issues);
|
|
379
|
+
const passingScore = assessment.passingScore;
|
|
380
|
+
if (passingScore !== void 0 && !(Number.isFinite(passingScore) && passingScore > 0)) {
|
|
381
|
+
issues.push({
|
|
382
|
+
path: `${path}.passingScore`,
|
|
383
|
+
message: "passingScore must be greater than 0 (absolute point threshold)"
|
|
384
|
+
});
|
|
272
385
|
}
|
|
273
|
-
return result;
|
|
274
386
|
}
|
|
275
|
-
|
|
387
|
+
|
|
388
|
+
// src/descriptor/validateCourse.ts
|
|
389
|
+
var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
|
|
390
|
+
var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
|
|
391
|
+
function validateCourseDescriptor(input) {
|
|
276
392
|
const issues = [];
|
|
277
|
-
const course = (0,
|
|
393
|
+
const course = (0, import_core3.validateId)(input.courseId, "courseId");
|
|
278
394
|
if (!course.ok) issues.push(...course.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
279
395
|
if (!input.title?.trim()) {
|
|
280
396
|
issues.push({ path: "title", message: "title is required" });
|
|
@@ -327,7 +443,7 @@ function validateDescriptorParsed(input) {
|
|
|
327
443
|
const spaPaths = /* @__PURE__ */ new Set();
|
|
328
444
|
for (const [index, lesson] of (input.lessons ?? []).entries()) {
|
|
329
445
|
const path = `lessons[${index}]`;
|
|
330
|
-
const lessonResult = (0,
|
|
446
|
+
const lessonResult = (0, import_core3.validateId)(lesson.id, `${path}.id`);
|
|
331
447
|
if (!lessonResult.ok) {
|
|
332
448
|
issues.push(...lessonResult.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
333
449
|
} else if (lessonIds.has(lessonResult.id)) {
|
|
@@ -359,7 +475,7 @@ function validateDescriptorParsed(input) {
|
|
|
359
475
|
}
|
|
360
476
|
if (layout === "single-spa" && input.spaLessonId?.trim()) {
|
|
361
477
|
const spaId = input.spaLessonId.trim();
|
|
362
|
-
const spaResult = (0,
|
|
478
|
+
const spaResult = (0, import_core3.validateId)(spaId, "spaLessonId");
|
|
363
479
|
if (!spaResult.ok) {
|
|
364
480
|
issues.push(...spaResult.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
365
481
|
} else if (!lessonIds.has(spaResult.id)) {
|
|
@@ -371,41 +487,48 @@ function validateDescriptorParsed(input) {
|
|
|
371
487
|
}
|
|
372
488
|
const checkIds = /* @__PURE__ */ new Set();
|
|
373
489
|
for (const [index, assessment] of (input.assessments ?? []).entries()) {
|
|
374
|
-
|
|
375
|
-
const check = (0, import_core.validateId)(assessment.checkId, `${path}.checkId`);
|
|
376
|
-
if (!check.ok) {
|
|
377
|
-
issues.push(...check.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
378
|
-
} else if (checkIds.has(check.id)) {
|
|
379
|
-
issues.push({ path: `${path}.checkId`, message: "duplicate checkId" });
|
|
380
|
-
} else {
|
|
381
|
-
checkIds.add(check.id);
|
|
382
|
-
}
|
|
383
|
-
if (!assessment.question?.trim()) {
|
|
384
|
-
issues.push({ path: `${path}.question`, message: "question is required" });
|
|
385
|
-
}
|
|
386
|
-
const trimmedChoices = (assessment.choices ?? []).map((c) => c.trim()).filter((c) => c.length > 0);
|
|
387
|
-
if (!trimmedChoices.length) {
|
|
388
|
-
issues.push({
|
|
389
|
-
path: `${path}.choices`,
|
|
390
|
-
message: "at least one non-empty choice is required"
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
if (!assessment.answer?.trim()) {
|
|
394
|
-
issues.push({ path: `${path}.answer`, message: "answer is required" });
|
|
395
|
-
} else if (trimmedChoices.length && !trimmedChoices.includes(assessment.answer.trim())) {
|
|
396
|
-
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
397
|
-
}
|
|
398
|
-
const passingScore = assessment.passingScore;
|
|
399
|
-
if (passingScore !== void 0 && !(Number.isFinite(passingScore) && passingScore > 0)) {
|
|
400
|
-
issues.push({
|
|
401
|
-
path: `${path}.passingScore`,
|
|
402
|
-
message: "passingScore must be greater than 0 (absolute point threshold)"
|
|
403
|
-
});
|
|
404
|
-
}
|
|
490
|
+
validateAssessmentEntry(assessment, index, issues, checkIds);
|
|
405
491
|
}
|
|
492
|
+
return issues;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/descriptor/validateForTarget.ts
|
|
496
|
+
function validateDescriptorForExportTarget(descriptor, target) {
|
|
497
|
+
if (target !== "xapi" && target !== "cmi5") return [];
|
|
498
|
+
const activityIri = descriptor.tracking?.xapi?.activityIri?.trim();
|
|
499
|
+
if (!activityIri) {
|
|
500
|
+
return [
|
|
501
|
+
{
|
|
502
|
+
path: "course.tracking.xapi.activityIri",
|
|
503
|
+
message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
|
|
504
|
+
}
|
|
505
|
+
];
|
|
506
|
+
}
|
|
507
|
+
return [];
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/validateDescriptor.ts
|
|
511
|
+
function validateDescriptorParsed(input) {
|
|
512
|
+
const issues = validateCourseDescriptor(input);
|
|
406
513
|
if (issues.length) return { ok: false, issues };
|
|
407
514
|
return { ok: true, descriptor: normalizeDescriptor(input) };
|
|
408
515
|
}
|
|
516
|
+
function validateDescriptor(input) {
|
|
517
|
+
const parsed = parseCourseDescriptorInput(input);
|
|
518
|
+
if (parsed === null) {
|
|
519
|
+
return { ok: false, issues: [{ path: "course", message: "must be an object" }] };
|
|
520
|
+
}
|
|
521
|
+
return validateDescriptorParsed(parsed);
|
|
522
|
+
}
|
|
523
|
+
function validateDescriptorForTarget(input, target) {
|
|
524
|
+
const result = validateDescriptor(input);
|
|
525
|
+
if (!result.ok || !target) return result;
|
|
526
|
+
const targetIssues = validateDescriptorForExportTarget(result.descriptor, target);
|
|
527
|
+
if (targetIssues.length) {
|
|
528
|
+
return { ok: false, issues: targetIssues };
|
|
529
|
+
}
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
409
532
|
|
|
410
533
|
// src/validateProjectPaths.ts
|
|
411
534
|
var import_node_path2 = require("path");
|
|
@@ -460,12 +583,12 @@ function resolveSafePackageOutputOverride(projectRoot, override) {
|
|
|
460
583
|
}
|
|
461
584
|
|
|
462
585
|
// src/mapIds.ts
|
|
463
|
-
var
|
|
586
|
+
var import_core4 = require("@lessonkit/core");
|
|
464
587
|
function mapLessonkitIds(descriptor) {
|
|
465
|
-
const courseId = (0,
|
|
466
|
-
const lessonIds = descriptor.lessons.map((l) => (0,
|
|
588
|
+
const courseId = (0, import_core4.assertValidId)(descriptor.courseId, "courseId");
|
|
589
|
+
const lessonIds = descriptor.lessons.map((l) => (0, import_core4.assertValidId)(l.id, "lessonId"));
|
|
467
590
|
const checkIds = (descriptor.assessments ?? []).map(
|
|
468
|
-
(a) => (0,
|
|
591
|
+
(a) => (0, import_core4.assertValidId)(a.checkId, "checkId")
|
|
469
592
|
);
|
|
470
593
|
return { courseId, lessonIds, checkIds };
|
|
471
594
|
}
|
|
@@ -476,7 +599,7 @@ function slugChoiceId(text, index) {
|
|
|
476
599
|
const stem = base.length ? base : "choice";
|
|
477
600
|
return `${stem}-${index + 1}`;
|
|
478
601
|
}
|
|
479
|
-
function
|
|
602
|
+
function mcqToLxpack(assessment) {
|
|
480
603
|
const choices = assessment.choices.map((text, index) => {
|
|
481
604
|
const id = slugChoiceId(text, index);
|
|
482
605
|
return {
|
|
@@ -497,8 +620,43 @@ function assessmentDescriptorToLxpack(assessment) {
|
|
|
497
620
|
]
|
|
498
621
|
};
|
|
499
622
|
}
|
|
623
|
+
function assessmentDescriptorToLxpack(assessment) {
|
|
624
|
+
const kind = assessment.kind ?? "mcq";
|
|
625
|
+
if (kind === "trueFalse" && assessment.kind === "trueFalse") {
|
|
626
|
+
const choices = ["True", "False"];
|
|
627
|
+
const answerText = assessment.answer ? "True" : "False";
|
|
628
|
+
return mcqToLxpack({
|
|
629
|
+
kind: "mcq",
|
|
630
|
+
checkId: assessment.checkId,
|
|
631
|
+
question: assessment.question,
|
|
632
|
+
choices,
|
|
633
|
+
answer: answerText,
|
|
634
|
+
passingScore: assessment.passingScore
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
if (kind === "fillInBlanks") {
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
if (kind === "findHotspot" && assessment.kind === "findHotspot") {
|
|
641
|
+
return mcqToLxpack({
|
|
642
|
+
kind: "mcq",
|
|
643
|
+
checkId: assessment.checkId,
|
|
644
|
+
question: assessment.question,
|
|
645
|
+
choices: [assessment.correctTargetId, "other"],
|
|
646
|
+
answer: assessment.correctTargetId,
|
|
647
|
+
passingScore: assessment.passingScore
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
if (kind === "findMultipleHotspots") {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
if ("choices" in assessment && "answer" in assessment && typeof assessment.answer === "string") {
|
|
654
|
+
return mcqToLxpack(assessment);
|
|
655
|
+
}
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
500
658
|
function extractAssessments(descriptor) {
|
|
501
|
-
return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack);
|
|
659
|
+
return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack).filter((a) => a !== null);
|
|
502
660
|
}
|
|
503
661
|
|
|
504
662
|
// src/interchange.ts
|
|
@@ -571,7 +729,8 @@ async function resolveSpaDirs(options) {
|
|
|
571
729
|
const { descriptor, spaDistDir, lessonSpaDirs, projectRoot } = options;
|
|
572
730
|
const spaLessons = resolveSpaLessons(descriptor);
|
|
573
731
|
if (descriptor.layout === "single-spa") {
|
|
574
|
-
const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ??
|
|
732
|
+
const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? /* v8 ignore next */
|
|
733
|
+
"dist";
|
|
575
734
|
const srcDist = projectRoot ? (0, import_node_path3.resolve)(projectRoot, spaDistRelative) : (0, import_node_path3.resolve)(spaDistRelative);
|
|
576
735
|
if (projectRoot) {
|
|
577
736
|
assertRealPathUnderRoot((0, import_node_path3.resolve)(projectRoot), srcDist);
|
|
@@ -586,7 +745,8 @@ async function resolveSpaDirs(options) {
|
|
|
586
745
|
} catch {
|
|
587
746
|
throw new Error(`spaDistDir must contain index.html: ${(0, import_node_path3.join)(srcDist, "index.html")}`);
|
|
588
747
|
}
|
|
589
|
-
const lessonId = spaLessons[0]?.id ??
|
|
748
|
+
const lessonId = spaLessons[0]?.id ?? /* v8 ignore next */
|
|
749
|
+
"main";
|
|
590
750
|
return { [lessonId]: srcDist };
|
|
591
751
|
}
|
|
592
752
|
const dirs = {};
|
|
@@ -670,7 +830,15 @@ function validatePackageInputs(options) {
|
|
|
670
830
|
ok: false,
|
|
671
831
|
courseDir: outDir,
|
|
672
832
|
target,
|
|
673
|
-
issues: [
|
|
833
|
+
issues: [
|
|
834
|
+
{
|
|
835
|
+
path: "outDir",
|
|
836
|
+
message: (
|
|
837
|
+
/* v8 ignore next */
|
|
838
|
+
err instanceof Error ? err.message : String(err)
|
|
839
|
+
)
|
|
840
|
+
}
|
|
841
|
+
]
|
|
674
842
|
};
|
|
675
843
|
}
|
|
676
844
|
}
|
|
@@ -702,7 +870,10 @@ function validatePackageInputs(options) {
|
|
|
702
870
|
issues: [
|
|
703
871
|
{
|
|
704
872
|
path: "outputBaseDir",
|
|
705
|
-
message:
|
|
873
|
+
message: (
|
|
874
|
+
/* v8 ignore next */
|
|
875
|
+
err instanceof Error ? err.message : String(err)
|
|
876
|
+
)
|
|
706
877
|
}
|
|
707
878
|
]
|
|
708
879
|
};
|
|
@@ -717,7 +888,15 @@ function validatePackageInputs(options) {
|
|
|
717
888
|
ok: false,
|
|
718
889
|
courseDir: outDir,
|
|
719
890
|
target,
|
|
720
|
-
issues: [
|
|
891
|
+
issues: [
|
|
892
|
+
{
|
|
893
|
+
path: "output",
|
|
894
|
+
message: (
|
|
895
|
+
/* v8 ignore next */
|
|
896
|
+
err instanceof Error ? err.message : String(err)
|
|
897
|
+
)
|
|
898
|
+
}
|
|
899
|
+
]
|
|
721
900
|
};
|
|
722
901
|
}
|
|
723
902
|
}
|
|
@@ -806,7 +985,10 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
806
985
|
try {
|
|
807
986
|
await renameOrCopy(tmpPromote, failedPromote2);
|
|
808
987
|
} catch {
|
|
809
|
-
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
988
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
989
|
+
/* v8 ignore next */
|
|
990
|
+
() => void 0
|
|
991
|
+
);
|
|
810
992
|
}
|
|
811
993
|
const promoteMsg = promoteError instanceof Error ? promoteError.message : String(promoteError);
|
|
812
994
|
const restoreMsg = restoreError instanceof Error ? restoreError.message : String(restoreError);
|
|
@@ -822,7 +1004,10 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
822
1004
|
`[lessonkit/lxpack] failed to restore ${stagingDir} after promote error:`,
|
|
823
1005
|
restoreError instanceof Error ? restoreError.message : restoreError
|
|
824
1006
|
);
|
|
825
|
-
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
1007
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
1008
|
+
/* v8 ignore next */
|
|
1009
|
+
() => void 0
|
|
1010
|
+
);
|
|
826
1011
|
}
|
|
827
1012
|
throw promoteError;
|
|
828
1013
|
}
|
|
@@ -830,12 +1015,18 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
830
1015
|
try {
|
|
831
1016
|
await renameOrCopy(tmpPromote, failedPromote);
|
|
832
1017
|
} catch {
|
|
833
|
-
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
1018
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
1019
|
+
/* v8 ignore next */
|
|
1020
|
+
() => void 0
|
|
1021
|
+
);
|
|
834
1022
|
}
|
|
835
1023
|
throw promoteError;
|
|
836
1024
|
}
|
|
837
1025
|
if (backup) {
|
|
838
|
-
await fsp.rm(backup, { recursive: true, force: true }).catch(
|
|
1026
|
+
await fsp.rm(backup, { recursive: true, force: true }).catch(
|
|
1027
|
+
/* v8 ignore next */
|
|
1028
|
+
() => void 0
|
|
1029
|
+
);
|
|
839
1030
|
}
|
|
840
1031
|
}
|
|
841
1032
|
|
|
@@ -898,7 +1089,10 @@ async function buildStagingPackage(options) {
|
|
|
898
1089
|
outputDir: "outputDir" in build ? build.outputDir : void 0
|
|
899
1090
|
};
|
|
900
1091
|
} catch (err) {
|
|
901
|
-
await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1092
|
+
await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1093
|
+
/* v8 ignore next */
|
|
1094
|
+
() => void 0
|
|
1095
|
+
);
|
|
902
1096
|
throw err;
|
|
903
1097
|
}
|
|
904
1098
|
}
|
|
@@ -964,7 +1158,10 @@ async function packageLessonkitCourse(options) {
|
|
|
964
1158
|
outputBaseDir
|
|
965
1159
|
});
|
|
966
1160
|
if (!staged.ok) {
|
|
967
|
-
await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(
|
|
1161
|
+
await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(
|
|
1162
|
+
/* v8 ignore next */
|
|
1163
|
+
() => void 0
|
|
1164
|
+
);
|
|
968
1165
|
const validation2 = staged.build ? { ok: false, issues: staged.build.issues } : void 0;
|
|
969
1166
|
return {
|
|
970
1167
|
ok: false,
|
|
@@ -982,7 +1179,10 @@ async function packageLessonkitCourse(options) {
|
|
|
982
1179
|
validateArtifactInStaging(stagingRoot, staged.outputDir, "outputDir")
|
|
983
1180
|
].filter((issue) => issue != null);
|
|
984
1181
|
if (artifactIssues.length > 0) {
|
|
985
|
-
await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1182
|
+
await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1183
|
+
/* v8 ignore next */
|
|
1184
|
+
() => void 0
|
|
1185
|
+
);
|
|
986
1186
|
return {
|
|
987
1187
|
ok: false,
|
|
988
1188
|
courseDir: outDir,
|
|
@@ -1084,7 +1284,10 @@ function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
|
|
|
1084
1284
|
if (!validation.ok) {
|
|
1085
1285
|
for (const i of validation.issues) {
|
|
1086
1286
|
issues.push({
|
|
1087
|
-
path:
|
|
1287
|
+
path: (
|
|
1288
|
+
/* v8 ignore next */
|
|
1289
|
+
i.path.startsWith("course.") ? i.path : `course.${i.path}`
|
|
1290
|
+
),
|
|
1088
1291
|
message: i.message
|
|
1089
1292
|
});
|
|
1090
1293
|
}
|