@lessonkit/lxpack 1.6.0 → 1.7.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/README.md +1 -1
- package/dist/bridge.cjs +4 -0
- package/dist/bridge.js +4 -0
- package/dist/index.cjs +426 -129
- package/dist/index.d.cts +51 -2
- package/dist/index.d.ts +51 -2
- package/dist/index.js +417 -121
- package/lessonkit-manifest.v1.json +69 -2
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -63,6 +63,7 @@ __export(index_exports, {
|
|
|
63
63
|
validateLessonkitProject: () => validateLessonkitProject,
|
|
64
64
|
validateLkcourse: () => validateLkcourse,
|
|
65
65
|
validateLkcourseArchiveEntries: () => validateLkcourseArchiveEntries,
|
|
66
|
+
validateManifestName: () => validateManifestName,
|
|
66
67
|
validatePackageInputs: () => validatePackageInputs,
|
|
67
68
|
validateProjectPaths: () => validateProjectPaths,
|
|
68
69
|
validateReactManifestParity: () => validateReactManifestParity,
|
|
@@ -130,13 +131,40 @@ function normalizeDescriptor(input) {
|
|
|
130
131
|
correctTargetIds: assessment.correctTargetIds.map((id) => id.trim()).filter((id) => id.length > 0)
|
|
131
132
|
};
|
|
132
133
|
}
|
|
134
|
+
if (assessment.kind === "sortParagraphs") {
|
|
135
|
+
return {
|
|
136
|
+
...assessment,
|
|
137
|
+
checkId: check.id,
|
|
138
|
+
question,
|
|
139
|
+
paragraphs: assessment.paragraphs.map((p) => p.trim()).filter((p) => p.length > 0),
|
|
140
|
+
correctOrder: [...assessment.correctOrder]
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (assessment.kind === "guessTheAnswer") {
|
|
144
|
+
return {
|
|
145
|
+
...assessment,
|
|
146
|
+
checkId: check.id,
|
|
147
|
+
question,
|
|
148
|
+
answer: assessment.answer.trim()
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (assessment.kind === "multimediaChoice") {
|
|
152
|
+
return {
|
|
153
|
+
...assessment,
|
|
154
|
+
checkId: check.id,
|
|
155
|
+
question,
|
|
156
|
+
choices: assessment.choices.map((c) => c.trim()).filter((c) => c.length > 0),
|
|
157
|
+
answer: assessment.answer.trim()
|
|
158
|
+
};
|
|
159
|
+
}
|
|
133
160
|
const mcq = assessment;
|
|
134
161
|
return {
|
|
135
162
|
...mcq,
|
|
136
163
|
checkId: check.id,
|
|
137
164
|
question,
|
|
138
165
|
choices: mcq.choices.map((c) => c.trim()).filter((c) => c.length > 0),
|
|
139
|
-
answer: mcq.answer.trim()
|
|
166
|
+
answer: mcq.answer.trim(),
|
|
167
|
+
answers: mcq.answers?.map((a) => a.trim()).filter((a) => a.length > 0)
|
|
140
168
|
};
|
|
141
169
|
})
|
|
142
170
|
};
|
|
@@ -210,7 +238,30 @@ function parseAssessmentDescriptor(raw) {
|
|
|
210
238
|
correctTargetIds: Array.isArray(raw.correctTargetIds) ? raw.correctTargetIds.filter((id) => typeof id === "string") : []
|
|
211
239
|
};
|
|
212
240
|
}
|
|
213
|
-
if (
|
|
241
|
+
if (kind === "sortParagraphs") {
|
|
242
|
+
return {
|
|
243
|
+
kind: "sortParagraphs",
|
|
244
|
+
...base,
|
|
245
|
+
paragraphs: Array.isArray(raw.paragraphs) ? raw.paragraphs.filter((p) => typeof p === "string") : [],
|
|
246
|
+
correctOrder: Array.isArray(raw.correctOrder) ? raw.correctOrder.filter((n) => typeof n === "number" && Number.isFinite(n)) : []
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (kind === "guessTheAnswer") {
|
|
250
|
+
return {
|
|
251
|
+
kind: "guessTheAnswer",
|
|
252
|
+
...base,
|
|
253
|
+
answer: typeof raw.answer === "string" ? raw.answer : ""
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (kind === "multimediaChoice") {
|
|
257
|
+
return {
|
|
258
|
+
kind: "multimediaChoice",
|
|
259
|
+
...base,
|
|
260
|
+
choices: Array.isArray(raw.choices) ? raw.choices.filter((c) => typeof c === "string") : [],
|
|
261
|
+
answer: typeof raw.answer === "string" ? raw.answer : ""
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (typeof kind === "string" && kind !== "mcq" && kind !== "trueFalse" && kind !== "fillInBlanks" && kind !== "findHotspot" && kind !== "findMultipleHotspots" && kind !== "sortParagraphs" && kind !== "guessTheAnswer" && kind !== "multimediaChoice") {
|
|
214
265
|
return {
|
|
215
266
|
kind,
|
|
216
267
|
...base,
|
|
@@ -222,7 +273,15 @@ function parseAssessmentDescriptor(raw) {
|
|
|
222
273
|
kind: kind === "mcq" ? "mcq" : void 0,
|
|
223
274
|
...base,
|
|
224
275
|
choices: Array.isArray(raw.choices) ? raw.choices.filter((c) => typeof c === "string") : [],
|
|
225
|
-
answer: typeof raw.answer === "string" ? raw.answer : ""
|
|
276
|
+
answer: typeof raw.answer === "string" ? raw.answer : "",
|
|
277
|
+
answers: Array.isArray(raw.answers) ? raw.answers.filter((a) => typeof a === "string") : void 0,
|
|
278
|
+
shuffleChoices: typeof raw.shuffleChoices === "boolean" ? raw.shuffleChoices : void 0,
|
|
279
|
+
shuffleSeed: typeof raw.shuffleSeed === "string" || typeof raw.shuffleSeed === "number" ? raw.shuffleSeed : void 0,
|
|
280
|
+
choiceFeedback: raw.choiceFeedback && typeof raw.choiceFeedback === "object" && !Array.isArray(raw.choiceFeedback) ? Object.fromEntries(
|
|
281
|
+
Object.entries(raw.choiceFeedback).filter(
|
|
282
|
+
(entry) => typeof entry[1] === "string"
|
|
283
|
+
)
|
|
284
|
+
) : void 0
|
|
226
285
|
};
|
|
227
286
|
}
|
|
228
287
|
function parseCourseDescriptorInput(input) {
|
|
@@ -266,7 +325,7 @@ function parseCourseDescriptorInput(input) {
|
|
|
266
325
|
}
|
|
267
326
|
|
|
268
327
|
// src/descriptor/validateCourse.ts
|
|
269
|
-
var
|
|
328
|
+
var import_core4 = require("@lessonkit/core");
|
|
270
329
|
var import_themes2 = require("@lessonkit/themes");
|
|
271
330
|
|
|
272
331
|
// src/spaPath.ts
|
|
@@ -297,43 +356,32 @@ function assertResolvedPathUnderRoot(root, target) {
|
|
|
297
356
|
throw new Error(`unsafe path escapes project root: ${target}`);
|
|
298
357
|
}
|
|
299
358
|
}
|
|
300
|
-
function
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const next = (0, import_node_path.join)(current, segment);
|
|
309
|
-
if ((0, import_node_fs.existsSync)(next)) {
|
|
359
|
+
function resolvePhysicalPathForCheck(p) {
|
|
360
|
+
const resolved = resolveComparablePath(p);
|
|
361
|
+
try {
|
|
362
|
+
return import_node_fs.realpathSync.native(resolved);
|
|
363
|
+
} catch {
|
|
364
|
+
let probe = resolved;
|
|
365
|
+
let suffix = "";
|
|
366
|
+
while (true) {
|
|
310
367
|
try {
|
|
311
|
-
|
|
368
|
+
const physical = import_node_fs.realpathSync.native(probe);
|
|
369
|
+
return suffix ? (0, import_node_path.join)(physical, suffix) : physical;
|
|
312
370
|
} catch {
|
|
313
|
-
|
|
371
|
+
if (probe === (0, import_node_path.dirname)(probe)) {
|
|
372
|
+
return resolved;
|
|
373
|
+
}
|
|
374
|
+
const segment = (0, import_node_path.basename)(probe);
|
|
375
|
+
suffix = suffix ? (0, import_node_path.join)(segment, suffix) : segment;
|
|
376
|
+
probe = (0, import_node_path.dirname)(probe);
|
|
314
377
|
}
|
|
315
|
-
} else {
|
|
316
|
-
current = next;
|
|
317
378
|
}
|
|
318
|
-
assertResolvedPathUnderRoot(rootReal, current);
|
|
319
379
|
}
|
|
320
|
-
return current;
|
|
321
380
|
}
|
|
322
381
|
function assertRealPathUnderRoot(root, target) {
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
try {
|
|
327
|
-
rootReal = (0, import_node_fs.realpathSync)(rootResolved);
|
|
328
|
-
} catch {
|
|
329
|
-
rootReal = rootResolved;
|
|
330
|
-
}
|
|
331
|
-
try {
|
|
332
|
-
const targetCheck = (0, import_node_fs.realpathSync)(targetResolved);
|
|
333
|
-
assertResolvedPathUnderRoot(rootReal, targetCheck);
|
|
334
|
-
} catch {
|
|
335
|
-
resolveExistingPathUnderRoot(rootReal, rootResolved, targetResolved);
|
|
336
|
-
}
|
|
382
|
+
const rootPhysical = resolvePhysicalPathForCheck(root);
|
|
383
|
+
const targetPhysical = resolvePhysicalPathForCheck(target);
|
|
384
|
+
assertResolvedPathUnderRoot(rootPhysical, targetPhysical);
|
|
337
385
|
}
|
|
338
386
|
function normalizePathForComparison(p) {
|
|
339
387
|
const resolved = resolveComparablePath(p);
|
|
@@ -367,6 +415,21 @@ function isReservedOutputPath(value) {
|
|
|
367
415
|
const segments = normalized.split("/").filter(Boolean);
|
|
368
416
|
return segments.some((segment) => RESERVED_OUTPUT_SEGMENTS.has(segment));
|
|
369
417
|
}
|
|
418
|
+
function validateManifestName(name) {
|
|
419
|
+
if (!name.length) {
|
|
420
|
+
return "must be a non-empty string";
|
|
421
|
+
}
|
|
422
|
+
if (name.includes("/") || name.includes("\\")) {
|
|
423
|
+
return "must not contain path separators";
|
|
424
|
+
}
|
|
425
|
+
if (!isSafeRelativeSpaPath(name)) {
|
|
426
|
+
return "must be a safe relative name without '..' segments or absolute prefixes";
|
|
427
|
+
}
|
|
428
|
+
if (isReservedOutputPath(name) || isReservedOutputPath(`${name}.lkcourse`)) {
|
|
429
|
+
return "must not target reserved directories (.git, node_modules, .github)";
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
370
433
|
function isReservedResolvedOutputPath(projectRoot, resolved) {
|
|
371
434
|
const rootResolved = resolveComparablePath(projectRoot);
|
|
372
435
|
const targetResolved = resolveComparablePath(resolved);
|
|
@@ -465,6 +528,7 @@ function themeToLxpackRuntime(input) {
|
|
|
465
528
|
|
|
466
529
|
// src/descriptor/validateAssessments.ts
|
|
467
530
|
var import_core2 = require("@lessonkit/core");
|
|
531
|
+
var import_core3 = require("@lessonkit/core");
|
|
468
532
|
var validateMcqLike = (assessment, path, issues) => {
|
|
469
533
|
if (!("choices" in assessment) || !Array.isArray(assessment.choices)) {
|
|
470
534
|
issues.push({ path: `${path}.choices`, message: "choices is required for mcq" });
|
|
@@ -480,9 +544,44 @@ var validateMcqLike = (assessment, path, issues) => {
|
|
|
480
544
|
}
|
|
481
545
|
if (!assessment.answer.trim()) {
|
|
482
546
|
issues.push({ path: `${path}.answer`, message: "answer is required" });
|
|
483
|
-
} else if (trimmedChoices.length && !trimmedChoices.includes(assessment.answer.trim())) {
|
|
547
|
+
} else if (!("answers" in assessment && (0, import_core3.isMultiSelectMcq)({ answers: assessment.answers })) && trimmedChoices.length && !trimmedChoices.includes(assessment.answer.trim())) {
|
|
484
548
|
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
485
549
|
}
|
|
550
|
+
if ("answers" in assessment && assessment.answers !== void 0) {
|
|
551
|
+
if (!Array.isArray(assessment.answers)) {
|
|
552
|
+
issues.push({ path: `${path}.answers`, message: "answers must be an array when provided" });
|
|
553
|
+
} else {
|
|
554
|
+
const trimmedAnswers = assessment.answers.map((a) => a.trim()).filter((a) => a.length > 0);
|
|
555
|
+
if (assessment.answers.length > 0 && trimmedAnswers.length === 0) {
|
|
556
|
+
issues.push({ path: `${path}.answers`, message: "answers must include non-empty strings" });
|
|
557
|
+
}
|
|
558
|
+
const uniqueAnswers = new Set(trimmedAnswers);
|
|
559
|
+
if (trimmedAnswers.length !== uniqueAnswers.size) {
|
|
560
|
+
issues.push({ path: `${path}.answers`, message: "answers must be unique" });
|
|
561
|
+
}
|
|
562
|
+
for (const ans of trimmedAnswers) {
|
|
563
|
+
if (trimmedChoices.length && !trimmedChoices.includes(ans)) {
|
|
564
|
+
issues.push({ path: `${path}.answers`, message: "each answer must match a choice" });
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if ("choiceFeedback" in assessment && assessment.choiceFeedback !== void 0) {
|
|
571
|
+
if (typeof assessment.choiceFeedback !== "object" || assessment.choiceFeedback === null) {
|
|
572
|
+
issues.push({ path: `${path}.choiceFeedback`, message: "choiceFeedback must be an object" });
|
|
573
|
+
} else {
|
|
574
|
+
for (const key of Object.keys(assessment.choiceFeedback)) {
|
|
575
|
+
if (!trimmedChoices.includes(key.trim())) {
|
|
576
|
+
issues.push({
|
|
577
|
+
path: `${path}.choiceFeedback`,
|
|
578
|
+
message: "choiceFeedback keys must match choice labels"
|
|
579
|
+
});
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
486
585
|
const uniqueChoices = new Set(trimmedChoices);
|
|
487
586
|
if (trimmedChoices.length !== uniqueChoices.size) {
|
|
488
587
|
issues.push({ path: `${path}.choices`, message: "choices must be unique" });
|
|
@@ -502,6 +601,12 @@ function maxAchievableAssessmentScore(assessment) {
|
|
|
502
601
|
if (kind === "findMultipleHotspots" && assessment.kind === "findMultipleHotspots") {
|
|
503
602
|
return assessment.correctTargetIds?.map((id) => id.trim()).filter((id) => id.length > 0).length ?? 0;
|
|
504
603
|
}
|
|
604
|
+
if (kind === "sortParagraphs" && assessment.kind === "sortParagraphs") {
|
|
605
|
+
return assessment.paragraphs?.length ?? assessment.correctOrder?.length ?? 0;
|
|
606
|
+
}
|
|
607
|
+
if ("answers" in assessment && Array.isArray(assessment.answers) && assessment.answers.length > 1) {
|
|
608
|
+
return assessment.answers.filter((a) => a.trim().length > 0).length;
|
|
609
|
+
}
|
|
505
610
|
return 1;
|
|
506
611
|
}
|
|
507
612
|
var ASSESSMENT_VALIDATORS = {
|
|
@@ -587,7 +692,31 @@ var ASSESSMENT_VALIDATORS = {
|
|
|
587
692
|
message: "at least one non-empty correctTargetId is required for findMultipleHotspots"
|
|
588
693
|
});
|
|
589
694
|
}
|
|
590
|
-
}
|
|
695
|
+
},
|
|
696
|
+
sortParagraphs: (assessment, path, issues) => {
|
|
697
|
+
if (assessment.kind !== "sortParagraphs") return;
|
|
698
|
+
if (!Array.isArray(assessment.paragraphs) || assessment.paragraphs.length === 0) {
|
|
699
|
+
issues.push({ path: `${path}.paragraphs`, message: "paragraphs is required for sortParagraphs" });
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (!Array.isArray(assessment.correctOrder) || assessment.correctOrder.length === 0) {
|
|
703
|
+
issues.push({ path: `${path}.correctOrder`, message: "correctOrder is required for sortParagraphs" });
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (assessment.correctOrder.length !== assessment.paragraphs.length) {
|
|
707
|
+
issues.push({
|
|
708
|
+
path: `${path}.correctOrder`,
|
|
709
|
+
message: "correctOrder length must match paragraphs length for sortParagraphs"
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
guessTheAnswer: (assessment, path, issues) => {
|
|
714
|
+
if (assessment.kind !== "guessTheAnswer") return;
|
|
715
|
+
if (!assessment.answer?.trim()) {
|
|
716
|
+
issues.push({ path: `${path}.answer`, message: "answer is required for guessTheAnswer" });
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
multimediaChoice: validateMcqLike
|
|
591
720
|
};
|
|
592
721
|
function validateAssessmentEntry(assessment, index, issues, checkIds) {
|
|
593
722
|
const path = `assessments[${index}]`;
|
|
@@ -642,7 +771,7 @@ var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
|
|
|
642
771
|
var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
|
|
643
772
|
function validateCourseDescriptor(input) {
|
|
644
773
|
const issues = [];
|
|
645
|
-
const course = (0,
|
|
774
|
+
const course = (0, import_core4.validateId)(input.courseId, "courseId");
|
|
646
775
|
if (!course.ok) issues.push(...course.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
647
776
|
if (!input.title?.trim()) {
|
|
648
777
|
issues.push({ path: "title", message: "title is required" });
|
|
@@ -719,7 +848,7 @@ function validateCourseDescriptor(input) {
|
|
|
719
848
|
const spaPaths = /* @__PURE__ */ new Set();
|
|
720
849
|
for (const [index, lesson] of (input.lessons ?? []).entries()) {
|
|
721
850
|
const path = `lessons[${index}]`;
|
|
722
|
-
const lessonResult = (0,
|
|
851
|
+
const lessonResult = (0, import_core4.validateId)(lesson.id, `${path}.id`);
|
|
723
852
|
if (!lessonResult.ok) {
|
|
724
853
|
issues.push(...lessonResult.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
725
854
|
} else if (lessonIds.has(lessonResult.id)) {
|
|
@@ -751,7 +880,7 @@ function validateCourseDescriptor(input) {
|
|
|
751
880
|
}
|
|
752
881
|
if (layout === "single-spa" && input.spaLessonId?.trim()) {
|
|
753
882
|
const spaId = input.spaLessonId.trim();
|
|
754
|
-
const spaResult = (0,
|
|
883
|
+
const spaResult = (0, import_core4.validateId)(spaId, "spaLessonId");
|
|
755
884
|
if (!spaResult.ok) {
|
|
756
885
|
issues.push(...spaResult.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
757
886
|
} else if (!lessonIds.has(spaResult.id)) {
|
|
@@ -769,6 +898,7 @@ function validateCourseDescriptor(input) {
|
|
|
769
898
|
}
|
|
770
899
|
|
|
771
900
|
// src/assessments.ts
|
|
901
|
+
var import_core5 = require("@lessonkit/core");
|
|
772
902
|
var DEFAULT_SHELL_PASSING_SCORE = 1;
|
|
773
903
|
function escapeShellText(text) {
|
|
774
904
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -794,6 +924,7 @@ function mcqToLxpack(assessment) {
|
|
|
794
924
|
const prompt = sanitizeShellField(assessment.question);
|
|
795
925
|
if (!checkId || !prompt) return null;
|
|
796
926
|
const normalizedAnswer = assessment.answer.trim();
|
|
927
|
+
const multiCorrect = assessment.answers && assessment.answers.length > 1 ? new Set(assessment.answers.map((a) => a.trim())) : /* @__PURE__ */ new Set([normalizedAnswer]);
|
|
797
928
|
const choices = assessment.choices.map((text, index) => {
|
|
798
929
|
const sanitizedText = sanitizeShellField(text);
|
|
799
930
|
if (!sanitizedText) return null;
|
|
@@ -801,17 +932,21 @@ function mcqToLxpack(assessment) {
|
|
|
801
932
|
return {
|
|
802
933
|
id,
|
|
803
934
|
text: sanitizedText,
|
|
804
|
-
correct: text.trim()
|
|
935
|
+
correct: multiCorrect.has(text.trim())
|
|
805
936
|
};
|
|
806
937
|
});
|
|
807
938
|
if (choices.some((choice) => choice === null)) return null;
|
|
939
|
+
const multiSelect = (0, import_core5.isMultiSelectMcq)(assessment);
|
|
808
940
|
return {
|
|
809
941
|
id: checkId,
|
|
810
942
|
passingScore: assessment.passingScore ?? DEFAULT_SHELL_PASSING_SCORE,
|
|
943
|
+
shuffleChoices: assessment.shuffleChoices === true ? true : void 0,
|
|
944
|
+
showFeedback: assessment.choiceFeedback && Object.keys(assessment.choiceFeedback).length > 0 ? "immediate" : void 0,
|
|
811
945
|
questions: [
|
|
812
946
|
{
|
|
813
947
|
id: "q1",
|
|
814
948
|
prompt,
|
|
949
|
+
...multiSelect ? { selectionMode: "multiple" } : {},
|
|
815
950
|
choices
|
|
816
951
|
}
|
|
817
952
|
]
|
|
@@ -840,7 +975,20 @@ function assessmentDescriptorToLxpack(assessment) {
|
|
|
840
975
|
if (kind === "findMultipleHotspots") {
|
|
841
976
|
return null;
|
|
842
977
|
}
|
|
843
|
-
if (
|
|
978
|
+
if (kind === "sortParagraphs" || kind === "guessTheAnswer") {
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
if (kind === "multimediaChoice" && assessment.kind === "multimediaChoice") {
|
|
982
|
+
return mcqToLxpack({
|
|
983
|
+
kind: "mcq",
|
|
984
|
+
checkId: assessment.checkId,
|
|
985
|
+
question: assessment.question,
|
|
986
|
+
choices: assessment.choices,
|
|
987
|
+
answer: assessment.answer,
|
|
988
|
+
passingScore: assessment.passingScore
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
if ((kind === "mcq" || assessment.kind === void 0) && "choices" in assessment && "answer" in assessment && typeof assessment.answer === "string") {
|
|
844
992
|
return mcqToLxpack(assessment);
|
|
845
993
|
}
|
|
846
994
|
return null;
|
|
@@ -852,7 +1000,13 @@ function extractAssessments(descriptor) {
|
|
|
852
1000
|
// src/descriptor/validateInjectableAssessments.ts
|
|
853
1001
|
function validateInjectableAssessments(descriptor) {
|
|
854
1002
|
const issues = [];
|
|
855
|
-
const spaOnlyKinds = /* @__PURE__ */ new Set([
|
|
1003
|
+
const spaOnlyKinds = /* @__PURE__ */ new Set([
|
|
1004
|
+
"fillInBlanks",
|
|
1005
|
+
"findHotspot",
|
|
1006
|
+
"findMultipleHotspots",
|
|
1007
|
+
"sortParagraphs",
|
|
1008
|
+
"guessTheAnswer"
|
|
1009
|
+
]);
|
|
856
1010
|
(descriptor.assessments ?? []).forEach((assessment, index) => {
|
|
857
1011
|
if (assessmentDescriptorToLxpack(assessment) === null) {
|
|
858
1012
|
const kind = assessment.kind ?? "mcq";
|
|
@@ -1174,12 +1328,12 @@ function validateReactManifestParity(opts) {
|
|
|
1174
1328
|
}
|
|
1175
1329
|
|
|
1176
1330
|
// src/mapIds.ts
|
|
1177
|
-
var
|
|
1331
|
+
var import_core6 = require("@lessonkit/core");
|
|
1178
1332
|
function mapLessonkitIds(descriptor) {
|
|
1179
|
-
const courseId = (0,
|
|
1180
|
-
const lessonIds = descriptor.lessons.map((l) => (0,
|
|
1333
|
+
const courseId = (0, import_core6.assertValidId)(descriptor.courseId, "courseId");
|
|
1334
|
+
const lessonIds = descriptor.lessons.map((l) => (0, import_core6.assertValidId)(l.id, "lessonId"));
|
|
1181
1335
|
const checkIds = (descriptor.assessments ?? []).map(
|
|
1182
|
-
(a) => (0,
|
|
1336
|
+
(a) => (0, import_core6.assertValidId)(a.checkId, "checkId")
|
|
1183
1337
|
);
|
|
1184
1338
|
return { courseId, lessonIds, checkIds };
|
|
1185
1339
|
}
|
|
@@ -1398,8 +1552,8 @@ async function writeLxpackProject(options) {
|
|
|
1398
1552
|
}
|
|
1399
1553
|
|
|
1400
1554
|
// src/packageCourse.ts
|
|
1401
|
-
var
|
|
1402
|
-
var
|
|
1555
|
+
var import_node_path11 = require("path");
|
|
1556
|
+
var fsp4 = __toESM(require("fs/promises"), 1);
|
|
1403
1557
|
var import_api2 = require("@lxpack/api");
|
|
1404
1558
|
|
|
1405
1559
|
// src/packaging/validateInputs.ts
|
|
@@ -1552,21 +1706,6 @@ function validatePackageInputs(options) {
|
|
|
1552
1706
|
]
|
|
1553
1707
|
};
|
|
1554
1708
|
}
|
|
1555
|
-
try {
|
|
1556
|
-
relativePathUnderRoot(outDir, resolvedOutput);
|
|
1557
|
-
} catch {
|
|
1558
|
-
return {
|
|
1559
|
-
ok: false,
|
|
1560
|
-
courseDir: outDir,
|
|
1561
|
-
target,
|
|
1562
|
-
issues: [
|
|
1563
|
-
{
|
|
1564
|
-
path: "output",
|
|
1565
|
-
message: "output must resolve inside outDir"
|
|
1566
|
-
}
|
|
1567
|
-
]
|
|
1568
|
-
};
|
|
1569
|
-
}
|
|
1570
1709
|
}
|
|
1571
1710
|
return { ok: true, outDir, projectRoot };
|
|
1572
1711
|
}
|
|
@@ -1855,14 +1994,29 @@ async function promoteStagingToOutDir(stagingDir, outDir, options) {
|
|
|
1855
1994
|
});
|
|
1856
1995
|
}
|
|
1857
1996
|
|
|
1858
|
-
// src/packaging/
|
|
1997
|
+
// src/packaging/relocateOutput.ts
|
|
1859
1998
|
var fsp2 = __toESM(require("fs/promises"), 1);
|
|
1860
1999
|
var import_node_path9 = require("path");
|
|
2000
|
+
async function relocatePackageOutput(builtOutputPath, requestedOutputPath, projectRoot) {
|
|
2001
|
+
if (!builtOutputPath || !requestedOutputPath) return builtOutputPath;
|
|
2002
|
+
const resolvedBuilt = resolveComparablePath(builtOutputPath);
|
|
2003
|
+
const resolvedRequested = resolveComparablePath(requestedOutputPath);
|
|
2004
|
+
if (resolvedBuilt === resolvedRequested) return builtOutputPath;
|
|
2005
|
+
const root = (0, import_node_path9.resolve)(projectRoot);
|
|
2006
|
+
assertRealPathUnderRoot(root, resolvedRequested);
|
|
2007
|
+
await fsp2.mkdir((0, import_node_path9.dirname)(resolvedRequested), { recursive: true });
|
|
2008
|
+
await renameOrCopy(resolvedBuilt, resolvedRequested);
|
|
2009
|
+
return resolvedRequested;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// src/packaging/staging.ts
|
|
2013
|
+
var fsp3 = __toESM(require("fs/promises"), 1);
|
|
2014
|
+
var import_node_path10 = require("path");
|
|
1861
2015
|
var import_node_os = require("os");
|
|
1862
2016
|
var import_api = require("@lxpack/api");
|
|
1863
2017
|
async function buildStagingPackage(options) {
|
|
1864
2018
|
const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
|
|
1865
|
-
const stagingDir = await
|
|
2019
|
+
const stagingDir = await fsp3.mkdtemp((0, import_node_path10.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
|
|
1866
2020
|
let succeeded = false;
|
|
1867
2021
|
try {
|
|
1868
2022
|
let spaDirs;
|
|
@@ -1894,14 +2048,28 @@ async function buildStagingPackage(options) {
|
|
|
1894
2048
|
}
|
|
1895
2049
|
const interchange = descriptorToInterchange(descriptor);
|
|
1896
2050
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
1897
|
-
await
|
|
1898
|
-
const defaultOutput =
|
|
2051
|
+
await fsp3.mkdir((0, import_node_path10.join)(stagingDir, outputBase), { recursive: true });
|
|
2052
|
+
const defaultOutput = dir ? (0, import_node_path10.join)(outputBase, target) : (0, import_node_path10.join)(outputBase, `course-${target}.zip`);
|
|
2053
|
+
let packageOutput = output ?? defaultOutput;
|
|
2054
|
+
let requestedOutputPath;
|
|
2055
|
+
let requestedOutputDir;
|
|
2056
|
+
if (output) {
|
|
2057
|
+
const projectRoot = (0, import_node_path10.resolve)(writeOpts.projectRoot);
|
|
2058
|
+
const requested = (0, import_node_path10.isAbsolute)(output) ? (0, import_node_path10.resolve)(output) : (0, import_node_path10.resolve)(projectRoot, output);
|
|
2059
|
+
if (dir) {
|
|
2060
|
+
requestedOutputDir = requested;
|
|
2061
|
+
packageOutput = defaultOutput;
|
|
2062
|
+
} else {
|
|
2063
|
+
requestedOutputPath = requested;
|
|
2064
|
+
packageOutput = defaultOutput;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
1899
2067
|
const build = await (0, import_api.packageLessonkit)({
|
|
1900
2068
|
interchange,
|
|
1901
2069
|
spaDirs,
|
|
1902
2070
|
target,
|
|
1903
2071
|
courseDir: stagingDir,
|
|
1904
|
-
output:
|
|
2072
|
+
output: packageOutput,
|
|
1905
2073
|
dir,
|
|
1906
2074
|
outputBaseDir,
|
|
1907
2075
|
outputAnchorDir: stagingDir,
|
|
@@ -1925,17 +2093,19 @@ async function buildStagingPackage(options) {
|
|
|
1925
2093
|
stagingDir,
|
|
1926
2094
|
build,
|
|
1927
2095
|
outputPath: "outputPath" in build ? build.outputPath : void 0,
|
|
1928
|
-
outputDir: "outputDir" in build ? build.outputDir : void 0
|
|
2096
|
+
outputDir: "outputDir" in build ? build.outputDir : void 0,
|
|
2097
|
+
requestedOutputPath,
|
|
2098
|
+
requestedOutputDir
|
|
1929
2099
|
};
|
|
1930
2100
|
} catch (err) {
|
|
1931
|
-
await
|
|
2101
|
+
await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1932
2102
|
/* v8 ignore next */
|
|
1933
2103
|
() => void 0
|
|
1934
2104
|
);
|
|
1935
2105
|
throw err;
|
|
1936
2106
|
} finally {
|
|
1937
2107
|
if (!succeeded) {
|
|
1938
|
-
await
|
|
2108
|
+
await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1939
2109
|
/* v8 ignore next */
|
|
1940
2110
|
() => void 0
|
|
1941
2111
|
);
|
|
@@ -1943,7 +2113,7 @@ async function buildStagingPackage(options) {
|
|
|
1943
2113
|
}
|
|
1944
2114
|
}
|
|
1945
2115
|
async function ensureOutDirParent(outDir) {
|
|
1946
|
-
await
|
|
2116
|
+
await fsp3.mkdir((0, import_node_path10.dirname)(outDir), { recursive: true });
|
|
1947
2117
|
}
|
|
1948
2118
|
|
|
1949
2119
|
// src/packaging/issueSeverity.ts
|
|
@@ -1964,13 +2134,13 @@ function findPackagingWarningIssues(issues) {
|
|
|
1964
2134
|
// src/packageCourse.ts
|
|
1965
2135
|
async function validateLessonkitProject(options) {
|
|
1966
2136
|
return (0, import_api2.validateCourse)({
|
|
1967
|
-
courseDir: (0,
|
|
2137
|
+
courseDir: (0, import_node_path11.resolve)(options.courseDir),
|
|
1968
2138
|
target: options.target
|
|
1969
2139
|
});
|
|
1970
2140
|
}
|
|
1971
2141
|
async function buildLessonkitProject(options) {
|
|
1972
2142
|
const buildOptions = {
|
|
1973
|
-
courseDir: (0,
|
|
2143
|
+
courseDir: (0, import_node_path11.resolve)(options.courseDir),
|
|
1974
2144
|
target: options.target,
|
|
1975
2145
|
output: options.output,
|
|
1976
2146
|
dir: options.dir,
|
|
@@ -2001,7 +2171,7 @@ async function packageLessonkitCourse(options) {
|
|
|
2001
2171
|
if (!descriptorValidation.ok) {
|
|
2002
2172
|
return {
|
|
2003
2173
|
ok: false,
|
|
2004
|
-
courseDir: (0,
|
|
2174
|
+
courseDir: (0, import_node_path11.resolve)(writeOpts.outDir),
|
|
2005
2175
|
target,
|
|
2006
2176
|
issues: descriptorValidation.issues.map((i) => ({
|
|
2007
2177
|
path: i.path,
|
|
@@ -2051,7 +2221,7 @@ async function packageLessonkitCourse(options) {
|
|
|
2051
2221
|
};
|
|
2052
2222
|
}
|
|
2053
2223
|
if (!staged.ok) {
|
|
2054
|
-
await
|
|
2224
|
+
await fsp4.rm(staged.stagingDir, { recursive: true, force: true }).catch(
|
|
2055
2225
|
/* v8 ignore next */
|
|
2056
2226
|
() => void 0
|
|
2057
2227
|
);
|
|
@@ -2068,7 +2238,7 @@ async function packageLessonkitCourse(options) {
|
|
|
2068
2238
|
const { stagingDir, build } = staged;
|
|
2069
2239
|
const buildErrorIssues = findPackagingErrorIssues(build.issues);
|
|
2070
2240
|
if (buildErrorIssues.length > 0) {
|
|
2071
|
-
await
|
|
2241
|
+
await fsp4.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
2072
2242
|
/* v8 ignore next */
|
|
2073
2243
|
() => void 0
|
|
2074
2244
|
);
|
|
@@ -2085,13 +2255,13 @@ async function packageLessonkitCourse(options) {
|
|
|
2085
2255
|
}))
|
|
2086
2256
|
};
|
|
2087
2257
|
}
|
|
2088
|
-
const stagingRoot = await
|
|
2258
|
+
const stagingRoot = await fsp4.realpath(stagingDir);
|
|
2089
2259
|
const artifactIssues = [
|
|
2090
2260
|
validateArtifactInStaging(stagingRoot, staged.outputPath, "outputPath"),
|
|
2091
2261
|
validateArtifactInStaging(stagingRoot, staged.outputDir, "outputDir")
|
|
2092
2262
|
].filter((issue) => issue != null);
|
|
2093
2263
|
if (artifactIssues.length > 0) {
|
|
2094
|
-
await
|
|
2264
|
+
await fsp4.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
2095
2265
|
/* v8 ignore next */
|
|
2096
2266
|
() => void 0
|
|
2097
2267
|
);
|
|
@@ -2106,7 +2276,7 @@ async function packageLessonkitCourse(options) {
|
|
|
2106
2276
|
}
|
|
2107
2277
|
const buildWarningIssues = findPackagingWarningIssues(build.issues);
|
|
2108
2278
|
if (options.strictBuild && buildWarningIssues.length > 0) {
|
|
2109
|
-
await
|
|
2279
|
+
await fsp4.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
2110
2280
|
/* v8 ignore next */
|
|
2111
2281
|
() => void 0
|
|
2112
2282
|
);
|
|
@@ -2123,8 +2293,8 @@ async function packageLessonkitCourse(options) {
|
|
|
2123
2293
|
}))
|
|
2124
2294
|
};
|
|
2125
2295
|
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2296
|
+
let remappedOutputPath = remapArtifactPaths(stagingRoot, outDir, staged.outputPath);
|
|
2297
|
+
let remappedOutputDir = remapArtifactPaths(stagingRoot, outDir, staged.outputDir);
|
|
2128
2298
|
const validation = {
|
|
2129
2299
|
ok: true,
|
|
2130
2300
|
manifest: build.manifest,
|
|
@@ -2137,7 +2307,7 @@ async function packageLessonkitCourse(options) {
|
|
|
2137
2307
|
projectRoot: writeOpts.projectRoot
|
|
2138
2308
|
});
|
|
2139
2309
|
} catch (err) {
|
|
2140
|
-
await
|
|
2310
|
+
await fsp4.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
2141
2311
|
/* v8 ignore next */
|
|
2142
2312
|
() => void 0
|
|
2143
2313
|
);
|
|
@@ -2155,6 +2325,32 @@ async function packageLessonkitCourse(options) {
|
|
|
2155
2325
|
]
|
|
2156
2326
|
};
|
|
2157
2327
|
}
|
|
2328
|
+
try {
|
|
2329
|
+
remappedOutputPath = await relocatePackageOutput(
|
|
2330
|
+
remappedOutputPath,
|
|
2331
|
+
staged.requestedOutputPath,
|
|
2332
|
+
writeOpts.projectRoot
|
|
2333
|
+
);
|
|
2334
|
+
remappedOutputDir = await relocatePackageOutput(
|
|
2335
|
+
remappedOutputDir,
|
|
2336
|
+
staged.requestedOutputDir,
|
|
2337
|
+
writeOpts.projectRoot
|
|
2338
|
+
);
|
|
2339
|
+
} catch (err) {
|
|
2340
|
+
return {
|
|
2341
|
+
ok: false,
|
|
2342
|
+
courseDir: outDir,
|
|
2343
|
+
target,
|
|
2344
|
+
validation,
|
|
2345
|
+
build,
|
|
2346
|
+
issues: [
|
|
2347
|
+
{
|
|
2348
|
+
path: "output",
|
|
2349
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2350
|
+
}
|
|
2351
|
+
]
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2158
2354
|
const remappedBuild = { ...build };
|
|
2159
2355
|
if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
|
|
2160
2356
|
remappedBuild.outputPath = remappedOutputPath;
|
|
@@ -2198,8 +2394,9 @@ function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
|
|
|
2198
2394
|
}
|
|
2199
2395
|
const nameRaw = config.name;
|
|
2200
2396
|
const name = typeof nameRaw === "string" ? nameRaw.trim() : "";
|
|
2201
|
-
|
|
2202
|
-
|
|
2397
|
+
const nameIssue = validateManifestName(name);
|
|
2398
|
+
if (nameIssue) {
|
|
2399
|
+
issues.push({ path: "name", message: nameIssue });
|
|
2203
2400
|
}
|
|
2204
2401
|
const courseRaw = config.course;
|
|
2205
2402
|
if (Array.isArray(courseRaw)) {
|
|
@@ -2363,12 +2560,12 @@ var import_validators4 = require("@lxpack/validators");
|
|
|
2363
2560
|
|
|
2364
2561
|
// src/lkcourse/zip.ts
|
|
2365
2562
|
var import_node_fs5 = require("fs");
|
|
2366
|
-
var
|
|
2563
|
+
var import_node_path12 = require("path");
|
|
2367
2564
|
var import_fflate = require("fflate");
|
|
2368
2565
|
var MAX_LKCOURSE_UNCOMPRESSED_BYTES = 256 * 1024 * 1024;
|
|
2369
2566
|
function canonicalZipEntryPath(entryPath) {
|
|
2370
2567
|
const slashNormalized = entryPath.replace(/\\/g, "/");
|
|
2371
|
-
const canonical = (0,
|
|
2568
|
+
const canonical = (0, import_node_path12.normalize)(slashNormalized).replace(/\\/g, "/");
|
|
2372
2569
|
if (canonical !== slashNormalized) return null;
|
|
2373
2570
|
return canonical;
|
|
2374
2571
|
}
|
|
@@ -2443,7 +2640,7 @@ async function collectDistEntries(distDir, spaDistRelative) {
|
|
|
2443
2640
|
const walk = async (absDir, relPrefix) => {
|
|
2444
2641
|
const dirEntries = await readdir4(absDir, { withFileTypes: true });
|
|
2445
2642
|
for (const entry of dirEntries) {
|
|
2446
|
-
const abs = (0,
|
|
2643
|
+
const abs = (0, import_node_path12.join)(absDir, entry.name);
|
|
2447
2644
|
const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
|
|
2448
2645
|
const zipPath = `${spaDistRelative}/${rel}`.replace(/\\/g, "/");
|
|
2449
2646
|
if (!isSafeRelativeSpaPath(zipPath)) {
|
|
@@ -2545,8 +2742,8 @@ function parseLkcourseEnvelope(raw, label = "manifest.json") {
|
|
|
2545
2742
|
// src/lkcourse/blockTree.ts
|
|
2546
2743
|
var import_node_fs6 = require("fs");
|
|
2547
2744
|
var import_node_module = require("module");
|
|
2548
|
-
var
|
|
2549
|
-
var
|
|
2745
|
+
var import_node_path13 = require("path");
|
|
2746
|
+
var import_core7 = require("@lessonkit/core");
|
|
2550
2747
|
var import_meta = {};
|
|
2551
2748
|
var SCANNABLE_EXTENSIONS2 = [".tsx", ".ts", ".jsx", ".js"];
|
|
2552
2749
|
var ID_PROPS = ["courseId", "lessonId", "checkId", "blockId", "nodeId"];
|
|
@@ -2554,12 +2751,12 @@ function stripComments2(source) {
|
|
|
2554
2751
|
return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
|
|
2555
2752
|
}
|
|
2556
2753
|
function collectSourceUnderSrc2(projectRoot) {
|
|
2557
|
-
const srcDir = (0,
|
|
2754
|
+
const srcDir = (0, import_node_path13.join)(projectRoot, "src");
|
|
2558
2755
|
if (!(0, import_node_fs6.existsSync)(srcDir)) return [];
|
|
2559
2756
|
const results = [];
|
|
2560
2757
|
const walk = (dir) => {
|
|
2561
2758
|
for (const entry of (0, import_node_fs6.readdirSync)(dir)) {
|
|
2562
|
-
const abs = (0,
|
|
2759
|
+
const abs = (0, import_node_path13.join)(dir, entry);
|
|
2563
2760
|
try {
|
|
2564
2761
|
assertRealPathUnderRoot(projectRoot, abs);
|
|
2565
2762
|
} catch {
|
|
@@ -2570,7 +2767,7 @@ function collectSourceUnderSrc2(projectRoot) {
|
|
|
2570
2767
|
if (stat2.isDirectory()) {
|
|
2571
2768
|
walk(abs);
|
|
2572
2769
|
} else if (SCANNABLE_EXTENSIONS2.some((ext) => entry.endsWith(ext))) {
|
|
2573
|
-
results.push((0,
|
|
2770
|
+
results.push((0, import_node_path13.relative)(projectRoot, abs));
|
|
2574
2771
|
}
|
|
2575
2772
|
}
|
|
2576
2773
|
};
|
|
@@ -2684,7 +2881,7 @@ function validateNodeIds(node, pathPrefix, issues) {
|
|
|
2684
2881
|
for (const prop of ID_PROPS) {
|
|
2685
2882
|
const value = node[prop];
|
|
2686
2883
|
if (value === void 0) continue;
|
|
2687
|
-
const validated = (0,
|
|
2884
|
+
const validated = (0, import_core7.validateId)(value, prop);
|
|
2688
2885
|
if (!validated.ok) {
|
|
2689
2886
|
issues.push({
|
|
2690
2887
|
path: `${pathPrefix}.${prop}`,
|
|
@@ -2708,7 +2905,7 @@ function extractBlockTree(options) {
|
|
|
2708
2905
|
const sources = options.appSources ?? collectSourceUnderSrc2(options.projectRoot);
|
|
2709
2906
|
const blocks = [];
|
|
2710
2907
|
for (const rel of sources) {
|
|
2711
|
-
const abs = (0,
|
|
2908
|
+
const abs = (0, import_node_path13.join)(options.projectRoot, rel);
|
|
2712
2909
|
if (!(0, import_node_fs6.existsSync)(abs)) continue;
|
|
2713
2910
|
const source = (0, import_node_fs6.readFileSync)(abs, "utf8");
|
|
2714
2911
|
const parsed = parseJsxBlocks(source, blockTypes);
|
|
@@ -2724,7 +2921,7 @@ function extractBlockTree(options) {
|
|
|
2724
2921
|
// src/lkcourse/export.ts
|
|
2725
2922
|
var import_promises3 = require("fs/promises");
|
|
2726
2923
|
var import_node_module2 = require("module");
|
|
2727
|
-
var
|
|
2924
|
+
var import_node_path14 = require("path");
|
|
2728
2925
|
var import_validators2 = require("@lxpack/validators");
|
|
2729
2926
|
var import_meta2 = {};
|
|
2730
2927
|
function resolveLessonkitVersion(explicit) {
|
|
@@ -2738,9 +2935,9 @@ function resolveLessonkitVersion(explicit) {
|
|
|
2738
2935
|
}
|
|
2739
2936
|
}
|
|
2740
2937
|
async function exportLkcourse(options) {
|
|
2741
|
-
const projectRoot = (0,
|
|
2938
|
+
const projectRoot = (0, import_node_path14.resolve)(options.projectRoot);
|
|
2742
2939
|
const manifest = options.manifest;
|
|
2743
|
-
const spaDistDir = (0,
|
|
2940
|
+
const spaDistDir = (0, import_node_path14.join)(projectRoot, manifest.paths.spaDistDir);
|
|
2744
2941
|
try {
|
|
2745
2942
|
assertRealPathUnderRoot(projectRoot, spaDistDir);
|
|
2746
2943
|
await assertSpaDistContentsSafe({ main: spaDistDir }, projectRoot);
|
|
@@ -2755,6 +2952,16 @@ async function exportLkcourse(options) {
|
|
|
2755
2952
|
]
|
|
2756
2953
|
};
|
|
2757
2954
|
}
|
|
2955
|
+
const injectableIssues = validateInjectableAssessments(manifest.course);
|
|
2956
|
+
if (injectableIssues.length > 0) {
|
|
2957
|
+
return {
|
|
2958
|
+
ok: false,
|
|
2959
|
+
issues: injectableIssues.map((issue) => ({
|
|
2960
|
+
path: issue.path,
|
|
2961
|
+
message: issue.message
|
|
2962
|
+
}))
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2758
2965
|
const interchange = descriptorToInterchange(manifest.course);
|
|
2759
2966
|
const interchangeParsed = (0, import_validators2.parseLessonkitInterchange)(interchange);
|
|
2760
2967
|
if (!interchangeParsed.ok) {
|
|
@@ -2846,10 +3053,11 @@ async function exportLkcourse(options) {
|
|
|
2846
3053
|
return { ok: false, issues: envelopeCheck.issues };
|
|
2847
3054
|
}
|
|
2848
3055
|
zipEntries.set("manifest.json", utf8ToEntry(JSON.stringify(envelope, null, 2)));
|
|
2849
|
-
const archivePath = (0,
|
|
3056
|
+
const archivePath = (0, import_node_path14.resolve)(
|
|
2850
3057
|
projectRoot,
|
|
2851
3058
|
options.outPath ?? `${manifest.name}.lkcourse`
|
|
2852
3059
|
);
|
|
3060
|
+
const archiveRel = options.outPath ?? `${manifest.name}.lkcourse`;
|
|
2853
3061
|
try {
|
|
2854
3062
|
assertRealPathUnderRoot(projectRoot, archivePath);
|
|
2855
3063
|
} catch (err) {
|
|
@@ -2857,20 +3065,31 @@ async function exportLkcourse(options) {
|
|
|
2857
3065
|
ok: false,
|
|
2858
3066
|
issues: [
|
|
2859
3067
|
{
|
|
2860
|
-
path:
|
|
3068
|
+
path: archiveRel,
|
|
2861
3069
|
message: err instanceof Error ? err.message : String(err)
|
|
2862
3070
|
}
|
|
2863
3071
|
]
|
|
2864
3072
|
};
|
|
2865
3073
|
}
|
|
2866
|
-
if (
|
|
3074
|
+
if (isReservedResolvedOutputPath(projectRoot, archivePath)) {
|
|
3075
|
+
return {
|
|
3076
|
+
ok: false,
|
|
3077
|
+
issues: [
|
|
3078
|
+
{
|
|
3079
|
+
path: archiveRel,
|
|
3080
|
+
message: "output path must not target reserved directories (.git, node_modules, .github)"
|
|
3081
|
+
}
|
|
3082
|
+
]
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
if (!isSafeZipEntryPath(archiveRel)) {
|
|
2867
3086
|
return {
|
|
2868
3087
|
ok: false,
|
|
2869
3088
|
issues: [{ path: "outPath", message: "output path must be a safe relative path" }]
|
|
2870
3089
|
};
|
|
2871
3090
|
}
|
|
2872
3091
|
try {
|
|
2873
|
-
await (0, import_promises3.mkdir)((0,
|
|
3092
|
+
await (0, import_promises3.mkdir)((0, import_node_path14.dirname)(archivePath), { recursive: true });
|
|
2874
3093
|
const zipped = createZip(zipEntries);
|
|
2875
3094
|
await (0, import_promises3.writeFile)(archivePath, zipped);
|
|
2876
3095
|
} catch (err) {
|
|
@@ -2894,6 +3113,29 @@ async function exportLkcourse(options) {
|
|
|
2894
3113
|
|
|
2895
3114
|
// src/lkcourse/validate.ts
|
|
2896
3115
|
var import_validators3 = require("@lxpack/validators");
|
|
3116
|
+
|
|
3117
|
+
// src/lkcourse/assessmentParity.ts
|
|
3118
|
+
function validateLkcourseAssessmentConsistency(descriptor, interchange) {
|
|
3119
|
+
const issues = [];
|
|
3120
|
+
for (const issue of validateInjectableAssessments(descriptor)) {
|
|
3121
|
+
issues.push({
|
|
3122
|
+
path: `sourceManifest.course.${issue.path}`,
|
|
3123
|
+
message: issue.message
|
|
3124
|
+
});
|
|
3125
|
+
}
|
|
3126
|
+
const expectedIds = extractAssessments(descriptor).map((a) => a.id).sort();
|
|
3127
|
+
const interchangeIds = (interchange.assessments ?? []).map((a) => a.id).sort();
|
|
3128
|
+
const matches = expectedIds.length === interchangeIds.length && expectedIds.every((id, index) => id === interchangeIds[index]);
|
|
3129
|
+
if (!matches) {
|
|
3130
|
+
issues.push({
|
|
3131
|
+
path: "interchange.assessments",
|
|
3132
|
+
message: `injectable assessment ids [${expectedIds.join(", ")}] do not match interchange [${interchangeIds.join(", ")}]`
|
|
3133
|
+
});
|
|
3134
|
+
}
|
|
3135
|
+
return issues;
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
// src/lkcourse/validate.ts
|
|
2897
3139
|
function validateLkcourseArchiveEntries(entries, _archiveLabel) {
|
|
2898
3140
|
const issues = [];
|
|
2899
3141
|
const manifestData = entries.get("manifest.json");
|
|
@@ -2926,6 +3168,8 @@ function validateLkcourseArchiveEntries(entries, _archiveLabel) {
|
|
|
2926
3168
|
if (!entries.has(spaIndexPath)) {
|
|
2927
3169
|
issues.push({ path: spaIndexPath, message: "required file missing from archive" });
|
|
2928
3170
|
}
|
|
3171
|
+
const allowlisted = new Set(envelope.entries.map((entryPath) => entryPath.replace(/\\/g, "/")));
|
|
3172
|
+
const spaDistPrefix = `${spaDistDir}/`;
|
|
2929
3173
|
for (const entryPath of envelope.entries) {
|
|
2930
3174
|
if (!entries.has(entryPath)) {
|
|
2931
3175
|
issues.push({
|
|
@@ -2934,6 +3178,16 @@ function validateLkcourseArchiveEntries(entries, _archiveLabel) {
|
|
|
2934
3178
|
});
|
|
2935
3179
|
}
|
|
2936
3180
|
}
|
|
3181
|
+
for (const zipPath of entries.keys()) {
|
|
3182
|
+
const normalized = zipPath.replace(/\\/g, "/");
|
|
3183
|
+
if (!normalized.startsWith(spaDistPrefix)) continue;
|
|
3184
|
+
if (!allowlisted.has(normalized)) {
|
|
3185
|
+
issues.push({
|
|
3186
|
+
path: zipPath,
|
|
3187
|
+
message: "unlisted file under spaDistDir; not in manifest.entries"
|
|
3188
|
+
});
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
2937
3191
|
if (issues.length) return { ok: false, issues };
|
|
2938
3192
|
let interchangeRaw;
|
|
2939
3193
|
try {
|
|
@@ -2967,6 +3221,12 @@ function validateLkcourseArchiveEntries(entries, _archiveLabel) {
|
|
|
2967
3221
|
message: `does not match interchange.course.id (${interchangeCourseId})`
|
|
2968
3222
|
});
|
|
2969
3223
|
}
|
|
3224
|
+
issues.push(
|
|
3225
|
+
...validateLkcourseAssessmentConsistency(
|
|
3226
|
+
envelope.sourceManifest.course,
|
|
3227
|
+
interchange
|
|
3228
|
+
)
|
|
3229
|
+
);
|
|
2970
3230
|
if (issues.length) return { ok: false, issues };
|
|
2971
3231
|
const blockTreeData = entries.get("block-tree.json");
|
|
2972
3232
|
if (blockTreeData) {
|
|
@@ -3007,7 +3267,7 @@ function validateLkcourse(archivePath) {
|
|
|
3007
3267
|
|
|
3008
3268
|
// src/lkcourse/import.ts
|
|
3009
3269
|
var import_promises4 = require("fs/promises");
|
|
3010
|
-
var
|
|
3270
|
+
var import_node_path15 = require("path");
|
|
3011
3271
|
var IMPORT_ARTIFACTS = ["lessonkit.json", "dist"];
|
|
3012
3272
|
async function pathExists2(path) {
|
|
3013
3273
|
try {
|
|
@@ -3028,10 +3288,10 @@ async function renameOrCopy2(from, to, opts) {
|
|
|
3028
3288
|
await (0, import_promises4.rm)(from, { recursive: true, force: true });
|
|
3029
3289
|
}
|
|
3030
3290
|
}
|
|
3031
|
-
async function writeImportTree(stagingDir, manifest, entries, spaDistDir) {
|
|
3291
|
+
async function writeImportTree(stagingDir, manifest, entries, spaDistDir, allowlistedSpaPaths) {
|
|
3032
3292
|
let fileCount = 0;
|
|
3033
3293
|
await (0, import_promises4.writeFile)(
|
|
3034
|
-
(0,
|
|
3294
|
+
(0, import_node_path15.join)(stagingDir, "lessonkit.json"),
|
|
3035
3295
|
`${JSON.stringify(manifest, null, 2)}
|
|
3036
3296
|
`,
|
|
3037
3297
|
"utf8"
|
|
@@ -3040,14 +3300,17 @@ async function writeImportTree(stagingDir, manifest, entries, spaDistDir) {
|
|
|
3040
3300
|
for (const [entryPath, data] of entries) {
|
|
3041
3301
|
const normalized = entryPath.replace(/\\/g, "/");
|
|
3042
3302
|
if (!normalized.startsWith(`${spaDistDir}/`)) continue;
|
|
3303
|
+
if (!allowlistedSpaPaths.has(normalized)) {
|
|
3304
|
+
throw new Error(`unlisted spaDist entry rejected: ${entryPath}`);
|
|
3305
|
+
}
|
|
3043
3306
|
const relativeUnderSpa = normalized.slice(spaDistDir.length + 1);
|
|
3044
|
-
const outPath = (0,
|
|
3045
|
-
const resolvedOut = (0,
|
|
3307
|
+
const outPath = (0, import_node_path15.join)(stagingDir, spaDistDir, relativeUnderSpa);
|
|
3308
|
+
const resolvedOut = (0, import_node_path15.resolve)(outPath);
|
|
3046
3309
|
assertRealPathUnderRoot(stagingDir, resolvedOut);
|
|
3047
|
-
if (!isSafeZipEntryPath((0,
|
|
3310
|
+
if (!isSafeZipEntryPath((0, import_node_path15.join)(spaDistDir, relativeUnderSpa))) {
|
|
3048
3311
|
throw new Error(`unsafe extraction path: ${entryPath}`);
|
|
3049
3312
|
}
|
|
3050
|
-
await (0, import_promises4.mkdir)((0,
|
|
3313
|
+
await (0, import_promises4.mkdir)((0, import_node_path15.dirname)(resolvedOut), { recursive: true });
|
|
3051
3314
|
await (0, import_promises4.writeFile)(resolvedOut, data);
|
|
3052
3315
|
fileCount += 1;
|
|
3053
3316
|
}
|
|
@@ -3056,45 +3319,64 @@ async function writeImportTree(stagingDir, manifest, entries, spaDistDir) {
|
|
|
3056
3319
|
async function backupImportArtifacts(targetDir) {
|
|
3057
3320
|
const existing = [];
|
|
3058
3321
|
for (const name of IMPORT_ARTIFACTS) {
|
|
3059
|
-
if (await pathExists2((0,
|
|
3322
|
+
if (await pathExists2((0, import_node_path15.join)(targetDir, name))) {
|
|
3060
3323
|
existing.push(name);
|
|
3061
3324
|
}
|
|
3062
3325
|
}
|
|
3063
3326
|
if (!existing.length) return void 0;
|
|
3064
|
-
const backupDir = await (0, import_promises4.mkdtemp)((0,
|
|
3327
|
+
const backupDir = await (0, import_promises4.mkdtemp)((0, import_node_path15.join)(targetDir, ".lkcourse-backup-"));
|
|
3065
3328
|
for (const name of existing) {
|
|
3066
|
-
await renameOrCopy2((0,
|
|
3329
|
+
await renameOrCopy2((0, import_node_path15.join)(targetDir, name), (0, import_node_path15.join)(backupDir, name));
|
|
3067
3330
|
}
|
|
3068
3331
|
return backupDir;
|
|
3069
3332
|
}
|
|
3070
3333
|
async function restoreImportBackup(targetDir, backupDir) {
|
|
3071
3334
|
for (const name of IMPORT_ARTIFACTS) {
|
|
3072
|
-
const backupPath = (0,
|
|
3335
|
+
const backupPath = (0, import_node_path15.join)(backupDir, name);
|
|
3073
3336
|
if (!await pathExists2(backupPath)) continue;
|
|
3074
|
-
const destPath = (0,
|
|
3337
|
+
const destPath = (0, import_node_path15.join)(targetDir, name);
|
|
3075
3338
|
if (await pathExists2(destPath)) {
|
|
3076
3339
|
await (0, import_promises4.rm)(destPath, { recursive: true, force: true });
|
|
3077
3340
|
}
|
|
3078
3341
|
await renameOrCopy2(backupPath, destPath);
|
|
3079
3342
|
}
|
|
3080
3343
|
}
|
|
3344
|
+
async function snapshotPreExistingImportArtifacts(targetDir) {
|
|
3345
|
+
const existing = /* @__PURE__ */ new Set();
|
|
3346
|
+
for (const name of IMPORT_ARTIFACTS) {
|
|
3347
|
+
if (await pathExists2((0, import_node_path15.join)(targetDir, name))) {
|
|
3348
|
+
existing.add(name);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
return existing;
|
|
3352
|
+
}
|
|
3353
|
+
async function rollbackFailedImport(targetDir, backupDir, preExisting) {
|
|
3354
|
+
if (backupDir) {
|
|
3355
|
+
await restoreImportBackup(targetDir, backupDir);
|
|
3356
|
+
}
|
|
3357
|
+
for (const name of IMPORT_ARTIFACTS) {
|
|
3358
|
+
if (preExisting.has(name)) continue;
|
|
3359
|
+
const destPath = (0, import_node_path15.join)(targetDir, name);
|
|
3360
|
+
if (await pathExists2(destPath)) {
|
|
3361
|
+
await (0, import_promises4.rm)(destPath, { recursive: true, force: true });
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3081
3365
|
async function promoteImportStaging(stagingDir, targetDir) {
|
|
3366
|
+
await (0, import_promises4.mkdir)(targetDir, { recursive: true });
|
|
3082
3367
|
const entries = await (0, import_promises4.readdir)(stagingDir, { withFileTypes: true });
|
|
3083
3368
|
for (const entry of entries) {
|
|
3084
|
-
const srcPath = (0,
|
|
3085
|
-
const destPath = (0,
|
|
3086
|
-
if (entry.isDirectory()) {
|
|
3087
|
-
await (
|
|
3088
|
-
} else if (entry.isFile()) {
|
|
3089
|
-
await (0, import_promises4.mkdir)((0, import_node_path14.dirname)(destPath), { recursive: true });
|
|
3090
|
-
await (0, import_promises4.cp)(srcPath, destPath);
|
|
3369
|
+
const srcPath = (0, import_node_path15.join)(stagingDir, entry.name);
|
|
3370
|
+
const destPath = (0, import_node_path15.join)(targetDir, entry.name);
|
|
3371
|
+
if (entry.isDirectory() || entry.isFile()) {
|
|
3372
|
+
await renameOrCopy2(srcPath, destPath);
|
|
3091
3373
|
}
|
|
3092
3374
|
}
|
|
3093
3375
|
}
|
|
3094
3376
|
var promoteImportStagingImpl = promoteImportStaging;
|
|
3095
3377
|
async function importLkcourse(options) {
|
|
3096
|
-
const archivePath = (0,
|
|
3097
|
-
const targetDir = (0,
|
|
3378
|
+
const archivePath = (0, import_node_path15.resolve)(options.archivePath);
|
|
3379
|
+
const targetDir = (0, import_node_path15.resolve)(options.targetDir);
|
|
3098
3380
|
const validated = validateLkcourse(archivePath);
|
|
3099
3381
|
if (!validated.ok) return validated;
|
|
3100
3382
|
const { envelope, interchange } = validated;
|
|
@@ -3118,16 +3400,26 @@ async function importLkcourse(options) {
|
|
|
3118
3400
|
if (!read.ok) return read;
|
|
3119
3401
|
let stagingDir;
|
|
3120
3402
|
let backupDir;
|
|
3403
|
+
let preExisting;
|
|
3121
3404
|
try {
|
|
3122
|
-
stagingDir = await (0, import_promises4.mkdtemp)((0,
|
|
3123
|
-
const
|
|
3405
|
+
stagingDir = await (0, import_promises4.mkdtemp)((0, import_node_path15.join)(targetDir, ".lkcourse-import-"));
|
|
3406
|
+
const allowlistedSpaPaths = new Set(
|
|
3407
|
+
envelope.entries.map((entryPath) => entryPath.replace(/\\/g, "/")).filter((entryPath) => entryPath.startsWith(`${spaDistDir}/`))
|
|
3408
|
+
);
|
|
3409
|
+
const fileCount = await writeImportTree(
|
|
3410
|
+
stagingDir,
|
|
3411
|
+
manifest,
|
|
3412
|
+
read.entries,
|
|
3413
|
+
spaDistDir,
|
|
3414
|
+
allowlistedSpaPaths
|
|
3415
|
+
);
|
|
3416
|
+
preExisting = await snapshotPreExistingImportArtifacts(targetDir);
|
|
3124
3417
|
backupDir = await backupImportArtifacts(targetDir);
|
|
3125
3418
|
try {
|
|
3126
3419
|
await promoteImportStagingImpl(stagingDir, targetDir);
|
|
3127
3420
|
} catch (promoteError) {
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
}
|
|
3421
|
+
await rollbackFailedImport(targetDir, backupDir, preExisting);
|
|
3422
|
+
backupDir = void 0;
|
|
3131
3423
|
throw promoteError;
|
|
3132
3424
|
}
|
|
3133
3425
|
if (backupDir) {
|
|
@@ -3144,8 +3436,12 @@ async function importLkcourse(options) {
|
|
|
3144
3436
|
fileCount
|
|
3145
3437
|
};
|
|
3146
3438
|
} catch (err) {
|
|
3147
|
-
if (
|
|
3439
|
+
if (preExisting) {
|
|
3440
|
+
await rollbackFailedImport(targetDir, backupDir, preExisting).catch(() => void 0);
|
|
3441
|
+
} else if (backupDir) {
|
|
3148
3442
|
await restoreImportBackup(targetDir, backupDir).catch(() => void 0);
|
|
3443
|
+
}
|
|
3444
|
+
if (backupDir) {
|
|
3149
3445
|
await (0, import_promises4.rm)(backupDir, { recursive: true, force: true }).catch(() => void 0);
|
|
3150
3446
|
}
|
|
3151
3447
|
if (stagingDir) {
|
|
@@ -3197,6 +3493,7 @@ async function importLkcourse(options) {
|
|
|
3197
3493
|
validateLessonkitProject,
|
|
3198
3494
|
validateLkcourse,
|
|
3199
3495
|
validateLkcourseArchiveEntries,
|
|
3496
|
+
validateManifestName,
|
|
3200
3497
|
validatePackageInputs,
|
|
3201
3498
|
validateProjectPaths,
|
|
3202
3499
|
validateReactManifestParity,
|