@lessonkit/lxpack 1.2.0 → 1.3.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/index.cjs +421 -164
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +443 -186
- package/lessonkit-manifest.v1.json +99 -7
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -134,6 +134,14 @@ function parseAssessmentDescriptor(raw) {
|
|
|
134
134
|
correctTargetIds: Array.isArray(raw.correctTargetIds) ? raw.correctTargetIds.filter((id) => typeof id === "string") : []
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
|
+
if (typeof kind === "string" && kind !== "mcq" && kind !== "trueFalse" && kind !== "fillInBlanks" && kind !== "findHotspot" && kind !== "findMultipleHotspots") {
|
|
138
|
+
return {
|
|
139
|
+
kind,
|
|
140
|
+
...base,
|
|
141
|
+
choices: [],
|
|
142
|
+
answer: ""
|
|
143
|
+
};
|
|
144
|
+
}
|
|
137
145
|
return {
|
|
138
146
|
kind: kind === "mcq" ? "mcq" : void 0,
|
|
139
147
|
...base,
|
|
@@ -183,10 +191,11 @@ function parseCourseDescriptorInput(input) {
|
|
|
183
191
|
|
|
184
192
|
// src/descriptor/validateCourse.ts
|
|
185
193
|
import { validateId as validateId3 } from "@lessonkit/core";
|
|
194
|
+
import { validateTheme } from "@lessonkit/themes";
|
|
186
195
|
|
|
187
196
|
// src/spaPath.ts
|
|
188
|
-
import { realpathSync } from "fs";
|
|
189
|
-
import { isAbsolute, relative, resolve, sep, win32 } from "path";
|
|
197
|
+
import { existsSync, realpathSync } from "fs";
|
|
198
|
+
import { isAbsolute, join, relative, resolve, sep, win32 } from "path";
|
|
190
199
|
function resolveComparablePath(p) {
|
|
191
200
|
if (/^[a-zA-Z]:[/\\]/.test(p)) {
|
|
192
201
|
return win32.resolve(p);
|
|
@@ -212,6 +221,28 @@ function assertResolvedPathUnderRoot(root, target) {
|
|
|
212
221
|
throw new Error(`unsafe path escapes project root: ${target}`);
|
|
213
222
|
}
|
|
214
223
|
}
|
|
224
|
+
function resolveExistingPathUnderRoot(rootReal, rootResolved, targetResolved) {
|
|
225
|
+
const rel = relative(rootResolved, targetResolved);
|
|
226
|
+
if (rel.startsWith("..") || rel.includes(`..${sep}`)) {
|
|
227
|
+
throw new Error(`unsafe path escapes project root: ${targetResolved}`);
|
|
228
|
+
}
|
|
229
|
+
const segments = rel.split(/[/\\]/).filter((s) => s.length > 0 && s !== ".");
|
|
230
|
+
let current = rootReal;
|
|
231
|
+
for (const segment of segments) {
|
|
232
|
+
const next = join(current, segment);
|
|
233
|
+
if (existsSync(next)) {
|
|
234
|
+
try {
|
|
235
|
+
current = realpathSync(next);
|
|
236
|
+
} catch {
|
|
237
|
+
current = next;
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
current = next;
|
|
241
|
+
}
|
|
242
|
+
assertResolvedPathUnderRoot(rootReal, current);
|
|
243
|
+
}
|
|
244
|
+
return current;
|
|
245
|
+
}
|
|
215
246
|
function assertRealPathUnderRoot(root, target) {
|
|
216
247
|
const rootResolved = resolveComparablePath(root);
|
|
217
248
|
const targetResolved = resolveComparablePath(target);
|
|
@@ -221,17 +252,12 @@ function assertRealPathUnderRoot(root, target) {
|
|
|
221
252
|
} catch {
|
|
222
253
|
rootReal = rootResolved;
|
|
223
254
|
}
|
|
224
|
-
let targetCheck;
|
|
225
255
|
try {
|
|
226
|
-
targetCheck = realpathSync(targetResolved);
|
|
256
|
+
const targetCheck = realpathSync(targetResolved);
|
|
257
|
+
assertResolvedPathUnderRoot(rootReal, targetCheck);
|
|
227
258
|
} catch {
|
|
228
|
-
|
|
229
|
-
if (rel.startsWith("..") || rel.includes(`..${sep}`)) {
|
|
230
|
-
throw new Error(`unsafe path escapes project root: ${target}`);
|
|
231
|
-
}
|
|
232
|
-
targetCheck = resolve(rootReal, rel);
|
|
259
|
+
resolveExistingPathUnderRoot(rootReal, rootResolved, targetResolved);
|
|
233
260
|
}
|
|
234
|
-
assertResolvedPathUnderRoot(rootReal, targetCheck);
|
|
235
261
|
}
|
|
236
262
|
function normalizePathForComparison(p) {
|
|
237
263
|
const resolved = resolveComparablePath(p);
|
|
@@ -272,7 +298,12 @@ function themeToLxpackRuntime(input) {
|
|
|
272
298
|
// src/descriptor/validateAssessments.ts
|
|
273
299
|
import { validateId as validateId2 } from "@lessonkit/core";
|
|
274
300
|
var validateMcqLike = (assessment, path, issues) => {
|
|
275
|
-
if (!("choices" in assessment) || !(
|
|
301
|
+
if (!("choices" in assessment) || !Array.isArray(assessment.choices)) {
|
|
302
|
+
issues.push({ path: `${path}.choices`, message: "choices is required for mcq" });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (!("answer" in assessment) || typeof assessment.answer !== "string") {
|
|
306
|
+
issues.push({ path: `${path}.answer`, message: "answer is required for mcq" });
|
|
276
307
|
return;
|
|
277
308
|
}
|
|
278
309
|
const trimmedChoices = assessment.choices.map((c) => c.trim()).filter((c) => c.length > 0);
|
|
@@ -285,6 +316,22 @@ var validateMcqLike = (assessment, path, issues) => {
|
|
|
285
316
|
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
286
317
|
}
|
|
287
318
|
};
|
|
319
|
+
function countStarDelimitedBlanks(template) {
|
|
320
|
+
const matches = template.match(/\*[^*]+\*/g);
|
|
321
|
+
return matches?.length ?? 0;
|
|
322
|
+
}
|
|
323
|
+
function maxAchievableAssessmentScore(assessment) {
|
|
324
|
+
const kind = assessment.kind ?? "mcq";
|
|
325
|
+
if (kind === "fillInBlanks" && assessment.kind === "fillInBlanks") {
|
|
326
|
+
const explicit = assessment.blanks?.filter((b) => b?.id?.trim() && b?.answer?.trim()).length ?? 0;
|
|
327
|
+
if (explicit > 0) return explicit;
|
|
328
|
+
return countStarDelimitedBlanks(assessment.template ?? "");
|
|
329
|
+
}
|
|
330
|
+
if (kind === "findMultipleHotspots" && assessment.kind === "findMultipleHotspots") {
|
|
331
|
+
return assessment.correctTargetIds?.map((id) => id.trim()).filter((id) => id.length > 0).length ?? 0;
|
|
332
|
+
}
|
|
333
|
+
return 1;
|
|
334
|
+
}
|
|
288
335
|
var ASSESSMENT_VALIDATORS = {
|
|
289
336
|
mcq: validateMcqLike,
|
|
290
337
|
trueFalse: (assessment, path, issues) => {
|
|
@@ -297,9 +344,33 @@ var ASSESSMENT_VALIDATORS = {
|
|
|
297
344
|
issues.push({ path: `${path}.template`, message: "template is required for fillInBlanks" });
|
|
298
345
|
}
|
|
299
346
|
},
|
|
300
|
-
findHotspot: () => {
|
|
347
|
+
findHotspot: (assessment, path, issues) => {
|
|
348
|
+
if (assessment.kind !== "findHotspot") return;
|
|
349
|
+
if (!assessment.src?.trim()) {
|
|
350
|
+
issues.push({ path: `${path}.src`, message: "src is required for findHotspot" });
|
|
351
|
+
}
|
|
352
|
+
if (!assessment.alt?.trim()) {
|
|
353
|
+
issues.push({ path: `${path}.alt`, message: "alt is required for findHotspot" });
|
|
354
|
+
}
|
|
355
|
+
if (!assessment.correctTargetId?.trim()) {
|
|
356
|
+
issues.push({ path: `${path}.correctTargetId`, message: "correctTargetId is required for findHotspot" });
|
|
357
|
+
}
|
|
301
358
|
},
|
|
302
|
-
findMultipleHotspots: () => {
|
|
359
|
+
findMultipleHotspots: (assessment, path, issues) => {
|
|
360
|
+
if (assessment.kind !== "findMultipleHotspots") return;
|
|
361
|
+
if (!assessment.src?.trim()) {
|
|
362
|
+
issues.push({ path: `${path}.src`, message: "src is required for findMultipleHotspots" });
|
|
363
|
+
}
|
|
364
|
+
if (!assessment.alt?.trim()) {
|
|
365
|
+
issues.push({ path: `${path}.alt`, message: "alt is required for findMultipleHotspots" });
|
|
366
|
+
}
|
|
367
|
+
const ids = assessment.correctTargetIds?.map((id) => id.trim()).filter((id) => id.length > 0) ?? [];
|
|
368
|
+
if (!ids.length) {
|
|
369
|
+
issues.push({
|
|
370
|
+
path: `${path}.correctTargetIds`,
|
|
371
|
+
message: "at least one non-empty correctTargetId is required for findMultipleHotspots"
|
|
372
|
+
});
|
|
373
|
+
}
|
|
303
374
|
}
|
|
304
375
|
};
|
|
305
376
|
function validateAssessmentEntry(assessment, index, issues, checkIds) {
|
|
@@ -315,14 +386,38 @@ function validateAssessmentEntry(assessment, index, issues, checkIds) {
|
|
|
315
386
|
if (!assessment.question?.trim()) {
|
|
316
387
|
issues.push({ path: `${path}.question`, message: "question is required" });
|
|
317
388
|
}
|
|
389
|
+
const knownKinds = Object.keys(ASSESSMENT_VALIDATORS);
|
|
390
|
+
if (assessment.kind !== void 0 && assessment.kind !== "mcq" && !knownKinds.includes(assessment.kind)) {
|
|
391
|
+
issues.push({
|
|
392
|
+
path: `${path}.kind`,
|
|
393
|
+
message: `unknown kind; use one of: ${knownKinds.join(", ")}`
|
|
394
|
+
});
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
318
397
|
const kind = assessment.kind ?? "mcq";
|
|
319
|
-
ASSESSMENT_VALIDATORS[kind]
|
|
398
|
+
const validator = ASSESSMENT_VALIDATORS[kind];
|
|
399
|
+
if (!validator) {
|
|
400
|
+
issues.push({
|
|
401
|
+
path: `${path}.kind`,
|
|
402
|
+
message: `unknown kind; use one of: ${knownKinds.join(", ")}`
|
|
403
|
+
});
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
validator(assessment, path, issues);
|
|
320
407
|
const passingScore = assessment.passingScore;
|
|
321
408
|
if (passingScore !== void 0 && !(Number.isFinite(passingScore) && passingScore > 0)) {
|
|
322
409
|
issues.push({
|
|
323
410
|
path: `${path}.passingScore`,
|
|
324
411
|
message: "passingScore must be greater than 0 (absolute point threshold)"
|
|
325
412
|
});
|
|
413
|
+
} else if (passingScore !== void 0) {
|
|
414
|
+
const maxAchievable = maxAchievableAssessmentScore(assessment);
|
|
415
|
+
if (maxAchievable > 0 && passingScore > maxAchievable) {
|
|
416
|
+
issues.push({
|
|
417
|
+
path: `${path}.passingScore`,
|
|
418
|
+
message: `passingScore cannot exceed achievable score (${maxAchievable}) for this assessment kind`
|
|
419
|
+
});
|
|
420
|
+
}
|
|
326
421
|
}
|
|
327
422
|
}
|
|
328
423
|
|
|
@@ -356,13 +451,23 @@ function validateCourseDescriptor(input) {
|
|
|
356
451
|
});
|
|
357
452
|
}
|
|
358
453
|
if (input.theme?.theme) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
454
|
+
const themeResult = validateTheme(input.theme.theme);
|
|
455
|
+
if (!themeResult.ok) {
|
|
456
|
+
for (const issue of themeResult.issues) {
|
|
457
|
+
issues.push({
|
|
458
|
+
path: issue.path ? `theme.theme.${issue.path}` : "theme.theme",
|
|
459
|
+
message: issue.message
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
try {
|
|
464
|
+
themeToLxpackRuntime({ preset: themePreset, theme: themeResult.theme });
|
|
465
|
+
} catch (err) {
|
|
466
|
+
issues.push({
|
|
467
|
+
path: "theme.theme",
|
|
468
|
+
message: err instanceof Error ? err.message : "invalid custom theme"
|
|
469
|
+
});
|
|
470
|
+
}
|
|
366
471
|
}
|
|
367
472
|
}
|
|
368
473
|
const completionThreshold = input.tracking?.completion?.threshold;
|
|
@@ -433,19 +538,102 @@ function validateCourseDescriptor(input) {
|
|
|
433
538
|
return issues;
|
|
434
539
|
}
|
|
435
540
|
|
|
541
|
+
// src/assessments.ts
|
|
542
|
+
function slugChoiceId(text, index) {
|
|
543
|
+
const base = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
|
|
544
|
+
const stem = base.length ? base : "choice";
|
|
545
|
+
return `${stem}-${index + 1}`;
|
|
546
|
+
}
|
|
547
|
+
function mcqToLxpack(assessment) {
|
|
548
|
+
const choices = assessment.choices.map((text, index) => {
|
|
549
|
+
const id = slugChoiceId(text, index);
|
|
550
|
+
return {
|
|
551
|
+
id,
|
|
552
|
+
text,
|
|
553
|
+
correct: text === assessment.answer
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
return {
|
|
557
|
+
id: assessment.checkId,
|
|
558
|
+
passingScore: assessment.passingScore ?? 1,
|
|
559
|
+
questions: [
|
|
560
|
+
{
|
|
561
|
+
id: "q1",
|
|
562
|
+
prompt: assessment.question,
|
|
563
|
+
choices
|
|
564
|
+
}
|
|
565
|
+
]
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function assessmentDescriptorToLxpack(assessment) {
|
|
569
|
+
const kind = assessment.kind ?? "mcq";
|
|
570
|
+
if (kind === "trueFalse" && assessment.kind === "trueFalse") {
|
|
571
|
+
const choices = ["True", "False"];
|
|
572
|
+
const answerText = assessment.answer ? "True" : "False";
|
|
573
|
+
return mcqToLxpack({
|
|
574
|
+
kind: "mcq",
|
|
575
|
+
checkId: assessment.checkId,
|
|
576
|
+
question: assessment.question,
|
|
577
|
+
choices,
|
|
578
|
+
answer: answerText,
|
|
579
|
+
passingScore: assessment.passingScore
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
if (kind === "fillInBlanks") {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
if (kind === "findHotspot" && assessment.kind === "findHotspot") {
|
|
586
|
+
return mcqToLxpack({
|
|
587
|
+
kind: "mcq",
|
|
588
|
+
checkId: assessment.checkId,
|
|
589
|
+
question: assessment.question,
|
|
590
|
+
choices: [assessment.correctTargetId, "other"],
|
|
591
|
+
answer: assessment.correctTargetId,
|
|
592
|
+
passingScore: assessment.passingScore
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
if (kind === "findMultipleHotspots") {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
if ("choices" in assessment && "answer" in assessment && typeof assessment.answer === "string") {
|
|
599
|
+
return mcqToLxpack(assessment);
|
|
600
|
+
}
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
function extractAssessments(descriptor) {
|
|
604
|
+
return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack).filter((a) => a !== null);
|
|
605
|
+
}
|
|
606
|
+
|
|
436
607
|
// src/descriptor/validateForTarget.ts
|
|
608
|
+
var LMS_SHELL_TARGETS = /* @__PURE__ */ new Set([
|
|
609
|
+
"scorm12",
|
|
610
|
+
"scorm2004",
|
|
611
|
+
"standalone",
|
|
612
|
+
"xapi",
|
|
613
|
+
"cmi5"
|
|
614
|
+
]);
|
|
437
615
|
function validateDescriptorForExportTarget(descriptor, target) {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
{
|
|
616
|
+
const issues = [];
|
|
617
|
+
if (target === "xapi" || target === "cmi5") {
|
|
618
|
+
const activityIri = descriptor.tracking?.xapi?.activityIri?.trim();
|
|
619
|
+
if (!activityIri) {
|
|
620
|
+
issues.push({
|
|
443
621
|
path: "course.tracking.xapi.activityIri",
|
|
444
622
|
message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (LMS_SHELL_TARGETS.has(target)) {
|
|
627
|
+
(descriptor.assessments ?? []).forEach((assessment, index) => {
|
|
628
|
+
if (assessmentDescriptorToLxpack(assessment) === null) {
|
|
629
|
+
issues.push({
|
|
630
|
+
path: `assessments[${index}]`,
|
|
631
|
+
message: `assessment kind "${assessment.kind ?? "mcq"}" (checkId "${assessment.checkId}") is not injected into LMS shell quizzes for target "${target}"`
|
|
632
|
+
});
|
|
445
633
|
}
|
|
446
|
-
|
|
634
|
+
});
|
|
447
635
|
}
|
|
448
|
-
return
|
|
636
|
+
return issues;
|
|
449
637
|
}
|
|
450
638
|
|
|
451
639
|
// src/validateDescriptor.ts
|
|
@@ -534,72 +722,6 @@ function mapLessonkitIds(descriptor) {
|
|
|
534
722
|
return { courseId, lessonIds, checkIds };
|
|
535
723
|
}
|
|
536
724
|
|
|
537
|
-
// src/assessments.ts
|
|
538
|
-
function slugChoiceId(text, index) {
|
|
539
|
-
const base = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
|
|
540
|
-
const stem = base.length ? base : "choice";
|
|
541
|
-
return `${stem}-${index + 1}`;
|
|
542
|
-
}
|
|
543
|
-
function mcqToLxpack(assessment) {
|
|
544
|
-
const choices = assessment.choices.map((text, index) => {
|
|
545
|
-
const id = slugChoiceId(text, index);
|
|
546
|
-
return {
|
|
547
|
-
id,
|
|
548
|
-
text,
|
|
549
|
-
correct: text === assessment.answer
|
|
550
|
-
};
|
|
551
|
-
});
|
|
552
|
-
return {
|
|
553
|
-
id: assessment.checkId,
|
|
554
|
-
passingScore: assessment.passingScore ?? 1,
|
|
555
|
-
questions: [
|
|
556
|
-
{
|
|
557
|
-
id: "q1",
|
|
558
|
-
prompt: assessment.question,
|
|
559
|
-
choices
|
|
560
|
-
}
|
|
561
|
-
]
|
|
562
|
-
};
|
|
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
|
-
}
|
|
599
|
-
function extractAssessments(descriptor) {
|
|
600
|
-
return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack).filter((a) => a !== null);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
725
|
// src/interchange.ts
|
|
604
726
|
function mapDescriptorTracking(tracking) {
|
|
605
727
|
if (!tracking) return void 0;
|
|
@@ -660,12 +782,12 @@ function descriptorToInterchange(descriptor) {
|
|
|
660
782
|
}
|
|
661
783
|
|
|
662
784
|
// src/writeProject.ts
|
|
663
|
-
import { join as
|
|
785
|
+
import { join as join3, resolve as resolve4 } from "path";
|
|
664
786
|
import { materializeLessonkitProject } from "@lxpack/validators";
|
|
665
787
|
|
|
666
788
|
// src/spaDirs.ts
|
|
667
789
|
import { access } from "fs/promises";
|
|
668
|
-
import { join, resolve as resolve3 } from "path";
|
|
790
|
+
import { join as join2, resolve as resolve3 } from "path";
|
|
669
791
|
async function resolveSpaDirs(options) {
|
|
670
792
|
const { descriptor, spaDistDir, lessonSpaDirs, projectRoot } = options;
|
|
671
793
|
const spaLessons = resolveSpaLessons(descriptor);
|
|
@@ -682,9 +804,9 @@ async function resolveSpaDirs(options) {
|
|
|
682
804
|
throw new Error(`spaDistDir not found: ${srcDist}`);
|
|
683
805
|
}
|
|
684
806
|
try {
|
|
685
|
-
await access(
|
|
807
|
+
await access(join2(srcDist, "index.html"));
|
|
686
808
|
} catch {
|
|
687
|
-
throw new Error(`spaDistDir must contain index.html: ${
|
|
809
|
+
throw new Error(`spaDistDir must contain index.html: ${join2(srcDist, "index.html")}`);
|
|
688
810
|
}
|
|
689
811
|
const lessonId = spaLessons[0]?.id ?? /* v8 ignore next */
|
|
690
812
|
"main";
|
|
@@ -707,10 +829,10 @@ async function resolveSpaDirs(options) {
|
|
|
707
829
|
throw new Error(`lessonSpaDirs path not found for lesson "${lesson.id}": ${resolved}`);
|
|
708
830
|
}
|
|
709
831
|
try {
|
|
710
|
-
await access(
|
|
832
|
+
await access(join2(resolved, "index.html"));
|
|
711
833
|
} catch {
|
|
712
834
|
throw new Error(
|
|
713
|
-
`lessonSpaDirs must contain index.html for lesson "${lesson.id}": ${
|
|
835
|
+
`lessonSpaDirs must contain index.html for lesson "${lesson.id}": ${join2(resolved, "index.html")}`
|
|
714
836
|
);
|
|
715
837
|
}
|
|
716
838
|
dirs[lesson.id] = resolved;
|
|
@@ -747,13 +869,13 @@ async function writeLxpackProject(options) {
|
|
|
747
869
|
const courseDir = materialized.courseDir;
|
|
748
870
|
return {
|
|
749
871
|
outDir: courseDir,
|
|
750
|
-
courseYamlPath:
|
|
751
|
-
lessonkitJsonPath:
|
|
872
|
+
courseYamlPath: join3(courseDir, "course.yaml"),
|
|
873
|
+
lessonkitJsonPath: join3(courseDir, "lessonkit.json")
|
|
752
874
|
};
|
|
753
875
|
}
|
|
754
876
|
|
|
755
877
|
// src/packageCourse.ts
|
|
756
|
-
import { resolve as
|
|
878
|
+
import { resolve as resolve7 } from "path";
|
|
757
879
|
import * as fsp3 from "fs/promises";
|
|
758
880
|
import {
|
|
759
881
|
buildCourse,
|
|
@@ -761,48 +883,75 @@ import {
|
|
|
761
883
|
} from "@lxpack/api";
|
|
762
884
|
|
|
763
885
|
// src/packaging/validateInputs.ts
|
|
764
|
-
import { isAbsolute as isAbsolute3, join as
|
|
886
|
+
import { isAbsolute as isAbsolute3, join as join4, resolve as resolve5, win32 as win322 } from "path";
|
|
765
887
|
function validatePackageInputs(options) {
|
|
766
888
|
const { target, output, outputBaseDir } = options;
|
|
767
889
|
const outDir = resolve5(options.outDir);
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
courseDir: outDir,
|
|
776
|
-
target,
|
|
777
|
-
issues: [
|
|
778
|
-
{
|
|
779
|
-
path: "outDir",
|
|
780
|
-
message: (
|
|
781
|
-
/* v8 ignore next */
|
|
782
|
-
err instanceof Error ? err.message : String(err)
|
|
783
|
-
)
|
|
784
|
-
}
|
|
785
|
-
]
|
|
786
|
-
};
|
|
787
|
-
}
|
|
890
|
+
if (!options.projectRoot) {
|
|
891
|
+
return {
|
|
892
|
+
ok: false,
|
|
893
|
+
courseDir: outDir,
|
|
894
|
+
target,
|
|
895
|
+
issues: [{ path: "projectRoot", message: "projectRoot is required for packageLessonkitCourse" }]
|
|
896
|
+
};
|
|
788
897
|
}
|
|
789
|
-
|
|
898
|
+
const projectRoot = resolve5(options.projectRoot);
|
|
899
|
+
try {
|
|
900
|
+
assertRealPathUnderRoot(projectRoot, outDir);
|
|
901
|
+
} catch (err) {
|
|
790
902
|
return {
|
|
791
903
|
ok: false,
|
|
792
904
|
courseDir: outDir,
|
|
793
905
|
target,
|
|
794
|
-
issues: [
|
|
906
|
+
issues: [
|
|
907
|
+
{
|
|
908
|
+
path: "outDir",
|
|
909
|
+
message: (
|
|
910
|
+
/* v8 ignore next */
|
|
911
|
+
err instanceof Error ? err.message : String(err)
|
|
912
|
+
)
|
|
913
|
+
}
|
|
914
|
+
]
|
|
795
915
|
};
|
|
796
916
|
}
|
|
797
|
-
if (
|
|
917
|
+
if (outputBaseDir && !isSafeRelativeSpaPath(outputBaseDir)) {
|
|
798
918
|
return {
|
|
799
919
|
ok: false,
|
|
800
920
|
courseDir: outDir,
|
|
801
921
|
target,
|
|
802
|
-
issues: [{ path: "
|
|
922
|
+
issues: [{ path: "outputBaseDir", message: `unsafe outputBaseDir: ${outputBaseDir}` }]
|
|
803
923
|
};
|
|
804
924
|
}
|
|
805
|
-
if (
|
|
925
|
+
if (output && !isSafeRelativeSpaPath(output)) {
|
|
926
|
+
if (isAbsolute3(output)) {
|
|
927
|
+
try {
|
|
928
|
+
assertRealPathUnderRoot(projectRoot, resolve5(output));
|
|
929
|
+
} catch (err) {
|
|
930
|
+
return {
|
|
931
|
+
ok: false,
|
|
932
|
+
courseDir: outDir,
|
|
933
|
+
target,
|
|
934
|
+
issues: [
|
|
935
|
+
{
|
|
936
|
+
path: "output",
|
|
937
|
+
message: (
|
|
938
|
+
/* v8 ignore next */
|
|
939
|
+
err instanceof Error ? err.message : `unsafe output: ${output}`
|
|
940
|
+
)
|
|
941
|
+
}
|
|
942
|
+
]
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
} else {
|
|
946
|
+
return {
|
|
947
|
+
ok: false,
|
|
948
|
+
courseDir: outDir,
|
|
949
|
+
target,
|
|
950
|
+
issues: [{ path: "output", message: `unsafe output: ${output}` }]
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (outputBaseDir) {
|
|
806
955
|
const resolvedOutputBase = resolve5(projectRoot, outputBaseDir);
|
|
807
956
|
try {
|
|
808
957
|
assertRealPathUnderRoot(projectRoot, resolvedOutputBase);
|
|
@@ -823,8 +972,8 @@ function validatePackageInputs(options) {
|
|
|
823
972
|
};
|
|
824
973
|
}
|
|
825
974
|
}
|
|
826
|
-
if (
|
|
827
|
-
const resolvedOutput = resolve5(projectRoot, output);
|
|
975
|
+
if (output) {
|
|
976
|
+
const resolvedOutput = isAbsolute3(output) ? resolve5(output) : resolve5(projectRoot, output);
|
|
828
977
|
try {
|
|
829
978
|
assertRealPathUnderRoot(projectRoot, resolvedOutput);
|
|
830
979
|
} catch (err) {
|
|
@@ -861,23 +1010,23 @@ function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
|
|
|
861
1010
|
if (!artifactPath) return void 0;
|
|
862
1011
|
const resolved = resolveComparablePath(artifactPath);
|
|
863
1012
|
if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
|
|
864
|
-
|
|
1013
|
+
throw new Error(`${artifactPath} is outside the staging directory`);
|
|
865
1014
|
}
|
|
866
1015
|
const rel = relativePathUnderRoot(stagingRoot, resolved);
|
|
867
1016
|
if (rel.startsWith("..") || isAbsolute3(rel)) {
|
|
868
|
-
|
|
1017
|
+
throw new Error(`${artifactPath} is outside the staging directory`);
|
|
869
1018
|
}
|
|
870
1019
|
if (!rel) return outDir;
|
|
871
1020
|
if (/^[a-zA-Z]:[/\\]/.test(outDir)) {
|
|
872
1021
|
return win322.join(outDir, rel.replace(/\//g, win322.sep));
|
|
873
1022
|
}
|
|
874
|
-
return
|
|
1023
|
+
return join4(outDir, rel);
|
|
875
1024
|
}
|
|
876
1025
|
|
|
877
1026
|
// src/packaging/promote.ts
|
|
878
1027
|
import * as fsp from "fs/promises";
|
|
879
|
-
import { randomUUID } from "crypto";
|
|
880
|
-
import { dirname, join as
|
|
1028
|
+
import { createHash, randomUUID } from "crypto";
|
|
1029
|
+
import { dirname, join as join5, resolve as resolve6 } from "path";
|
|
881
1030
|
async function pathExists(path) {
|
|
882
1031
|
try {
|
|
883
1032
|
await fsp.access(path);
|
|
@@ -896,6 +1045,68 @@ async function renameOrCopy(from, to) {
|
|
|
896
1045
|
await fsp.rm(from, { recursive: true, force: true });
|
|
897
1046
|
}
|
|
898
1047
|
}
|
|
1048
|
+
function promoteLockPath(outDir) {
|
|
1049
|
+
const parent = dirname(outDir);
|
|
1050
|
+
const hash = createHash("sha256").update(resolve6(outDir)).digest("hex").slice(0, 16);
|
|
1051
|
+
return join5(parent, `.lk-promote-lock-${hash}`);
|
|
1052
|
+
}
|
|
1053
|
+
var STALE_LOCK_TTL_MS = 5 * 60 * 1e3;
|
|
1054
|
+
async function isStalePromoteLock(lockPath) {
|
|
1055
|
+
try {
|
|
1056
|
+
const stat2 = await fsp.stat(lockPath);
|
|
1057
|
+
if (Date.now() - stat2.mtimeMs > STALE_LOCK_TTL_MS) return true;
|
|
1058
|
+
const content = await fsp.readFile(lockPath, "utf8");
|
|
1059
|
+
const pid = Number.parseInt(content.trim(), 10);
|
|
1060
|
+
if (!Number.isFinite(pid) || pid <= 0) return true;
|
|
1061
|
+
try {
|
|
1062
|
+
process.kill(pid, 0);
|
|
1063
|
+
return false;
|
|
1064
|
+
} catch {
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
} catch {
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
async function withPromoteLock(outDir, fn) {
|
|
1072
|
+
const lockPath = promoteLockPath(outDir);
|
|
1073
|
+
await fsp.mkdir(dirname(outDir), { recursive: true });
|
|
1074
|
+
let lockHandle;
|
|
1075
|
+
for (let attempt = 0; attempt < 200; attempt++) {
|
|
1076
|
+
try {
|
|
1077
|
+
lockHandle = await fsp.open(lockPath, "wx");
|
|
1078
|
+
await lockHandle.writeFile(`${process.pid}
|
|
1079
|
+
`, "utf8");
|
|
1080
|
+
break;
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
|
|
1083
|
+
if (code !== "EEXIST") throw err;
|
|
1084
|
+
if (await isStalePromoteLock(lockPath)) {
|
|
1085
|
+
await fsp.rm(lockPath, { force: true }).catch(
|
|
1086
|
+
/* v8 ignore next */
|
|
1087
|
+
() => void 0
|
|
1088
|
+
);
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (!lockHandle) {
|
|
1095
|
+
throw new Error(`[lessonkit/lxpack] timed out acquiring promote lock for ${outDir}`);
|
|
1096
|
+
}
|
|
1097
|
+
try {
|
|
1098
|
+
return await fn();
|
|
1099
|
+
} finally {
|
|
1100
|
+
await lockHandle.close().catch(
|
|
1101
|
+
/* v8 ignore next */
|
|
1102
|
+
() => void 0
|
|
1103
|
+
);
|
|
1104
|
+
await fsp.rm(lockPath, { force: true }).catch(
|
|
1105
|
+
/* v8 ignore next */
|
|
1106
|
+
() => void 0
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
899
1110
|
async function assertNoLegacyPromoteArtifacts(outDir) {
|
|
900
1111
|
const legacyTmp = `${outDir}.tmp-promote`;
|
|
901
1112
|
const legacyBak = `${outDir}.bak`;
|
|
@@ -909,45 +1120,57 @@ async function assertNoLegacyPromoteArtifacts(outDir) {
|
|
|
909
1120
|
}
|
|
910
1121
|
}
|
|
911
1122
|
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
await renameOrCopy(outDir, backup);
|
|
920
|
-
}
|
|
921
|
-
try {
|
|
922
|
-
await renameOrCopy(tmpPromote, outDir);
|
|
923
|
-
} catch (promoteError) {
|
|
1123
|
+
return withPromoteLock(outDir, async () => {
|
|
1124
|
+
await assertNoLegacyPromoteArtifacts(outDir);
|
|
1125
|
+
const parent = dirname(outDir);
|
|
1126
|
+
const tmpPromote = await fsp.mkdtemp(join5(parent, ".lk-promote-"));
|
|
1127
|
+
await renameOrCopy(stagingDir, tmpPromote);
|
|
1128
|
+
const hadOutDir = await pathExists(outDir);
|
|
1129
|
+
const backup = hadOutDir ? await fsp.mkdtemp(join5(parent, ".lk-backup-")) : void 0;
|
|
924
1130
|
if (hadOutDir && backup) {
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1131
|
+
await renameOrCopy(outDir, backup);
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
await renameOrCopy(tmpPromote, outDir);
|
|
1135
|
+
} catch (promoteError) {
|
|
1136
|
+
if (hadOutDir && backup) {
|
|
929
1137
|
try {
|
|
930
|
-
await renameOrCopy(
|
|
931
|
-
} catch {
|
|
1138
|
+
await renameOrCopy(backup, outDir);
|
|
1139
|
+
} catch (restoreError) {
|
|
1140
|
+
const failedPromote2 = join5(parent, `.lk-failed-promote-${randomUUID()}`);
|
|
1141
|
+
try {
|
|
1142
|
+
await renameOrCopy(tmpPromote, failedPromote2);
|
|
1143
|
+
} catch {
|
|
1144
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
1145
|
+
/* v8 ignore next */
|
|
1146
|
+
() => void 0
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
const promoteMsg = promoteError instanceof Error ? promoteError.message : String(promoteError);
|
|
1150
|
+
const restoreMsg = restoreError instanceof Error ? restoreError.message : String(restoreError);
|
|
1151
|
+
throw new Error(
|
|
1152
|
+
`[lessonkit/lxpack] promote failed (${promoteMsg}) and could not restore ${outDir} (${restoreMsg}). Recovery: previous output may be in ${backup}; staged package may be in ${failedPromote2}.`
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
} else {
|
|
1156
|
+
try {
|
|
1157
|
+
await renameOrCopy(tmpPromote, stagingDir);
|
|
1158
|
+
} catch (restoreError) {
|
|
1159
|
+
console.warn(
|
|
1160
|
+
`[lessonkit/lxpack] failed to restore ${stagingDir} after promote error:`,
|
|
1161
|
+
restoreError instanceof Error ? restoreError.message : restoreError
|
|
1162
|
+
);
|
|
932
1163
|
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
933
1164
|
/* v8 ignore next */
|
|
934
1165
|
() => void 0
|
|
935
1166
|
);
|
|
936
1167
|
}
|
|
937
|
-
|
|
938
|
-
const restoreMsg = restoreError instanceof Error ? restoreError.message : String(restoreError);
|
|
939
|
-
throw new Error(
|
|
940
|
-
`[lessonkit/lxpack] promote failed (${promoteMsg}) and could not restore ${outDir} (${restoreMsg}). Recovery: previous output may be in ${backup}; staged package may be in ${failedPromote2}.`
|
|
941
|
-
);
|
|
1168
|
+
throw promoteError;
|
|
942
1169
|
}
|
|
943
|
-
|
|
1170
|
+
const failedPromote = join5(parent, `.lk-failed-promote-${randomUUID()}`);
|
|
944
1171
|
try {
|
|
945
|
-
await renameOrCopy(tmpPromote,
|
|
946
|
-
} catch
|
|
947
|
-
console.warn(
|
|
948
|
-
`[lessonkit/lxpack] failed to restore ${stagingDir} after promote error:`,
|
|
949
|
-
restoreError instanceof Error ? restoreError.message : restoreError
|
|
950
|
-
);
|
|
1172
|
+
await renameOrCopy(tmpPromote, failedPromote);
|
|
1173
|
+
} catch {
|
|
951
1174
|
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
952
1175
|
/* v8 ignore next */
|
|
953
1176
|
() => void 0
|
|
@@ -955,33 +1178,23 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
955
1178
|
}
|
|
956
1179
|
throw promoteError;
|
|
957
1180
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
await renameOrCopy(tmpPromote, failedPromote);
|
|
961
|
-
} catch {
|
|
962
|
-
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
|
|
1181
|
+
if (backup) {
|
|
1182
|
+
await fsp.rm(backup, { recursive: true, force: true }).catch(
|
|
963
1183
|
/* v8 ignore next */
|
|
964
1184
|
() => void 0
|
|
965
1185
|
);
|
|
966
1186
|
}
|
|
967
|
-
|
|
968
|
-
}
|
|
969
|
-
if (backup) {
|
|
970
|
-
await fsp.rm(backup, { recursive: true, force: true }).catch(
|
|
971
|
-
/* v8 ignore next */
|
|
972
|
-
() => void 0
|
|
973
|
-
);
|
|
974
|
-
}
|
|
1187
|
+
});
|
|
975
1188
|
}
|
|
976
1189
|
|
|
977
1190
|
// src/packaging/staging.ts
|
|
978
1191
|
import * as fsp2 from "fs/promises";
|
|
979
|
-
import { dirname as dirname2, join as
|
|
1192
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
980
1193
|
import { tmpdir } from "os";
|
|
981
1194
|
import { packageLessonkit } from "@lxpack/api";
|
|
982
1195
|
async function buildStagingPackage(options) {
|
|
983
1196
|
const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
|
|
984
|
-
const stagingDir = await fsp2.mkdtemp(
|
|
1197
|
+
const stagingDir = await fsp2.mkdtemp(join6(tmpdir(), "lessonkit-lxpack-"));
|
|
985
1198
|
try {
|
|
986
1199
|
let spaDirs;
|
|
987
1200
|
try {
|
|
@@ -1000,8 +1213,8 @@ async function buildStagingPackage(options) {
|
|
|
1000
1213
|
}
|
|
1001
1214
|
const interchange = descriptorToInterchange(descriptor);
|
|
1002
1215
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
1003
|
-
await fsp2.mkdir(
|
|
1004
|
-
const defaultOutput = output ?? (dir ?
|
|
1216
|
+
await fsp2.mkdir(join6(stagingDir, outputBase), { recursive: true });
|
|
1217
|
+
const defaultOutput = output ?? (dir ? join6(outputBase, target) : join6(outputBase, `course-${target}.zip`));
|
|
1005
1218
|
const build = await packageLessonkit({
|
|
1006
1219
|
interchange,
|
|
1007
1220
|
spaDirs,
|
|
@@ -1044,16 +1257,25 @@ async function ensureOutDirParent(outDir) {
|
|
|
1044
1257
|
await fsp2.mkdir(dirname2(outDir), { recursive: true });
|
|
1045
1258
|
}
|
|
1046
1259
|
|
|
1260
|
+
// src/packaging/issueSeverity.ts
|
|
1261
|
+
function isPackagingErrorIssue(issue) {
|
|
1262
|
+
const severity = issue.severity?.toLowerCase();
|
|
1263
|
+
return severity === "error" || severity === "fatal";
|
|
1264
|
+
}
|
|
1265
|
+
function findPackagingErrorIssues(issues) {
|
|
1266
|
+
return (issues ?? []).filter(isPackagingErrorIssue);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1047
1269
|
// src/packageCourse.ts
|
|
1048
1270
|
async function validateLessonkitProject(options) {
|
|
1049
1271
|
return validateCourse({
|
|
1050
|
-
courseDir:
|
|
1272
|
+
courseDir: resolve7(options.courseDir),
|
|
1051
1273
|
target: options.target
|
|
1052
1274
|
});
|
|
1053
1275
|
}
|
|
1054
1276
|
async function buildLessonkitProject(options) {
|
|
1055
1277
|
const buildOptions = {
|
|
1056
|
-
courseDir:
|
|
1278
|
+
courseDir: resolve7(options.courseDir),
|
|
1057
1279
|
target: options.target,
|
|
1058
1280
|
output: options.output,
|
|
1059
1281
|
dir: options.dir,
|
|
@@ -1084,7 +1306,7 @@ async function packageLessonkitCourse(options) {
|
|
|
1084
1306
|
if (!descriptorValidation.ok) {
|
|
1085
1307
|
return {
|
|
1086
1308
|
ok: false,
|
|
1087
|
-
courseDir:
|
|
1309
|
+
courseDir: resolve7(writeOpts.outDir),
|
|
1088
1310
|
target,
|
|
1089
1311
|
issues: descriptorValidation.issues.map((i) => ({
|
|
1090
1312
|
path: i.path,
|
|
@@ -1093,6 +1315,18 @@ async function packageLessonkitCourse(options) {
|
|
|
1093
1315
|
};
|
|
1094
1316
|
}
|
|
1095
1317
|
const descriptor = descriptorValidation.descriptor;
|
|
1318
|
+
const nonInjectableAssessments = (descriptor.assessments ?? []).map((assessment, index) => ({ assessment, index })).filter(({ assessment }) => assessmentDescriptorToLxpack(assessment) === null);
|
|
1319
|
+
if (nonInjectableAssessments.length > 0) {
|
|
1320
|
+
return {
|
|
1321
|
+
ok: false,
|
|
1322
|
+
courseDir: outDir,
|
|
1323
|
+
target,
|
|
1324
|
+
issues: nonInjectableAssessments.map(({ assessment, index }) => ({
|
|
1325
|
+
path: `assessments[${index}]`,
|
|
1326
|
+
message: `assessment kind "${assessment.kind ?? "mcq"}" (checkId "${assessment.checkId}") is not injected into LMS shell quizzes for target "${target}"`
|
|
1327
|
+
}))
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1096
1330
|
const staged = await buildStagingPackage({
|
|
1097
1331
|
...writeOpts,
|
|
1098
1332
|
descriptor,
|
|
@@ -1117,6 +1351,25 @@ async function packageLessonkitCourse(options) {
|
|
|
1117
1351
|
};
|
|
1118
1352
|
}
|
|
1119
1353
|
const { stagingDir, build } = staged;
|
|
1354
|
+
const buildErrorIssues = findPackagingErrorIssues(build.issues);
|
|
1355
|
+
if (buildErrorIssues.length > 0) {
|
|
1356
|
+
await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1357
|
+
/* v8 ignore next */
|
|
1358
|
+
() => void 0
|
|
1359
|
+
);
|
|
1360
|
+
return {
|
|
1361
|
+
ok: false,
|
|
1362
|
+
courseDir: outDir,
|
|
1363
|
+
target,
|
|
1364
|
+
validation: { ok: false, manifest: build.manifest, issues: build.issues },
|
|
1365
|
+
build,
|
|
1366
|
+
issues: build.issues.filter((i) => findPackagingErrorIssues([i]).length > 0).map((i) => ({
|
|
1367
|
+
path: i.path ?? "build",
|
|
1368
|
+
message: i.message,
|
|
1369
|
+
severity: i.severity
|
|
1370
|
+
}))
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1120
1373
|
const stagingRoot = await fsp3.realpath(stagingDir);
|
|
1121
1374
|
const artifactIssues = [
|
|
1122
1375
|
validateArtifactInStaging(stagingRoot, staged.outputPath, "outputPath"),
|
|
@@ -1147,6 +1400,10 @@ async function packageLessonkitCourse(options) {
|
|
|
1147
1400
|
await ensureOutDirParent(outDir);
|
|
1148
1401
|
await promoteStagingToOutDir(stagingDir, outDir);
|
|
1149
1402
|
} catch (err) {
|
|
1403
|
+
await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1404
|
+
/* v8 ignore next */
|
|
1405
|
+
() => void 0
|
|
1406
|
+
);
|
|
1150
1407
|
return {
|
|
1151
1408
|
ok: false,
|
|
1152
1409
|
courseDir: outDir,
|