@lessonkit/lxpack 1.4.0 → 1.6.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 +49 -7
- package/block-tree.v1.json +40 -0
- package/dist/bridge.cjs +229 -55
- package/dist/bridge.d.cts +59 -11
- package/dist/bridge.d.ts +59 -11
- package/dist/bridge.js +162 -42
- package/dist/chunk-HTZR4CF3.js +94 -0
- package/dist/index.cjs +1527 -209
- package/dist/index.d.cts +184 -5
- package/dist/index.d.ts +184 -5
- package/dist/index.js +1472 -183
- package/dist/telemetry-0fIWoomS.d.cts +17 -0
- package/dist/telemetry-0fIWoomS.d.ts +17 -0
- package/lessonkit-manifest.v1.json +6 -3
- package/lkcourse-format.v1.json +21 -0
- package/package.json +14 -8
- package/dist/chunk-DYQI222N.js +0 -41
- package/dist/telemetry-gCxlwc7I.d.cts +0 -9
- package/dist/telemetry-gCxlwc7I.d.ts +0 -9
package/dist/index.cjs
CHANGED
|
@@ -31,21 +31,27 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
LESSONKIT_TELEMETRY_EVENTS: () => import_tracking_schema2.LESSONKIT_TELEMETRY_EVENTS,
|
|
34
|
+
assertSpaDistContentsSafe: () => assertSpaDistContentsSafe,
|
|
34
35
|
assessmentDescriptorToLxpack: () => assessmentDescriptorToLxpack,
|
|
35
36
|
buildLessonkitProject: () => buildLessonkitProject,
|
|
36
37
|
buildStagingPackage: () => buildStagingPackage,
|
|
37
38
|
descriptorToInterchange: () => descriptorToInterchange,
|
|
38
39
|
ensureOutDirParent: () => ensureOutDirParent,
|
|
40
|
+
escapeShellText: () => escapeShellText,
|
|
41
|
+
exportLkcourse: () => exportLkcourse,
|
|
39
42
|
extractAssessments: () => extractAssessments,
|
|
40
|
-
|
|
43
|
+
extractBlockTree: () => extractBlockTree,
|
|
44
|
+
importLkcourse: () => importLkcourse,
|
|
45
|
+
lessonkitInterchangeSchema: () => import_validators4.lessonkitInterchangeSchema,
|
|
41
46
|
loadLessonkitManifestFromFile: () => loadLessonkitManifestFromFile,
|
|
42
47
|
mapLessonkitIds: () => mapLessonkitIds,
|
|
43
48
|
mapLessonkitTelemetryToBridgeAction: () => import_tracking_schema2.mapLessonkitTelemetryToBridgeAction,
|
|
44
49
|
mapLessonkitTelemetryToLxpack: () => import_tracking_schema2.mapLessonkitTelemetryToLxpack,
|
|
45
|
-
materializeLessonkitProject: () =>
|
|
50
|
+
materializeLessonkitProject: () => import_validators4.materializeLessonkitProject,
|
|
46
51
|
packageLessonkitCourse: () => packageLessonkitCourse,
|
|
47
|
-
parseLessonkitInterchange: () =>
|
|
52
|
+
parseLessonkitInterchange: () => import_validators4.parseLessonkitInterchange,
|
|
48
53
|
parseLessonkitManifest: () => parseLessonkitManifest,
|
|
54
|
+
parseLkcourseEnvelope: () => parseLkcourseEnvelope,
|
|
49
55
|
promoteStagingToOutDir: () => promoteStagingToOutDir,
|
|
50
56
|
remapArtifactPaths: () => remapArtifactPaths,
|
|
51
57
|
resolveSafePackageOutputOverride: () => resolveSafePackageOutputOverride,
|
|
@@ -55,6 +61,8 @@ __export(index_exports, {
|
|
|
55
61
|
validateDescriptor: () => validateDescriptor,
|
|
56
62
|
validateDescriptorForTarget: () => validateDescriptorForTarget,
|
|
57
63
|
validateLessonkitProject: () => validateLessonkitProject,
|
|
64
|
+
validateLkcourse: () => validateLkcourse,
|
|
65
|
+
validateLkcourseArchiveEntries: () => validateLkcourseArchiveEntries,
|
|
58
66
|
validatePackageInputs: () => validatePackageInputs,
|
|
59
67
|
validateProjectPaths: () => validateProjectPaths,
|
|
60
68
|
validateReactManifestParity: () => validateReactManifestParity,
|
|
@@ -159,10 +167,18 @@ function parseAssessmentDescriptor(raw) {
|
|
|
159
167
|
};
|
|
160
168
|
const kind = raw.kind;
|
|
161
169
|
if (kind === "trueFalse") {
|
|
170
|
+
let answer;
|
|
171
|
+
if (typeof raw.answer === "boolean") {
|
|
172
|
+
answer = raw.answer;
|
|
173
|
+
} else if (raw.answer === "true") {
|
|
174
|
+
answer = true;
|
|
175
|
+
} else if (raw.answer === "false") {
|
|
176
|
+
answer = false;
|
|
177
|
+
}
|
|
162
178
|
return {
|
|
163
179
|
kind: "trueFalse",
|
|
164
180
|
...base,
|
|
165
|
-
answer
|
|
181
|
+
answer
|
|
166
182
|
};
|
|
167
183
|
}
|
|
168
184
|
if (kind === "fillInBlanks") {
|
|
@@ -340,6 +356,98 @@ function isResolvedPathUnderRoot(root, target) {
|
|
|
340
356
|
return !rel.startsWith("..") && !(0, import_node_path.isAbsolute)(rel);
|
|
341
357
|
}
|
|
342
358
|
|
|
359
|
+
// src/validateProjectPaths.ts
|
|
360
|
+
var import_node_fs2 = require("fs");
|
|
361
|
+
var import_node_path2 = require("path");
|
|
362
|
+
var RESERVED_OUTPUT_SEGMENTS = /* @__PURE__ */ new Set([".git", "node_modules", ".github"]);
|
|
363
|
+
function isReservedOutputPath(value) {
|
|
364
|
+
let normalized = value.replace(/\\/g, "/");
|
|
365
|
+
while (normalized.startsWith("/")) normalized = normalized.slice(1);
|
|
366
|
+
while (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
|
|
367
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
368
|
+
return segments.some((segment) => RESERVED_OUTPUT_SEGMENTS.has(segment));
|
|
369
|
+
}
|
|
370
|
+
function isReservedResolvedOutputPath(projectRoot, resolved) {
|
|
371
|
+
const rootResolved = resolveComparablePath(projectRoot);
|
|
372
|
+
const targetResolved = resolveComparablePath(resolved);
|
|
373
|
+
try {
|
|
374
|
+
const rootReal = (0, import_node_fs2.existsSync)(rootResolved) ? (0, import_node_fs2.realpathSync)(rootResolved) : rootResolved;
|
|
375
|
+
const targetReal = (0, import_node_fs2.existsSync)(targetResolved) ? (0, import_node_fs2.realpathSync)(targetResolved) : targetResolved;
|
|
376
|
+
const rel = relativePathUnderRoot(rootReal, targetReal);
|
|
377
|
+
return isReservedOutputPath(rel);
|
|
378
|
+
} catch {
|
|
379
|
+
return isReservedOutputPath(resolved);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function validatePathField(value, fieldPath, projectRoot, issues, options) {
|
|
383
|
+
if (!isSafeRelativeSpaPath(value)) {
|
|
384
|
+
issues.push({
|
|
385
|
+
path: fieldPath,
|
|
386
|
+
message: "path must be relative without '..' segments or absolute prefixes"
|
|
387
|
+
});
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (options?.rejectReserved && isReservedOutputPath(value)) {
|
|
391
|
+
issues.push({
|
|
392
|
+
path: fieldPath,
|
|
393
|
+
message: "path must not target reserved directories (.git, node_modules, .github)"
|
|
394
|
+
});
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
assertRealPathUnderRoot(projectRoot, (0, import_node_path2.resolve)(projectRoot, value));
|
|
399
|
+
} catch {
|
|
400
|
+
issues.push({
|
|
401
|
+
path: fieldPath,
|
|
402
|
+
message: "path must resolve inside the project root"
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function validateProjectPaths(projectRoot, paths) {
|
|
407
|
+
const issues = [];
|
|
408
|
+
const root = (0, import_node_path2.resolve)(projectRoot);
|
|
409
|
+
if (paths.spaDistDir?.trim()) {
|
|
410
|
+
validatePathField(paths.spaDistDir.trim(), "paths.spaDistDir", root, issues, {
|
|
411
|
+
rejectReserved: true
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
if (paths.lxpackOutDir?.trim()) {
|
|
415
|
+
validatePathField(paths.lxpackOutDir.trim(), "paths.lxpackOutDir", root, issues, {
|
|
416
|
+
rejectReserved: true
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
if (paths.outputBaseDir?.trim()) {
|
|
420
|
+
validatePathField(paths.outputBaseDir.trim(), "paths.outputBaseDir", root, issues, {
|
|
421
|
+
rejectReserved: true
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return issues;
|
|
425
|
+
}
|
|
426
|
+
function resolveSafePackageOutputOverride(projectRoot, override) {
|
|
427
|
+
const root = (0, import_node_path2.resolve)(projectRoot);
|
|
428
|
+
const trimmed = override.trim();
|
|
429
|
+
if (!trimmed) {
|
|
430
|
+
throw new Error("output override must be a non-empty path");
|
|
431
|
+
}
|
|
432
|
+
if ((0, import_node_path2.isAbsolute)(trimmed)) {
|
|
433
|
+
const resolved2 = (0, import_node_path2.resolve)(trimmed);
|
|
434
|
+
assertRealPathUnderRoot(root, resolved2);
|
|
435
|
+
if (isReservedOutputPath(trimmed) || isReservedResolvedOutputPath(root, resolved2)) {
|
|
436
|
+
throw new Error(`unsafe output path: ${override} targets a reserved directory`);
|
|
437
|
+
}
|
|
438
|
+
return resolved2;
|
|
439
|
+
}
|
|
440
|
+
if (!isSafeRelativeSpaPath(trimmed)) {
|
|
441
|
+
throw new Error(`unsafe output path: ${override}`);
|
|
442
|
+
}
|
|
443
|
+
const resolved = (0, import_node_path2.resolve)(root, trimmed);
|
|
444
|
+
assertRealPathUnderRoot(root, resolved);
|
|
445
|
+
if (isReservedOutputPath(trimmed) || isReservedResolvedOutputPath(root, resolved)) {
|
|
446
|
+
throw new Error(`unsafe output path: ${override} targets a reserved directory`);
|
|
447
|
+
}
|
|
448
|
+
return resolved;
|
|
449
|
+
}
|
|
450
|
+
|
|
343
451
|
// src/theme.ts
|
|
344
452
|
var import_themes = require("@lessonkit/themes");
|
|
345
453
|
function themeToLxpackRuntime(input) {
|
|
@@ -404,8 +512,52 @@ var ASSESSMENT_VALIDATORS = {
|
|
|
404
512
|
}
|
|
405
513
|
},
|
|
406
514
|
fillInBlanks: (assessment, path, issues) => {
|
|
407
|
-
if (assessment.kind
|
|
515
|
+
if (assessment.kind !== "fillInBlanks") return;
|
|
516
|
+
if (!assessment.template?.trim()) {
|
|
408
517
|
issues.push({ path: `${path}.template`, message: "template is required for fillInBlanks" });
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const templateBlankCount = countStarDelimitedBlanks(assessment.template);
|
|
521
|
+
if (templateBlankCount === 0) {
|
|
522
|
+
issues.push({
|
|
523
|
+
path: `${path}.template`,
|
|
524
|
+
message: "template must include at least one blank wrapped in asterisks for fillInBlanks"
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
const explicitBlanks = [];
|
|
528
|
+
if (assessment.blanks !== void 0) {
|
|
529
|
+
for (let i = 0; i < assessment.blanks.length; i++) {
|
|
530
|
+
const blank = assessment.blanks[i];
|
|
531
|
+
if (!blank || typeof blank !== "object") {
|
|
532
|
+
issues.push({
|
|
533
|
+
path: `${path}.blanks[${i}]`,
|
|
534
|
+
message: "blank entry must be an object with non-empty id and answer"
|
|
535
|
+
});
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
const id = blank.id?.trim() ?? "";
|
|
539
|
+
const answer = blank.answer?.trim() ?? "";
|
|
540
|
+
if (!id || !answer) {
|
|
541
|
+
issues.push({
|
|
542
|
+
path: `${path}.blanks[${i}]`,
|
|
543
|
+
message: "blank entry must include non-empty id and answer"
|
|
544
|
+
});
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
explicitBlanks.push({ id, answer });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (assessment.blanks !== void 0 && explicitBlanks.length === 0 && !issues.some((issue) => issue.path?.startsWith(`${path}.blanks`))) {
|
|
551
|
+
issues.push({
|
|
552
|
+
path: `${path}.blanks`,
|
|
553
|
+
message: "blanks must include at least one entry with non-empty id and answer"
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
if (explicitBlanks.length > 0 && explicitBlanks.length !== templateBlankCount) {
|
|
557
|
+
issues.push({
|
|
558
|
+
path: `${path}.blanks`,
|
|
559
|
+
message: `blanks length (${explicitBlanks.length}) must match template blank count (${templateBlankCount})`
|
|
560
|
+
});
|
|
409
561
|
}
|
|
410
562
|
},
|
|
411
563
|
findHotspot: (assessment, path, issues) => {
|
|
@@ -543,6 +695,20 @@ function validateCourseDescriptor(input) {
|
|
|
543
695
|
});
|
|
544
696
|
}
|
|
545
697
|
}
|
|
698
|
+
const descriptorSpaDistDir = input.spaDistDir?.trim();
|
|
699
|
+
if (descriptorSpaDistDir) {
|
|
700
|
+
if (!isSafeRelativeSpaPath(descriptorSpaDistDir)) {
|
|
701
|
+
issues.push({
|
|
702
|
+
path: "spaDistDir",
|
|
703
|
+
message: "spaDistDir must be a relative path without '..' segments or absolute prefixes"
|
|
704
|
+
});
|
|
705
|
+
} else if (isReservedOutputPath(descriptorSpaDistDir)) {
|
|
706
|
+
issues.push({
|
|
707
|
+
path: "spaDistDir",
|
|
708
|
+
message: "spaDistDir must not target reserved directories (.git, node_modules, .github)"
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
546
712
|
if (layout === "single-spa" && (input.lessons?.length ?? 0) > 1) {
|
|
547
713
|
issues.push({
|
|
548
714
|
path: "lessons",
|
|
@@ -603,27 +769,49 @@ function validateCourseDescriptor(input) {
|
|
|
603
769
|
}
|
|
604
770
|
|
|
605
771
|
// src/assessments.ts
|
|
772
|
+
var DEFAULT_SHELL_PASSING_SCORE = 1;
|
|
773
|
+
function escapeShellText(text) {
|
|
774
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
775
|
+
}
|
|
776
|
+
function decodeShellEntities(text) {
|
|
777
|
+
return text.replace(/&/gi, "&").replace(/</gi, "<").replace(/>/gi, ">").replace(/"/gi, '"').replace(/'/gi, "'").replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&#(\d+);/g, (_, num) => String.fromCharCode(Number(num)));
|
|
778
|
+
}
|
|
779
|
+
function containsUnsafeShellMarkup(text) {
|
|
780
|
+
const decoded = decodeShellEntities(text);
|
|
781
|
+
return /<\/script/i.test(decoded) || /<!--/.test(decoded) || /<[a-zA-Z!/]/.test(decoded);
|
|
782
|
+
}
|
|
783
|
+
function sanitizeShellField(text) {
|
|
784
|
+
if (containsUnsafeShellMarkup(text)) return null;
|
|
785
|
+
return escapeShellText(text);
|
|
786
|
+
}
|
|
606
787
|
function slugChoiceId(text, index) {
|
|
607
788
|
const base = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
|
|
608
789
|
const stem = base.length ? base : "choice";
|
|
609
790
|
return `${stem}-${index + 1}`;
|
|
610
791
|
}
|
|
611
792
|
function mcqToLxpack(assessment) {
|
|
793
|
+
const checkId = sanitizeShellField(assessment.checkId);
|
|
794
|
+
const prompt = sanitizeShellField(assessment.question);
|
|
795
|
+
if (!checkId || !prompt) return null;
|
|
796
|
+
const normalizedAnswer = assessment.answer.trim();
|
|
612
797
|
const choices = assessment.choices.map((text, index) => {
|
|
798
|
+
const sanitizedText = sanitizeShellField(text);
|
|
799
|
+
if (!sanitizedText) return null;
|
|
613
800
|
const id = slugChoiceId(text, index);
|
|
614
801
|
return {
|
|
615
802
|
id,
|
|
616
|
-
text,
|
|
617
|
-
correct: text ===
|
|
803
|
+
text: sanitizedText,
|
|
804
|
+
correct: text.trim() === normalizedAnswer
|
|
618
805
|
};
|
|
619
806
|
});
|
|
807
|
+
if (choices.some((choice) => choice === null)) return null;
|
|
620
808
|
return {
|
|
621
|
-
id:
|
|
622
|
-
passingScore: assessment.passingScore ??
|
|
809
|
+
id: checkId,
|
|
810
|
+
passingScore: assessment.passingScore ?? DEFAULT_SHELL_PASSING_SCORE,
|
|
623
811
|
questions: [
|
|
624
812
|
{
|
|
625
813
|
id: "q1",
|
|
626
|
-
prompt
|
|
814
|
+
prompt,
|
|
627
815
|
choices
|
|
628
816
|
}
|
|
629
817
|
]
|
|
@@ -664,11 +852,14 @@ function extractAssessments(descriptor) {
|
|
|
664
852
|
// src/descriptor/validateInjectableAssessments.ts
|
|
665
853
|
function validateInjectableAssessments(descriptor) {
|
|
666
854
|
const issues = [];
|
|
855
|
+
const spaOnlyKinds = /* @__PURE__ */ new Set(["fillInBlanks", "findHotspot", "findMultipleHotspots"]);
|
|
667
856
|
(descriptor.assessments ?? []).forEach((assessment, index) => {
|
|
668
857
|
if (assessmentDescriptorToLxpack(assessment) === null) {
|
|
858
|
+
const kind = assessment.kind ?? "mcq";
|
|
859
|
+
const hint = spaOnlyKinds.has(kind) ? " \u2014 score in the SPA only; remove from lessonkit.json for LMS targets or use an injectable kind (mcq, trueFalse)" : "";
|
|
669
860
|
issues.push({
|
|
670
861
|
path: `assessments[${index}]`,
|
|
671
|
-
message: `assessment kind "${
|
|
862
|
+
message: `assessment kind "${kind}" (checkId "${assessment.checkId}") is not injected into LMS shell quizzes${hint}`
|
|
672
863
|
});
|
|
673
864
|
}
|
|
674
865
|
});
|
|
@@ -683,22 +874,29 @@ var LMS_SHELL_TARGETS = /* @__PURE__ */ new Set([
|
|
|
683
874
|
"xapi",
|
|
684
875
|
"cmi5"
|
|
685
876
|
]);
|
|
877
|
+
function appendActivityIriIssues(issues, descriptor, target) {
|
|
878
|
+
const hasXapiTracking = Boolean(descriptor.tracking?.xapi?.activityIri?.trim());
|
|
879
|
+
const requiresForTarget = target === "xapi" || target === "cmi5";
|
|
880
|
+
if (!hasXapiTracking && !requiresForTarget) return;
|
|
881
|
+
const activityIri = descriptor.tracking?.xapi?.activityIri?.trim();
|
|
882
|
+
const targetSuffix = target === "xapi" || target === "cmi5" ? ` for ${target} export targets` : " when tracking.xapi is configured";
|
|
883
|
+
if (!activityIri) {
|
|
884
|
+
issues.push({
|
|
885
|
+
path: "tracking.xapi.activityIri",
|
|
886
|
+
message: `tracking.xapi.activityIri is required${targetSuffix}`
|
|
887
|
+
});
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (!/^https:\/\/.+/i.test(activityIri)) {
|
|
891
|
+
issues.push({
|
|
892
|
+
path: "tracking.xapi.activityIri",
|
|
893
|
+
message: `tracking.xapi.activityIri must be an HTTPS URL${targetSuffix}`
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
686
897
|
function validateDescriptorForExportTarget(descriptor, target) {
|
|
687
898
|
const issues = [];
|
|
688
|
-
|
|
689
|
-
const activityIri = descriptor.tracking?.xapi?.activityIri?.trim();
|
|
690
|
-
if (!activityIri) {
|
|
691
|
-
issues.push({
|
|
692
|
-
path: "tracking.xapi.activityIri",
|
|
693
|
-
message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
|
|
694
|
-
});
|
|
695
|
-
} else if (!/^https:\/\/.+/i.test(activityIri)) {
|
|
696
|
-
issues.push({
|
|
697
|
-
path: "tracking.xapi.activityIri",
|
|
698
|
-
message: "tracking.xapi.activityIri must be an HTTPS URL for xapi and cmi5 export targets"
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
}
|
|
899
|
+
appendActivityIriIssues(issues, descriptor, target);
|
|
702
900
|
if (LMS_SHELL_TARGETS.has(target)) {
|
|
703
901
|
issues.push(...validateInjectableAssessments(descriptor).map((issue) => ({
|
|
704
902
|
...issue,
|
|
@@ -732,40 +930,123 @@ function validateDescriptorForTarget(input, target) {
|
|
|
732
930
|
}
|
|
733
931
|
|
|
734
932
|
// src/validateReactParity.ts
|
|
735
|
-
var
|
|
736
|
-
var
|
|
933
|
+
var import_node_fs3 = require("fs");
|
|
934
|
+
var import_node_path3 = require("path");
|
|
737
935
|
var SCANNABLE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
738
|
-
function collectSourceUnderSrc(projectRoot) {
|
|
739
|
-
const srcDir = (0,
|
|
740
|
-
if (!(0,
|
|
936
|
+
function collectSourceUnderSrc(projectRoot, issues) {
|
|
937
|
+
const srcDir = (0, import_node_path3.join)(projectRoot, "src");
|
|
938
|
+
if (!(0, import_node_fs3.existsSync)(srcDir)) return [];
|
|
741
939
|
const results = [];
|
|
742
940
|
const walk = (dir) => {
|
|
743
|
-
for (const entry of (0,
|
|
744
|
-
const abs = (0,
|
|
745
|
-
|
|
941
|
+
for (const entry of (0, import_node_fs3.readdirSync)(dir)) {
|
|
942
|
+
const abs = (0, import_node_path3.join)(dir, entry);
|
|
943
|
+
let stat2;
|
|
944
|
+
try {
|
|
945
|
+
stat2 = (0, import_node_fs3.lstatSync)(abs);
|
|
946
|
+
} catch {
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (stat2.isSymbolicLink()) {
|
|
950
|
+
issues.push({
|
|
951
|
+
path: (0, import_node_path3.relative)(projectRoot, abs),
|
|
952
|
+
message: `Source tree contains symlink (rejected for parity scan): ${(0, import_node_path3.relative)(projectRoot, abs)}`,
|
|
953
|
+
severity: "error"
|
|
954
|
+
});
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
if (stat2.isDirectory()) {
|
|
958
|
+
try {
|
|
959
|
+
assertRealPathUnderRoot(projectRoot, abs);
|
|
960
|
+
} catch {
|
|
961
|
+
issues.push({
|
|
962
|
+
path: (0, import_node_path3.relative)(projectRoot, abs),
|
|
963
|
+
message: `Source directory escapes project root: ${(0, import_node_path3.relative)(projectRoot, abs)}`,
|
|
964
|
+
severity: "error"
|
|
965
|
+
});
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
746
968
|
walk(abs);
|
|
747
969
|
} else if (SCANNABLE_EXTENSIONS.some((ext) => entry.endsWith(ext))) {
|
|
748
|
-
|
|
970
|
+
try {
|
|
971
|
+
assertRealPathUnderRoot(projectRoot, abs);
|
|
972
|
+
} catch {
|
|
973
|
+
issues.push({
|
|
974
|
+
path: (0, import_node_path3.relative)(projectRoot, abs),
|
|
975
|
+
message: `Source file escapes project root: ${(0, import_node_path3.relative)(projectRoot, abs)}`,
|
|
976
|
+
severity: "error"
|
|
977
|
+
});
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
results.push((0, import_node_path3.relative)(projectRoot, abs));
|
|
749
981
|
}
|
|
750
982
|
}
|
|
751
983
|
};
|
|
752
984
|
walk(srcDir);
|
|
753
985
|
return results;
|
|
754
986
|
}
|
|
755
|
-
function readAppSources(projectRoot, appSources) {
|
|
756
|
-
return appSources.map((rel) =>
|
|
987
|
+
function readAppSources(projectRoot, appSources, issues, customSourcesProvided) {
|
|
988
|
+
return appSources.map((rel) => {
|
|
989
|
+
if (!isSafeRelativeSpaPath(rel)) {
|
|
990
|
+
if (customSourcesProvided) {
|
|
991
|
+
issues.push({
|
|
992
|
+
path: rel,
|
|
993
|
+
message: `Unsafe appSources path skipped: ${rel}`,
|
|
994
|
+
severity: "warning"
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
const abs = (0, import_node_path3.join)(projectRoot, rel);
|
|
1000
|
+
try {
|
|
1001
|
+
assertRealPathUnderRoot(projectRoot, abs);
|
|
1002
|
+
if ((0, import_node_fs3.existsSync)(abs) && (0, import_node_fs3.lstatSync)(abs).isSymbolicLink()) {
|
|
1003
|
+
issues.push({
|
|
1004
|
+
path: rel,
|
|
1005
|
+
message: `appSources path is a symlink: ${rel}`,
|
|
1006
|
+
severity: "error"
|
|
1007
|
+
});
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
} catch {
|
|
1011
|
+
issues.push({
|
|
1012
|
+
path: rel,
|
|
1013
|
+
message: `appSources path escapes project root: ${rel}`,
|
|
1014
|
+
severity: "error"
|
|
1015
|
+
});
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
if (!(0, import_node_fs3.existsSync)(abs)) return null;
|
|
1019
|
+
return (0, import_node_fs3.readFileSync)(abs, "utf8");
|
|
1020
|
+
}).filter((content) => content != null).join("\n");
|
|
757
1021
|
}
|
|
758
1022
|
function stripComments(source) {
|
|
759
1023
|
return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
|
|
760
1024
|
}
|
|
761
|
-
function
|
|
762
|
-
return [
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
1025
|
+
function maskUnrelatedStringLiterals(source) {
|
|
1026
|
+
return source.replace(/(["'`])(?:\\.|(?!\1).)*\1/g, (match, _quote, offset, full) => {
|
|
1027
|
+
const before = full.slice(Math.max(0, offset - 24), offset);
|
|
1028
|
+
if (/\b(?:courseId|checkId|lessonId)\s*=\s*$/.test(before)) {
|
|
1029
|
+
return match;
|
|
1030
|
+
}
|
|
1031
|
+
return '""';
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
function idPropPresent(source, prop, id) {
|
|
1035
|
+
const stripped = stripComments(source);
|
|
1036
|
+
const masked = maskUnrelatedStringLiterals(stripped);
|
|
1037
|
+
return jsxPropRegex(prop, id).test(masked);
|
|
1038
|
+
}
|
|
1039
|
+
function escapeRegExp(value) {
|
|
1040
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1041
|
+
}
|
|
1042
|
+
function jsxPropRegex(prop, id) {
|
|
1043
|
+
const escapedId = escapeRegExp(id);
|
|
1044
|
+
return new RegExp(
|
|
1045
|
+
`(?<![A-Za-z0-9_$])${prop}\\s*=\\s*(?:"${escapedId}"|'${escapedId}'|\\{\\s*["'\`]${escapedId}["'\`]\\s*\\}|\\{\\s*\`${escapedId}\`\\s*\\})`
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
function maskStringLiterals(source) {
|
|
1049
|
+
return source.replace(/(["'`])(?:\\.|(?!\1).)*\1/g, '""');
|
|
769
1050
|
}
|
|
770
1051
|
function extractStringConstants(source) {
|
|
771
1052
|
const stripped = stripComments(source);
|
|
@@ -776,7 +1057,9 @@ function extractStringConstants(source) {
|
|
|
776
1057
|
}
|
|
777
1058
|
return map;
|
|
778
1059
|
}
|
|
779
|
-
function idUsedViaConstant(
|
|
1060
|
+
function idUsedViaConstant(source, prop, id, constants) {
|
|
1061
|
+
const stripped = stripComments(source);
|
|
1062
|
+
const masked = maskStringLiterals(stripped);
|
|
780
1063
|
for (const [name, value] of constants) {
|
|
781
1064
|
if (value !== id) continue;
|
|
782
1065
|
const jsxPatterns = [
|
|
@@ -785,40 +1068,72 @@ function idUsedViaConstant(stripped, prop, id, constants) {
|
|
|
785
1068
|
`${prop}={${name} }`,
|
|
786
1069
|
`${prop}={ ${name}}`
|
|
787
1070
|
];
|
|
788
|
-
if (jsxPatterns.some((p) =>
|
|
789
|
-
const objPatterns = [`${prop}: ${name}`, `${prop}:${name}`];
|
|
790
|
-
if (objPatterns.some((p) => stripped.includes(p))) return true;
|
|
1071
|
+
if (jsxPatterns.some((p) => masked.includes(p))) return true;
|
|
791
1072
|
}
|
|
792
1073
|
return false;
|
|
793
1074
|
}
|
|
794
|
-
function
|
|
1075
|
+
function lessonIdInDataLiteral(source, lessonId) {
|
|
795
1076
|
const stripped = stripComments(source);
|
|
796
|
-
|
|
797
|
-
return
|
|
1077
|
+
const escaped = escapeRegExp(lessonId);
|
|
1078
|
+
return new RegExp(`\\bid\\s*:\\s*["'\`]${escaped}["'\`]`).test(stripped);
|
|
798
1079
|
}
|
|
799
|
-
function
|
|
1080
|
+
function lessonIdPresent(source, lessonId) {
|
|
1081
|
+
if (idPropPresent(source, "lessonId", lessonId)) return true;
|
|
1082
|
+
if (idUsedViaConstant(source, "lessonId", lessonId, extractStringConstants(source))) return true;
|
|
1083
|
+
return lessonIdInDataLiteral(source, lessonId);
|
|
1084
|
+
}
|
|
1085
|
+
function courseConfigCourseIdPresent(source, courseId) {
|
|
800
1086
|
const stripped = stripComments(source);
|
|
801
|
-
|
|
802
|
-
|
|
1087
|
+
const escaped = escapeRegExp(courseId);
|
|
1088
|
+
const literalPattern = new RegExp(
|
|
1089
|
+
`(?<![A-Za-z0-9_$])courseId\\s*:\\s*(?:"${escaped}"|'${escaped}')`
|
|
1090
|
+
);
|
|
1091
|
+
if (literalPattern.test(stripped)) return true;
|
|
1092
|
+
return idUsedViaConstant(source, "courseId", courseId, extractStringConstants(source));
|
|
1093
|
+
}
|
|
1094
|
+
function courseMetaCourseIdPresent(source, courseId) {
|
|
1095
|
+
const constants = extractStringConstants(source);
|
|
1096
|
+
const stripped = stripComments(source);
|
|
1097
|
+
for (const [name, value] of constants) {
|
|
1098
|
+
if (value !== courseId) continue;
|
|
1099
|
+
if (!new RegExp(`\\bcourseId\\s*:\\s*${name}\\b`).test(stripped)) continue;
|
|
1100
|
+
if (/\blessons\s*:\s*\S/.test(stripped)) return true;
|
|
1101
|
+
}
|
|
1102
|
+
return false;
|
|
1103
|
+
}
|
|
1104
|
+
function courseIdPresent(source, courseId) {
|
|
1105
|
+
if (idPropPresent(source, "courseId", courseId)) return true;
|
|
1106
|
+
if (idUsedViaConstant(source, "courseId", courseId, extractStringConstants(source))) return true;
|
|
1107
|
+
if (courseMetaCourseIdPresent(source, courseId)) return true;
|
|
1108
|
+
return courseConfigCourseIdPresent(source, courseId);
|
|
1109
|
+
}
|
|
1110
|
+
function checkIdPresent(source, checkId) {
|
|
1111
|
+
if (idPropPresent(source, "checkId", checkId)) return true;
|
|
1112
|
+
return idUsedViaConstant(source, "checkId", checkId, extractStringConstants(source));
|
|
803
1113
|
}
|
|
804
1114
|
var ID_SYNC_DOC = "https://lessonkit.readthedocs.io/en/latest/guides/react-developers/quickstart.html#keep-react-ids-in-sync-with-lessonkitjson";
|
|
805
1115
|
function parityHint(message) {
|
|
806
1116
|
return `${message} See ${ID_SYNC_DOC}`;
|
|
807
1117
|
}
|
|
808
1118
|
function validateReactManifestParity(opts) {
|
|
809
|
-
const
|
|
810
|
-
const
|
|
1119
|
+
const issues = [];
|
|
1120
|
+
const customSourcesProvided = opts.appSources !== void 0;
|
|
1121
|
+
const appSources = opts.appSources ?? collectSourceUnderSrc(opts.projectRoot, issues);
|
|
1122
|
+
const source = readAppSources(
|
|
1123
|
+
opts.projectRoot,
|
|
1124
|
+
appSources,
|
|
1125
|
+
issues,
|
|
1126
|
+
customSourcesProvided
|
|
1127
|
+
);
|
|
811
1128
|
const hasDescriptorIds = Boolean(opts.descriptor.courseId) || (opts.descriptor.assessments?.length ?? 0) > 0;
|
|
812
1129
|
if (!source.trim()) {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
];
|
|
1130
|
+
issues.push({
|
|
1131
|
+
path: appSources.length > 0 ? appSources.join(", ") : "src/",
|
|
1132
|
+
message: hasDescriptorIds ? "React app source required for ID parity check when descriptor defines courseId or assessments" : "React app source not found for ID parity check",
|
|
1133
|
+
severity: hasDescriptorIds ? "error" : "warning"
|
|
1134
|
+
});
|
|
1135
|
+
return issues;
|
|
820
1136
|
}
|
|
821
|
-
const issues = [];
|
|
822
1137
|
const courseId = opts.descriptor.courseId;
|
|
823
1138
|
if (!courseIdPresent(source, courseId)) {
|
|
824
1139
|
issues.push({
|
|
@@ -829,6 +1144,19 @@ function validateReactManifestParity(opts) {
|
|
|
829
1144
|
severity: "error"
|
|
830
1145
|
});
|
|
831
1146
|
}
|
|
1147
|
+
for (const lesson of opts.descriptor.lessons ?? []) {
|
|
1148
|
+
const lessonId = lesson.id;
|
|
1149
|
+
if (!lessonId) continue;
|
|
1150
|
+
if (!lessonIdPresent(source, lessonId)) {
|
|
1151
|
+
issues.push({
|
|
1152
|
+
path: `lessons.id:${lessonId}`,
|
|
1153
|
+
message: parityHint(
|
|
1154
|
+
`React app source missing lessonId="${lessonId}" declared in lessonkit.json.`
|
|
1155
|
+
),
|
|
1156
|
+
severity: "error"
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
832
1160
|
for (const assessment of opts.descriptor.assessments ?? []) {
|
|
833
1161
|
const checkId = assessment.checkId;
|
|
834
1162
|
if (!checkId) continue;
|
|
@@ -845,58 +1173,6 @@ function validateReactManifestParity(opts) {
|
|
|
845
1173
|
return issues;
|
|
846
1174
|
}
|
|
847
1175
|
|
|
848
|
-
// src/validateProjectPaths.ts
|
|
849
|
-
var import_node_path3 = require("path");
|
|
850
|
-
function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
851
|
-
if (!isSafeRelativeSpaPath(value)) {
|
|
852
|
-
issues.push({
|
|
853
|
-
path: fieldPath,
|
|
854
|
-
message: "path must be relative without '..' segments or absolute prefixes"
|
|
855
|
-
});
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
try {
|
|
859
|
-
assertRealPathUnderRoot(projectRoot, (0, import_node_path3.resolve)(projectRoot, value));
|
|
860
|
-
} catch {
|
|
861
|
-
issues.push({
|
|
862
|
-
path: fieldPath,
|
|
863
|
-
message: "path must resolve inside the project root"
|
|
864
|
-
});
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
function validateProjectPaths(projectRoot, paths) {
|
|
868
|
-
const issues = [];
|
|
869
|
-
const root = (0, import_node_path3.resolve)(projectRoot);
|
|
870
|
-
if (paths.spaDistDir?.trim()) {
|
|
871
|
-
validatePathField(paths.spaDistDir.trim(), "paths.spaDistDir", root, issues);
|
|
872
|
-
}
|
|
873
|
-
if (paths.lxpackOutDir?.trim()) {
|
|
874
|
-
validatePathField(paths.lxpackOutDir.trim(), "paths.lxpackOutDir", root, issues);
|
|
875
|
-
}
|
|
876
|
-
if (paths.outputBaseDir?.trim()) {
|
|
877
|
-
validatePathField(paths.outputBaseDir.trim(), "paths.outputBaseDir", root, issues);
|
|
878
|
-
}
|
|
879
|
-
return issues;
|
|
880
|
-
}
|
|
881
|
-
function resolveSafePackageOutputOverride(projectRoot, override) {
|
|
882
|
-
const root = (0, import_node_path3.resolve)(projectRoot);
|
|
883
|
-
const trimmed = override.trim();
|
|
884
|
-
if (!trimmed) {
|
|
885
|
-
throw new Error("output override must be a non-empty path");
|
|
886
|
-
}
|
|
887
|
-
if ((0, import_node_path3.isAbsolute)(trimmed)) {
|
|
888
|
-
const resolved2 = (0, import_node_path3.resolve)(trimmed);
|
|
889
|
-
assertRealPathUnderRoot(root, resolved2);
|
|
890
|
-
return resolved2;
|
|
891
|
-
}
|
|
892
|
-
if (!isSafeRelativeSpaPath(trimmed)) {
|
|
893
|
-
throw new Error(`unsafe output path: ${override}`);
|
|
894
|
-
}
|
|
895
|
-
const resolved = (0, import_node_path3.resolve)(root, trimmed);
|
|
896
|
-
assertRealPathUnderRoot(root, resolved);
|
|
897
|
-
return resolved;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
1176
|
// src/mapIds.ts
|
|
901
1177
|
var import_core4 = require("@lessonkit/core");
|
|
902
1178
|
function mapLessonkitIds(descriptor) {
|
|
@@ -1028,7 +1304,7 @@ async function resolveSpaDirs(options) {
|
|
|
1028
1304
|
|
|
1029
1305
|
// src/spaDistValidation.ts
|
|
1030
1306
|
var import_promises2 = require("fs/promises");
|
|
1031
|
-
var
|
|
1307
|
+
var import_node_fs4 = require("fs");
|
|
1032
1308
|
var import_node_path5 = require("path");
|
|
1033
1309
|
async function assertSpaDistContentsSafe(spaDirs, projectRoot) {
|
|
1034
1310
|
for (const [label, dir] of Object.entries(spaDirs)) {
|
|
@@ -1039,7 +1315,7 @@ async function assertSpaDistContentsSafe(spaDirs, projectRoot) {
|
|
|
1039
1315
|
}
|
|
1040
1316
|
let rootReal;
|
|
1041
1317
|
try {
|
|
1042
|
-
rootReal = (0,
|
|
1318
|
+
rootReal = (0, import_node_fs4.realpathSync)(dirResolved);
|
|
1043
1319
|
} catch {
|
|
1044
1320
|
throw new Error(`spa dist for "${label}" is not readable: ${dir}`);
|
|
1045
1321
|
}
|
|
@@ -1068,9 +1344,12 @@ async function walkDistDir(rootReal, current, label) {
|
|
|
1068
1344
|
}
|
|
1069
1345
|
let entryReal;
|
|
1070
1346
|
try {
|
|
1071
|
-
entryReal = (0,
|
|
1072
|
-
} catch {
|
|
1073
|
-
|
|
1347
|
+
entryReal = (0, import_node_fs4.realpathSync)(entryPath);
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
throw new Error(
|
|
1350
|
+
`spa dist for "${label}" could not resolve path: ${entryPath}`,
|
|
1351
|
+
{ cause: err }
|
|
1352
|
+
);
|
|
1074
1353
|
}
|
|
1075
1354
|
assertResolvedPathUnderRoot(rootReal, entryReal);
|
|
1076
1355
|
if (stat2.isDirectory()) {
|
|
@@ -1090,12 +1369,12 @@ async function writeLxpackProject(options) {
|
|
|
1090
1369
|
const descriptor = validation.descriptor;
|
|
1091
1370
|
const injectableIssues = validateInjectableAssessments(descriptor);
|
|
1092
1371
|
if (injectableIssues.length > 0) {
|
|
1093
|
-
throw new Error(
|
|
1372
|
+
throw new Error(
|
|
1373
|
+
injectableIssues.map((i) => `${i.path ?? "assessments"}: ${i.message}`).join("; ")
|
|
1374
|
+
);
|
|
1094
1375
|
}
|
|
1095
1376
|
const outDir = (0, import_node_path6.resolve)(options.outDir);
|
|
1096
|
-
|
|
1097
|
-
assertRealPathUnderRoot((0, import_node_path6.resolve)(options.projectRoot), outDir);
|
|
1098
|
-
}
|
|
1377
|
+
assertRealPathUnderRoot((0, import_node_path6.resolve)(options.projectRoot), outDir);
|
|
1099
1378
|
const spaDirs = await resolveSpaDirs({ ...options, descriptor });
|
|
1100
1379
|
await assertSpaDistContentsSafe(spaDirs, options.projectRoot);
|
|
1101
1380
|
const interchange = descriptorToInterchange(descriptor);
|
|
@@ -1155,6 +1434,19 @@ function validatePackageInputs(options) {
|
|
|
1155
1434
|
]
|
|
1156
1435
|
};
|
|
1157
1436
|
}
|
|
1437
|
+
if (isReservedOutputPath(outDir) || isReservedResolvedOutputPath(projectRoot, outDir)) {
|
|
1438
|
+
return {
|
|
1439
|
+
ok: false,
|
|
1440
|
+
courseDir: outDir,
|
|
1441
|
+
target,
|
|
1442
|
+
issues: [
|
|
1443
|
+
{
|
|
1444
|
+
path: "outDir",
|
|
1445
|
+
message: "outDir must not target reserved directories (.git, node_modules, .github)"
|
|
1446
|
+
}
|
|
1447
|
+
]
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1158
1450
|
if (outputBaseDir && !isSafeRelativeSpaPath(outputBaseDir)) {
|
|
1159
1451
|
return {
|
|
1160
1452
|
ok: false,
|
|
@@ -1212,6 +1504,19 @@ function validatePackageInputs(options) {
|
|
|
1212
1504
|
]
|
|
1213
1505
|
};
|
|
1214
1506
|
}
|
|
1507
|
+
if (isReservedOutputPath(outputBaseDir) || isReservedResolvedOutputPath(projectRoot, resolvedOutputBase)) {
|
|
1508
|
+
return {
|
|
1509
|
+
ok: false,
|
|
1510
|
+
courseDir: outDir,
|
|
1511
|
+
target,
|
|
1512
|
+
issues: [
|
|
1513
|
+
{
|
|
1514
|
+
path: "outputBaseDir",
|
|
1515
|
+
message: "outputBaseDir must not target reserved directories (.git, node_modules, .github)"
|
|
1516
|
+
}
|
|
1517
|
+
]
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1215
1520
|
}
|
|
1216
1521
|
if (output) {
|
|
1217
1522
|
const resolvedOutput = (0, import_node_path7.isAbsolute)(output) ? (0, import_node_path7.resolve)(output) : (0, import_node_path7.resolve)(projectRoot, output);
|
|
@@ -1233,28 +1538,57 @@ function validatePackageInputs(options) {
|
|
|
1233
1538
|
]
|
|
1234
1539
|
};
|
|
1235
1540
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1541
|
+
const outputRel = (0, import_node_path7.isAbsolute)(output) ? output : output;
|
|
1542
|
+
if (isReservedOutputPath(outputRel) || isReservedResolvedOutputPath(projectRoot, resolvedOutput)) {
|
|
1543
|
+
return {
|
|
1544
|
+
ok: false,
|
|
1545
|
+
courseDir: outDir,
|
|
1546
|
+
target,
|
|
1547
|
+
issues: [
|
|
1548
|
+
{
|
|
1549
|
+
path: "output",
|
|
1550
|
+
message: "output must not target reserved directories (.git, node_modules, .github)"
|
|
1551
|
+
}
|
|
1552
|
+
]
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
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
|
+
}
|
|
1571
|
+
return { ok: true, outDir, projectRoot };
|
|
1572
|
+
}
|
|
1573
|
+
function validateArtifactInStaging(stagingRoot, artifactPath, field) {
|
|
1574
|
+
if (!artifactPath) return null;
|
|
1575
|
+
const resolved = resolveComparablePath(artifactPath);
|
|
1576
|
+
if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
|
|
1577
|
+
return {
|
|
1578
|
+
path: field,
|
|
1579
|
+
message: `${field} is outside the staging directory: ${artifactPath}`
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
|
|
1585
|
+
if (!artifactPath) return void 0;
|
|
1586
|
+
const resolved = resolveComparablePath(artifactPath);
|
|
1587
|
+
if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
|
|
1588
|
+
throw new Error(`${artifactPath} is outside the staging directory`);
|
|
1589
|
+
}
|
|
1590
|
+
const rel = relativePathUnderRoot(stagingRoot, resolved);
|
|
1591
|
+
if (rel.startsWith("..") || (0, import_node_path7.isAbsolute)(rel)) {
|
|
1258
1592
|
throw new Error(`${artifactPath} is outside the staging directory`);
|
|
1259
1593
|
}
|
|
1260
1594
|
if (!rel) return outDir;
|
|
@@ -1291,33 +1625,53 @@ function promoteLockPath(outDir) {
|
|
|
1291
1625
|
const hash = (0, import_node_crypto.createHash)("sha256").update((0, import_node_path8.resolve)(outDir)).digest("hex").slice(0, 16);
|
|
1292
1626
|
return (0, import_node_path8.join)(parent, `.lk-promote-lock-${hash}`);
|
|
1293
1627
|
}
|
|
1294
|
-
var
|
|
1628
|
+
var STALE_ARTIFACT_TTL_MS = 5 * 60 * 1e3;
|
|
1629
|
+
var MAX_LOCK_AGE_MS = 30 * 60 * 1e3;
|
|
1630
|
+
var LOCK_TOKEN_RE = /^(\d+)\n([0-9a-f-]{36})(?:\n(\d+))?\n?$/i;
|
|
1295
1631
|
async function isStalePromoteLock(lockPath) {
|
|
1296
1632
|
try {
|
|
1633
|
+
const stat2 = await fsp.stat(lockPath);
|
|
1297
1634
|
const content = await fsp.readFile(lockPath, "utf8");
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
return true;
|
|
1635
|
+
const match = content.match(LOCK_TOKEN_RE);
|
|
1636
|
+
let lockAgeMs = Date.now() - stat2.mtimeMs;
|
|
1637
|
+
if (match?.[3]) {
|
|
1638
|
+
const startedAt = Number.parseInt(match[3], 10);
|
|
1639
|
+
if (Number.isFinite(startedAt) && startedAt > 0) {
|
|
1640
|
+
lockAgeMs = Date.now() - startedAt;
|
|
1305
1641
|
}
|
|
1306
1642
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1643
|
+
if (lockAgeMs > MAX_LOCK_AGE_MS) {
|
|
1644
|
+
return true;
|
|
1645
|
+
}
|
|
1646
|
+
if (match) {
|
|
1647
|
+
const pid = Number.parseInt(match[1], 10);
|
|
1648
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
1649
|
+
try {
|
|
1650
|
+
process.kill(pid, 0);
|
|
1651
|
+
return false;
|
|
1652
|
+
} catch {
|
|
1653
|
+
return true;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
return lockAgeMs > STALE_ARTIFACT_TTL_MS;
|
|
1309
1658
|
} catch {
|
|
1310
1659
|
return true;
|
|
1311
1660
|
}
|
|
1312
1661
|
}
|
|
1662
|
+
var PROMOTE_LOCK_TIMEOUT_MS = 15e3;
|
|
1313
1663
|
async function withPromoteLock(outDir, fn) {
|
|
1314
1664
|
const lockPath = promoteLockPath(outDir);
|
|
1315
1665
|
await fsp.mkdir((0, import_node_path8.dirname)(outDir), { recursive: true });
|
|
1316
1666
|
let lockHandle;
|
|
1317
|
-
|
|
1667
|
+
const maxAttempts = 400;
|
|
1668
|
+
const started = Date.now();
|
|
1669
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1318
1670
|
try {
|
|
1319
1671
|
lockHandle = await fsp.open(lockPath, "wx");
|
|
1320
1672
|
await lockHandle.writeFile(`${process.pid}
|
|
1673
|
+
${(0, import_node_crypto.randomUUID)()}
|
|
1674
|
+
${Date.now()}
|
|
1321
1675
|
`, "utf8");
|
|
1322
1676
|
break;
|
|
1323
1677
|
} catch (err) {
|
|
@@ -1330,7 +1684,9 @@ async function withPromoteLock(outDir, fn) {
|
|
|
1330
1684
|
);
|
|
1331
1685
|
continue;
|
|
1332
1686
|
}
|
|
1333
|
-
|
|
1687
|
+
if (Date.now() - started >= PROMOTE_LOCK_TIMEOUT_MS) break;
|
|
1688
|
+
const delayMs = Math.min(25 * 2 ** Math.floor(attempt / 20), 250);
|
|
1689
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, delayMs));
|
|
1334
1690
|
}
|
|
1335
1691
|
}
|
|
1336
1692
|
if (!lockHandle) {
|
|
@@ -1349,22 +1705,77 @@ async function withPromoteLock(outDir, fn) {
|
|
|
1349
1705
|
);
|
|
1350
1706
|
}
|
|
1351
1707
|
}
|
|
1352
|
-
async function
|
|
1708
|
+
async function removeStaleLegacyPromoteArtifacts(outDir) {
|
|
1353
1709
|
const legacyTmp = `${outDir}.tmp-promote`;
|
|
1354
1710
|
const legacyBak = `${outDir}.bak`;
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1711
|
+
const blocked = [];
|
|
1712
|
+
for (const legacyPath of [legacyTmp, legacyBak]) {
|
|
1713
|
+
if (!await pathExists(legacyPath)) continue;
|
|
1714
|
+
try {
|
|
1715
|
+
const stat2 = await fsp.stat(legacyPath);
|
|
1716
|
+
if (Date.now() - stat2.mtimeMs > STALE_ARTIFACT_TTL_MS) {
|
|
1717
|
+
await fsp.rm(legacyPath, { recursive: true, force: true }).catch(
|
|
1718
|
+
/* v8 ignore next */
|
|
1719
|
+
() => void 0
|
|
1720
|
+
);
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
} catch {
|
|
1724
|
+
}
|
|
1725
|
+
blocked.push(legacyPath);
|
|
1726
|
+
}
|
|
1727
|
+
if (blocked.length) {
|
|
1728
|
+
const rmHint = blocked.map((p) => `rm -rf ${JSON.stringify(p)}`).join("; ");
|
|
1359
1729
|
throw new Error(
|
|
1360
|
-
`[lessonkit/lxpack] cannot promote: remove stale packaging artifacts from a previous failed run: ${
|
|
1730
|
+
`[lessonkit/lxpack] cannot promote: remove stale packaging artifacts from a previous failed run: ${blocked.join(", ")}. Try: ${rmHint}`
|
|
1361
1731
|
);
|
|
1362
1732
|
}
|
|
1363
1733
|
}
|
|
1364
|
-
async function
|
|
1734
|
+
async function listRelativePaths(root, dir = root) {
|
|
1735
|
+
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
1736
|
+
const paths = [];
|
|
1737
|
+
for (const entry of entries) {
|
|
1738
|
+
const full = (0, import_node_path8.join)(dir, entry.name);
|
|
1739
|
+
if (entry.isDirectory()) {
|
|
1740
|
+
paths.push(...await listRelativePaths(root, full));
|
|
1741
|
+
} else if (entry.isFile()) {
|
|
1742
|
+
paths.push(full.slice(root.length + 1));
|
|
1743
|
+
} else {
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return paths;
|
|
1747
|
+
}
|
|
1748
|
+
async function mergePreservedOutArtifacts(priorArtifactsDir, destArtifactsDir, newArtifactPaths) {
|
|
1749
|
+
if (!await pathExists(priorArtifactsDir)) return;
|
|
1750
|
+
for (const rel of await listRelativePaths(priorArtifactsDir)) {
|
|
1751
|
+
if (newArtifactPaths.has(rel)) continue;
|
|
1752
|
+
const src = (0, import_node_path8.join)(priorArtifactsDir, rel);
|
|
1753
|
+
const dest = (0, import_node_path8.join)(destArtifactsDir, rel);
|
|
1754
|
+
await fsp.mkdir((0, import_node_path8.dirname)(dest), { recursive: true });
|
|
1755
|
+
await fsp.cp(src, dest, { force: true });
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
async function promoteStagingToOutDir(stagingDir, outDir, options) {
|
|
1759
|
+
const outputBaseDir = options?.outputBaseDir ?? ".lxpack/out";
|
|
1760
|
+
if (options?.projectRoot) {
|
|
1761
|
+
assertRealPathUnderRoot((0, import_node_path8.resolve)(options.projectRoot), (0, import_node_path8.resolve)(outDir));
|
|
1762
|
+
}
|
|
1365
1763
|
return withPromoteLock(outDir, async () => {
|
|
1366
|
-
await
|
|
1764
|
+
await removeStaleLegacyPromoteArtifacts(outDir);
|
|
1765
|
+
const stagingArtifactsDir = (0, import_node_path8.join)(stagingDir, outputBaseDir);
|
|
1766
|
+
const newArtifactPaths = /* @__PURE__ */ new Set();
|
|
1767
|
+
if (await pathExists(stagingArtifactsDir)) {
|
|
1768
|
+
for (const rel of await listRelativePaths(stagingArtifactsDir)) {
|
|
1769
|
+
newArtifactPaths.add(rel);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1367
1772
|
const parent = (0, import_node_path8.dirname)(outDir);
|
|
1773
|
+
let priorArtifactsBackup;
|
|
1774
|
+
const existingArtifactsDir = (0, import_node_path8.join)(outDir, outputBaseDir);
|
|
1775
|
+
if (await pathExists(outDir) && await pathExists(existingArtifactsDir)) {
|
|
1776
|
+
priorArtifactsBackup = await fsp.mkdtemp((0, import_node_path8.join)(parent, ".lk-prior-out-"));
|
|
1777
|
+
await fsp.cp(existingArtifactsDir, (0, import_node_path8.join)(priorArtifactsBackup, outputBaseDir), { recursive: true });
|
|
1778
|
+
}
|
|
1368
1779
|
const tmpPromote = await fsp.mkdtemp((0, import_node_path8.join)(parent, ".lk-promote-"));
|
|
1369
1780
|
await renameOrCopy(stagingDir, tmpPromote);
|
|
1370
1781
|
const hadOutDir = await pathExists(outDir);
|
|
@@ -1421,6 +1832,20 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1421
1832
|
}
|
|
1422
1833
|
throw promoteError;
|
|
1423
1834
|
}
|
|
1835
|
+
if (priorArtifactsBackup) {
|
|
1836
|
+
try {
|
|
1837
|
+
await mergePreservedOutArtifacts(
|
|
1838
|
+
(0, import_node_path8.join)(priorArtifactsBackup, outputBaseDir),
|
|
1839
|
+
(0, import_node_path8.join)(outDir, outputBaseDir),
|
|
1840
|
+
newArtifactPaths
|
|
1841
|
+
);
|
|
1842
|
+
} finally {
|
|
1843
|
+
await fsp.rm(priorArtifactsBackup, { recursive: true, force: true }).catch(
|
|
1844
|
+
/* v8 ignore next */
|
|
1845
|
+
() => void 0
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1424
1849
|
if (backup) {
|
|
1425
1850
|
await fsp.rm(backup, { recursive: true, force: true }).catch(
|
|
1426
1851
|
/* v8 ignore next */
|
|
@@ -1438,6 +1863,7 @@ var import_api = require("@lxpack/api");
|
|
|
1438
1863
|
async function buildStagingPackage(options) {
|
|
1439
1864
|
const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
|
|
1440
1865
|
const stagingDir = await fsp2.mkdtemp((0, import_node_path9.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
|
|
1866
|
+
let succeeded = false;
|
|
1441
1867
|
try {
|
|
1442
1868
|
let spaDirs;
|
|
1443
1869
|
try {
|
|
@@ -1493,6 +1919,7 @@ async function buildStagingPackage(options) {
|
|
|
1493
1919
|
}))
|
|
1494
1920
|
};
|
|
1495
1921
|
}
|
|
1922
|
+
succeeded = true;
|
|
1496
1923
|
return {
|
|
1497
1924
|
ok: true,
|
|
1498
1925
|
stagingDir,
|
|
@@ -1506,6 +1933,13 @@ async function buildStagingPackage(options) {
|
|
|
1506
1933
|
() => void 0
|
|
1507
1934
|
);
|
|
1508
1935
|
throw err;
|
|
1936
|
+
} finally {
|
|
1937
|
+
if (!succeeded) {
|
|
1938
|
+
await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1939
|
+
/* v8 ignore next */
|
|
1940
|
+
() => void 0
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1509
1943
|
}
|
|
1510
1944
|
}
|
|
1511
1945
|
async function ensureOutDirParent(outDir) {
|
|
@@ -1520,6 +1954,12 @@ function isPackagingErrorIssue(issue) {
|
|
|
1520
1954
|
function findPackagingErrorIssues(issues) {
|
|
1521
1955
|
return (issues ?? []).filter(isPackagingErrorIssue);
|
|
1522
1956
|
}
|
|
1957
|
+
function isPackagingWarningIssue(issue) {
|
|
1958
|
+
return issue.severity?.toLowerCase() === "warning";
|
|
1959
|
+
}
|
|
1960
|
+
function findPackagingWarningIssues(issues) {
|
|
1961
|
+
return (issues ?? []).filter(isPackagingWarningIssue);
|
|
1962
|
+
}
|
|
1523
1963
|
|
|
1524
1964
|
// src/packageCourse.ts
|
|
1525
1965
|
async function validateLessonkitProject(options) {
|
|
@@ -1570,33 +2010,46 @@ async function packageLessonkitCourse(options) {
|
|
|
1570
2010
|
};
|
|
1571
2011
|
}
|
|
1572
2012
|
const descriptor = descriptorValidation.descriptor;
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
2013
|
+
const parityIssues = validateReactManifestParity({
|
|
2014
|
+
projectRoot: writeOpts.projectRoot,
|
|
2015
|
+
descriptor
|
|
2016
|
+
});
|
|
2017
|
+
const parityFailures = writeOpts.strictParity ? parityIssues : parityIssues.filter((i) => i.severity === "error");
|
|
2018
|
+
if (parityFailures.length > 0) {
|
|
2019
|
+
return {
|
|
2020
|
+
ok: false,
|
|
2021
|
+
courseDir: outDir,
|
|
2022
|
+
target,
|
|
2023
|
+
issues: parityFailures.map((i) => ({
|
|
2024
|
+
path: i.path,
|
|
2025
|
+
message: i.message,
|
|
2026
|
+
severity: i.severity
|
|
2027
|
+
}))
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
let staged;
|
|
2031
|
+
try {
|
|
2032
|
+
staged = await buildStagingPackage({
|
|
2033
|
+
...writeOpts,
|
|
2034
|
+
descriptor,
|
|
2035
|
+
target,
|
|
2036
|
+
output,
|
|
2037
|
+
dir,
|
|
2038
|
+
outputBaseDir
|
|
1577
2039
|
});
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
path:
|
|
1586
|
-
message:
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
}
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
return {
|
|
2042
|
+
ok: false,
|
|
2043
|
+
courseDir: outDir,
|
|
2044
|
+
target,
|
|
2045
|
+
issues: [
|
|
2046
|
+
{
|
|
2047
|
+
path: "staging",
|
|
2048
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2049
|
+
}
|
|
2050
|
+
]
|
|
2051
|
+
};
|
|
1591
2052
|
}
|
|
1592
|
-
const staged = await buildStagingPackage({
|
|
1593
|
-
...writeOpts,
|
|
1594
|
-
descriptor,
|
|
1595
|
-
target,
|
|
1596
|
-
output,
|
|
1597
|
-
dir,
|
|
1598
|
-
outputBaseDir
|
|
1599
|
-
});
|
|
1600
2053
|
if (!staged.ok) {
|
|
1601
2054
|
await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(
|
|
1602
2055
|
/* v8 ignore next */
|
|
@@ -1646,11 +2099,30 @@ async function packageLessonkitCourse(options) {
|
|
|
1646
2099
|
ok: false,
|
|
1647
2100
|
courseDir: outDir,
|
|
1648
2101
|
target,
|
|
1649
|
-
validation: { ok:
|
|
2102
|
+
validation: { ok: false, manifest: build.manifest, issues: build.issues },
|
|
1650
2103
|
build,
|
|
1651
2104
|
issues: artifactIssues
|
|
1652
2105
|
};
|
|
1653
2106
|
}
|
|
2107
|
+
const buildWarningIssues = findPackagingWarningIssues(build.issues);
|
|
2108
|
+
if (options.strictBuild && buildWarningIssues.length > 0) {
|
|
2109
|
+
await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
2110
|
+
/* v8 ignore next */
|
|
2111
|
+
() => void 0
|
|
2112
|
+
);
|
|
2113
|
+
return {
|
|
2114
|
+
ok: false,
|
|
2115
|
+
courseDir: outDir,
|
|
2116
|
+
target,
|
|
2117
|
+
validation: { ok: false, manifest: build.manifest, issues: build.issues },
|
|
2118
|
+
build,
|
|
2119
|
+
issues: buildWarningIssues.map((i) => ({
|
|
2120
|
+
path: i.path ?? "build",
|
|
2121
|
+
message: i.message ?? "build warning",
|
|
2122
|
+
severity: i.severity
|
|
2123
|
+
}))
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
1654
2126
|
const remappedOutputPath = remapArtifactPaths(stagingRoot, outDir, staged.outputPath);
|
|
1655
2127
|
const remappedOutputDir = remapArtifactPaths(stagingRoot, outDir, staged.outputDir);
|
|
1656
2128
|
const validation = {
|
|
@@ -1660,7 +2132,10 @@ async function packageLessonkitCourse(options) {
|
|
|
1660
2132
|
};
|
|
1661
2133
|
try {
|
|
1662
2134
|
await ensureOutDirParent(outDir);
|
|
1663
|
-
await promoteStagingToOutDir(stagingDir, outDir
|
|
2135
|
+
await promoteStagingToOutDir(stagingDir, outDir, {
|
|
2136
|
+
outputBaseDir: outputBaseDir ?? ".lxpack/out",
|
|
2137
|
+
projectRoot: writeOpts.projectRoot
|
|
2138
|
+
});
|
|
1664
2139
|
} catch (err) {
|
|
1665
2140
|
await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1666
2141
|
/* v8 ignore next */
|
|
@@ -1783,6 +2258,20 @@ function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
|
|
|
1783
2258
|
message: `"course.spaDistDir" (${courseSpaDistDir}) differs from "paths.spaDistDir" (${paths.spaDistDir}). Use paths.spaDistDir for CLI build and package.`
|
|
1784
2259
|
});
|
|
1785
2260
|
}
|
|
2261
|
+
for (const key of ["spaDistDir", "lxpackOutDir", "outputBaseDir"]) {
|
|
2262
|
+
const value = paths[key];
|
|
2263
|
+
if (!isSafeRelativeSpaPath(value)) {
|
|
2264
|
+
issues.push({
|
|
2265
|
+
path: `paths.${key}`,
|
|
2266
|
+
message: "path must be relative without '..' segments or absolute prefixes"
|
|
2267
|
+
});
|
|
2268
|
+
} else if (isReservedOutputPath(value)) {
|
|
2269
|
+
issues.push({
|
|
2270
|
+
path: `paths.${key}`,
|
|
2271
|
+
message: "path must not target reserved directories (.git, node_modules, .github)"
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
1786
2275
|
if (projectRoot) {
|
|
1787
2276
|
const pathIssues = validateProjectPaths(projectRoot, paths);
|
|
1788
2277
|
for (const pi of pathIssues) {
|
|
@@ -1814,53 +2303,879 @@ var import_tracking_schema2 = require("@lxpack/tracking-schema");
|
|
|
1814
2303
|
|
|
1815
2304
|
// src/telemetry.ts
|
|
1816
2305
|
var import_tracking_schema = require("@lxpack/tracking-schema");
|
|
1817
|
-
var
|
|
2306
|
+
var BRANCH_TELEMETRY_EVENTS = ["branch_node_viewed", "branch_selected"];
|
|
2307
|
+
var ASSESSMENT_TELEMETRY_EVENTS = ["assessment_answered"];
|
|
2308
|
+
var SUPPORTED = /* @__PURE__ */ new Set([
|
|
2309
|
+
...import_tracking_schema.LESSONKIT_TELEMETRY_EVENTS,
|
|
2310
|
+
...BRANCH_TELEMETRY_EVENTS,
|
|
2311
|
+
...ASSESSMENT_TELEMETRY_EVENTS
|
|
2312
|
+
]);
|
|
1818
2313
|
function isQuizAnsweredData(data) {
|
|
1819
|
-
return typeof data === "object" && data !== null && typeof data.checkId === "string";
|
|
2314
|
+
return typeof data === "object" && data !== null && typeof data.checkId === "string" && data.checkId.length > 0;
|
|
1820
2315
|
}
|
|
1821
2316
|
function isQuizCompletedData(data) {
|
|
1822
|
-
return typeof data === "object" && data !== null && typeof data.checkId === "string";
|
|
2317
|
+
return typeof data === "object" && data !== null && typeof data.checkId === "string" && data.checkId.length > 0;
|
|
2318
|
+
}
|
|
2319
|
+
function isAssessmentAnsweredData(data) {
|
|
2320
|
+
return typeof data === "object" && data !== null && typeof data.checkId === "string" && data.checkId.length > 0;
|
|
1823
2321
|
}
|
|
1824
2322
|
function isInteractionData(data) {
|
|
1825
2323
|
return typeof data === "object" && data !== null;
|
|
1826
2324
|
}
|
|
2325
|
+
function isBranchNodeViewedData(data) {
|
|
2326
|
+
return typeof data === "object" && data !== null && typeof data.blockId === "string" && typeof data.nodeId === "string";
|
|
2327
|
+
}
|
|
2328
|
+
function isBranchSelectedData(data) {
|
|
2329
|
+
return typeof data === "object" && data !== null && typeof data.blockId === "string" && typeof data.fromNodeId === "string" && typeof data.toNodeId === "string";
|
|
2330
|
+
}
|
|
1827
2331
|
function telemetryEventToLessonkit(event) {
|
|
1828
2332
|
if (!SUPPORTED.has(event.name)) {
|
|
1829
2333
|
return null;
|
|
1830
2334
|
}
|
|
1831
|
-
const name = event.name;
|
|
1832
2335
|
const mapped = {
|
|
1833
|
-
name,
|
|
2336
|
+
name: event.name,
|
|
1834
2337
|
lessonId: event.lessonId
|
|
1835
2338
|
};
|
|
1836
|
-
if (name === "quiz_completed" || name === "quiz_answered") {
|
|
2339
|
+
if (event.name === "quiz_completed" || event.name === "quiz_answered" || event.name === "assessment_answered") {
|
|
1837
2340
|
const data = event.data;
|
|
1838
|
-
if (isQuizAnsweredData(data)
|
|
1839
|
-
|
|
1840
|
-
if ("score" in data) {
|
|
1841
|
-
mapped.score = data.score;
|
|
1842
|
-
mapped.maxScore = data.maxScore;
|
|
1843
|
-
mapped.passingScore = data.passingScore;
|
|
1844
|
-
}
|
|
1845
|
-
mapped.data = data;
|
|
2341
|
+
if (!isQuizAnsweredData(data) && !isQuizCompletedData(data) && !isAssessmentAnsweredData(data)) {
|
|
2342
|
+
return null;
|
|
1846
2343
|
}
|
|
1847
|
-
|
|
2344
|
+
mapped.assessmentId = data.checkId;
|
|
2345
|
+
if ("score" in data) {
|
|
2346
|
+
mapped.score = data.score;
|
|
2347
|
+
mapped.maxScore = data.maxScore;
|
|
2348
|
+
mapped.passingScore = data.passingScore;
|
|
2349
|
+
}
|
|
2350
|
+
mapped.data = data;
|
|
2351
|
+
} else if (mapped.name === "interaction" && event.data && isInteractionData(event.data)) {
|
|
2352
|
+
mapped.data = event.data;
|
|
2353
|
+
} else if (event.name === "branch_node_viewed" && isBranchNodeViewedData(event.data)) {
|
|
2354
|
+
mapped.data = event.data;
|
|
2355
|
+
} else if (event.name === "branch_selected" && isBranchSelectedData(event.data)) {
|
|
1848
2356
|
mapped.data = event.data;
|
|
1849
2357
|
}
|
|
1850
2358
|
return mapped;
|
|
1851
2359
|
}
|
|
1852
2360
|
|
|
1853
2361
|
// src/index.ts
|
|
2362
|
+
var import_validators4 = require("@lxpack/validators");
|
|
2363
|
+
|
|
2364
|
+
// src/lkcourse/zip.ts
|
|
2365
|
+
var import_node_fs5 = require("fs");
|
|
2366
|
+
var import_node_path11 = require("path");
|
|
2367
|
+
var import_fflate = require("fflate");
|
|
2368
|
+
var MAX_LKCOURSE_UNCOMPRESSED_BYTES = 256 * 1024 * 1024;
|
|
2369
|
+
function canonicalZipEntryPath(entryPath) {
|
|
2370
|
+
const slashNormalized = entryPath.replace(/\\/g, "/");
|
|
2371
|
+
const canonical = (0, import_node_path11.normalize)(slashNormalized).replace(/\\/g, "/");
|
|
2372
|
+
if (canonical !== slashNormalized) return null;
|
|
2373
|
+
return canonical;
|
|
2374
|
+
}
|
|
2375
|
+
function isSafeZipEntryPath(entryPath) {
|
|
2376
|
+
const canonical = canonicalZipEntryPath(entryPath);
|
|
2377
|
+
if (!canonical?.length || canonical.startsWith("/") || canonical.includes("\0")) {
|
|
2378
|
+
return false;
|
|
2379
|
+
}
|
|
2380
|
+
const segments = canonical.split("/").filter((s) => s.length > 0);
|
|
2381
|
+
if (segments.some((s) => s === "..")) return false;
|
|
2382
|
+
return segments.length > 0;
|
|
2383
|
+
}
|
|
2384
|
+
function createZip(entries) {
|
|
2385
|
+
const zipped = {};
|
|
2386
|
+
for (const [path, data] of entries) {
|
|
2387
|
+
if (!isSafeZipEntryPath(path)) {
|
|
2388
|
+
throw new Error(`unsafe zip entry path: ${path}`);
|
|
2389
|
+
}
|
|
2390
|
+
zipped[path.replace(/\\/g, "/")] = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
2391
|
+
}
|
|
2392
|
+
return (0, import_fflate.zipSync)(zipped, { level: 6 });
|
|
2393
|
+
}
|
|
2394
|
+
function readZip(archivePath) {
|
|
2395
|
+
const issues = [];
|
|
2396
|
+
let raw;
|
|
2397
|
+
try {
|
|
2398
|
+
raw = (0, import_node_fs5.readFileSync)(archivePath);
|
|
2399
|
+
} catch {
|
|
2400
|
+
return { ok: false, issues: [{ path: archivePath, message: "failed to read archive" }] };
|
|
2401
|
+
}
|
|
2402
|
+
if (!raw.length) {
|
|
2403
|
+
return { ok: false, issues: [{ path: archivePath, message: "archive is empty" }] };
|
|
2404
|
+
}
|
|
2405
|
+
let unzipped;
|
|
2406
|
+
try {
|
|
2407
|
+
unzipped = (0, import_fflate.unzipSync)(raw);
|
|
2408
|
+
} catch {
|
|
2409
|
+
return { ok: false, issues: [{ path: archivePath, message: "invalid zip archive" }] };
|
|
2410
|
+
}
|
|
2411
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2412
|
+
let totalUncompressed = 0;
|
|
2413
|
+
for (const [path, data] of Object.entries(unzipped)) {
|
|
2414
|
+
const canonical = canonicalZipEntryPath(path);
|
|
2415
|
+
if (!canonical || !isSafeZipEntryPath(canonical)) {
|
|
2416
|
+
issues.push({ path, message: "unsafe zip entry path" });
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
if (entries.has(canonical)) {
|
|
2420
|
+
issues.push({ path: canonical, message: "duplicate zip entry path" });
|
|
2421
|
+
continue;
|
|
2422
|
+
}
|
|
2423
|
+
totalUncompressed += data.byteLength;
|
|
2424
|
+
if (totalUncompressed > MAX_LKCOURSE_UNCOMPRESSED_BYTES) {
|
|
2425
|
+
return {
|
|
2426
|
+
ok: false,
|
|
2427
|
+
issues: [
|
|
2428
|
+
{
|
|
2429
|
+
path: archivePath,
|
|
2430
|
+
message: `archive exceeds max uncompressed size (${MAX_LKCOURSE_UNCOMPRESSED_BYTES} bytes)`
|
|
2431
|
+
}
|
|
2432
|
+
]
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
entries.set(canonical, data);
|
|
2436
|
+
}
|
|
2437
|
+
if (issues.length) return { ok: false, issues };
|
|
2438
|
+
return { ok: true, entries };
|
|
2439
|
+
}
|
|
2440
|
+
async function collectDistEntries(distDir, spaDistRelative) {
|
|
2441
|
+
const { lstat: lstat2, readdir: readdir4, readFile: readFile2 } = await import("fs/promises");
|
|
2442
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2443
|
+
const walk = async (absDir, relPrefix) => {
|
|
2444
|
+
const dirEntries = await readdir4(absDir, { withFileTypes: true });
|
|
2445
|
+
for (const entry of dirEntries) {
|
|
2446
|
+
const abs = (0, import_node_path11.join)(absDir, entry.name);
|
|
2447
|
+
const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
|
|
2448
|
+
const zipPath = `${spaDistRelative}/${rel}`.replace(/\\/g, "/");
|
|
2449
|
+
if (!isSafeRelativeSpaPath(zipPath)) {
|
|
2450
|
+
throw new Error(`unsafe dist path: ${zipPath}`);
|
|
2451
|
+
}
|
|
2452
|
+
const stat2 = await lstat2(abs);
|
|
2453
|
+
if (stat2.isSymbolicLink()) {
|
|
2454
|
+
throw new Error(`dist contains symlink: ${abs}`);
|
|
2455
|
+
}
|
|
2456
|
+
if (stat2.isDirectory()) {
|
|
2457
|
+
await walk(abs, rel);
|
|
2458
|
+
} else if (stat2.isFile()) {
|
|
2459
|
+
entries.set(zipPath.replace(/\\/g, "/"), await readFile2(abs));
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
2463
|
+
await walk(distDir, "");
|
|
2464
|
+
return entries;
|
|
2465
|
+
}
|
|
2466
|
+
function entryToUtf8(data) {
|
|
2467
|
+
return (0, import_fflate.strFromU8)(data);
|
|
2468
|
+
}
|
|
2469
|
+
function utf8ToEntry(text) {
|
|
2470
|
+
return (0, import_fflate.strToU8)(text);
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
// src/lkcourse/parseEnvelope.ts
|
|
2474
|
+
function parseLkcourseEnvelope(raw, label = "manifest.json") {
|
|
2475
|
+
const issues = [];
|
|
2476
|
+
if (!raw || typeof raw !== "object") {
|
|
2477
|
+
return { ok: false, issues: [{ path: label, message: "must be a JSON object" }] };
|
|
2478
|
+
}
|
|
2479
|
+
const obj = raw;
|
|
2480
|
+
if (obj.format !== "lkcourse") {
|
|
2481
|
+
issues.push({
|
|
2482
|
+
path: "format",
|
|
2483
|
+
message: `must be "lkcourse" (got ${String(obj.format)})`
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
let schemaVersion = obj.schemaVersion;
|
|
2487
|
+
if (schemaVersion === "1") schemaVersion = 1;
|
|
2488
|
+
if (schemaVersion !== 1) {
|
|
2489
|
+
issues.push({
|
|
2490
|
+
path: "schemaVersion",
|
|
2491
|
+
message: `must be 1 (got ${String(obj.schemaVersion)})`
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
const lessonkitVersion = typeof obj.lessonkitVersion === "string" ? obj.lessonkitVersion.trim() : "";
|
|
2495
|
+
if (!lessonkitVersion) {
|
|
2496
|
+
issues.push({ path: "lessonkitVersion", message: "must be a non-empty string" });
|
|
2497
|
+
}
|
|
2498
|
+
const exportedAt = typeof obj.exportedAt === "string" ? obj.exportedAt.trim() : "";
|
|
2499
|
+
if (!exportedAt) {
|
|
2500
|
+
issues.push({ path: "exportedAt", message: "must be a non-empty string" });
|
|
2501
|
+
}
|
|
2502
|
+
const entriesRaw = obj.entries;
|
|
2503
|
+
const entries = [];
|
|
2504
|
+
if (!Array.isArray(entriesRaw) || entriesRaw.length === 0) {
|
|
2505
|
+
issues.push({ path: "entries", message: "must be a non-empty array of relative paths" });
|
|
2506
|
+
} else {
|
|
2507
|
+
for (let i = 0; i < entriesRaw.length; i++) {
|
|
2508
|
+
const entry = entriesRaw[i];
|
|
2509
|
+
if (typeof entry !== "string" || !entry.trim()) {
|
|
2510
|
+
issues.push({ path: `entries[${i}]`, message: "must be a non-empty string" });
|
|
2511
|
+
} else {
|
|
2512
|
+
const trimmed = entry.trim();
|
|
2513
|
+
if (!isSafeZipEntryPath(trimmed)) {
|
|
2514
|
+
issues.push({ path: `entries[${i}]`, message: "must be a safe relative path" });
|
|
2515
|
+
} else {
|
|
2516
|
+
entries.push(trimmed);
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
if (issues.length) return { ok: false, issues };
|
|
2522
|
+
const manifestParsed = parseLessonkitManifest(obj.sourceManifest, `${label}.sourceManifest`);
|
|
2523
|
+
if (!manifestParsed.ok) {
|
|
2524
|
+
return {
|
|
2525
|
+
ok: false,
|
|
2526
|
+
issues: manifestParsed.issues.map((issue) => ({
|
|
2527
|
+
path: `sourceManifest.${issue.path}`,
|
|
2528
|
+
message: issue.message
|
|
2529
|
+
}))
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
return {
|
|
2533
|
+
ok: true,
|
|
2534
|
+
envelope: {
|
|
2535
|
+
format: "lkcourse",
|
|
2536
|
+
schemaVersion: 1,
|
|
2537
|
+
lessonkitVersion,
|
|
2538
|
+
exportedAt,
|
|
2539
|
+
sourceManifest: manifestParsed.manifest,
|
|
2540
|
+
entries
|
|
2541
|
+
}
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
// src/lkcourse/blockTree.ts
|
|
2546
|
+
var import_node_fs6 = require("fs");
|
|
2547
|
+
var import_node_module = require("module");
|
|
2548
|
+
var import_node_path12 = require("path");
|
|
2549
|
+
var import_core5 = require("@lessonkit/core");
|
|
2550
|
+
var import_meta = {};
|
|
2551
|
+
var SCANNABLE_EXTENSIONS2 = [".tsx", ".ts", ".jsx", ".js"];
|
|
2552
|
+
var ID_PROPS = ["courseId", "lessonId", "checkId", "blockId", "nodeId"];
|
|
2553
|
+
function stripComments2(source) {
|
|
2554
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
|
|
2555
|
+
}
|
|
2556
|
+
function collectSourceUnderSrc2(projectRoot) {
|
|
2557
|
+
const srcDir = (0, import_node_path12.join)(projectRoot, "src");
|
|
2558
|
+
if (!(0, import_node_fs6.existsSync)(srcDir)) return [];
|
|
2559
|
+
const results = [];
|
|
2560
|
+
const walk = (dir) => {
|
|
2561
|
+
for (const entry of (0, import_node_fs6.readdirSync)(dir)) {
|
|
2562
|
+
const abs = (0, import_node_path12.join)(dir, entry);
|
|
2563
|
+
try {
|
|
2564
|
+
assertRealPathUnderRoot(projectRoot, abs);
|
|
2565
|
+
} catch {
|
|
2566
|
+
continue;
|
|
2567
|
+
}
|
|
2568
|
+
const stat2 = (0, import_node_fs6.lstatSync)(abs);
|
|
2569
|
+
if (stat2.isSymbolicLink()) continue;
|
|
2570
|
+
if (stat2.isDirectory()) {
|
|
2571
|
+
walk(abs);
|
|
2572
|
+
} else if (SCANNABLE_EXTENSIONS2.some((ext) => entry.endsWith(ext))) {
|
|
2573
|
+
results.push((0, import_node_path12.relative)(projectRoot, abs));
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
walk(srcDir);
|
|
2578
|
+
return results;
|
|
2579
|
+
}
|
|
2580
|
+
function loadCatalogBlockTypes(blockTypes) {
|
|
2581
|
+
if (blockTypes?.length) return blockTypes;
|
|
2582
|
+
try {
|
|
2583
|
+
const require2 = (0, import_node_module.createRequire)(import_meta.url);
|
|
2584
|
+
const catalogPath = require2.resolve("@lessonkit/react/block-catalog.v3.json");
|
|
2585
|
+
const catalog = JSON.parse((0, import_node_fs6.readFileSync)(catalogPath, "utf8"));
|
|
2586
|
+
return (catalog.entries ?? []).map((e) => e.type).filter((t) => typeof t === "string" && t.length > 0);
|
|
2587
|
+
} catch {
|
|
2588
|
+
return [
|
|
2589
|
+
"Course",
|
|
2590
|
+
"Lesson",
|
|
2591
|
+
"Scenario",
|
|
2592
|
+
"Quiz",
|
|
2593
|
+
"KnowledgeCheck",
|
|
2594
|
+
"ProgressTracker",
|
|
2595
|
+
"Reflection",
|
|
2596
|
+
"TrueFalse",
|
|
2597
|
+
"MarkTheWords",
|
|
2598
|
+
"FillInTheBlanks",
|
|
2599
|
+
"DragTheWords",
|
|
2600
|
+
"DragAndDrop",
|
|
2601
|
+
"AssessmentSequence",
|
|
2602
|
+
"Text",
|
|
2603
|
+
"Heading",
|
|
2604
|
+
"Image",
|
|
2605
|
+
"Video",
|
|
2606
|
+
"Page",
|
|
2607
|
+
"InteractiveBook",
|
|
2608
|
+
"Slide",
|
|
2609
|
+
"SlideDeck",
|
|
2610
|
+
"TimedCue",
|
|
2611
|
+
"InteractiveVideo",
|
|
2612
|
+
"Summary",
|
|
2613
|
+
"BranchingScenario",
|
|
2614
|
+
"BranchNode",
|
|
2615
|
+
"BranchChoice",
|
|
2616
|
+
"Embed",
|
|
2617
|
+
"Chart"
|
|
2618
|
+
];
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
function extractIdProp(tagSource, prop) {
|
|
2622
|
+
const re = new RegExp(
|
|
2623
|
+
`\\b${prop}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|\\{\\s*["'\`]([^"'\`]+)["'\`]\\s*\\})`
|
|
2624
|
+
);
|
|
2625
|
+
const match = tagSource.match(re);
|
|
2626
|
+
if (!match) return void 0;
|
|
2627
|
+
return match[1] ?? match[2] ?? match[3];
|
|
2628
|
+
}
|
|
2629
|
+
function parseJsxBlocks(source, blockTypes) {
|
|
2630
|
+
const stripped = stripComments2(source);
|
|
2631
|
+
const tagRe = /<([A-Z][A-Za-z0-9]*)\b([^>]*?)(\/?)>/g;
|
|
2632
|
+
const stack = [];
|
|
2633
|
+
const roots = [];
|
|
2634
|
+
for (const match of stripped.matchAll(tagRe)) {
|
|
2635
|
+
const rawTag = match[1];
|
|
2636
|
+
const attrs = match[2] ?? "";
|
|
2637
|
+
const selfClosing = match[3] === "/";
|
|
2638
|
+
if (rawTag === "Fragment" || rawTag.endsWith("Provider")) continue;
|
|
2639
|
+
const known = blockTypes.has(rawTag);
|
|
2640
|
+
const node = known ? { type: rawTag } : { type: "Unknown", rawTag };
|
|
2641
|
+
for (const prop of ID_PROPS) {
|
|
2642
|
+
const value = extractIdProp(attrs, prop);
|
|
2643
|
+
if (value) node[prop] = value;
|
|
2644
|
+
}
|
|
2645
|
+
if (selfClosing) {
|
|
2646
|
+
if (stack.length) {
|
|
2647
|
+
const parent = stack[stack.length - 1];
|
|
2648
|
+
parent.children = parent.children ?? [];
|
|
2649
|
+
parent.children.push(node);
|
|
2650
|
+
} else {
|
|
2651
|
+
roots.push(node);
|
|
2652
|
+
}
|
|
2653
|
+
continue;
|
|
2654
|
+
}
|
|
2655
|
+
const closeRe = new RegExp(`</${rawTag}>`);
|
|
2656
|
+
const closeMatch = closeRe.exec(stripped.slice((match.index ?? 0) + match[0].length));
|
|
2657
|
+
if (!closeMatch) {
|
|
2658
|
+
if (stack.length) {
|
|
2659
|
+
const parent = stack[stack.length - 1];
|
|
2660
|
+
parent.children = parent.children ?? [];
|
|
2661
|
+
parent.children.push(node);
|
|
2662
|
+
} else {
|
|
2663
|
+
roots.push(node);
|
|
2664
|
+
}
|
|
2665
|
+
continue;
|
|
2666
|
+
}
|
|
2667
|
+
stack.push(node);
|
|
2668
|
+
const nextClose = stripped.indexOf(`</${rawTag}>`, (match.index ?? 0) + match[0].length);
|
|
2669
|
+
const inner = stripped.slice((match.index ?? 0) + match[0].length, nextClose);
|
|
2670
|
+
if (!inner.includes("<")) {
|
|
2671
|
+
stack.pop();
|
|
2672
|
+
if (stack.length) {
|
|
2673
|
+
const parent = stack[stack.length - 1];
|
|
2674
|
+
parent.children = parent.children ?? [];
|
|
2675
|
+
parent.children.push(node);
|
|
2676
|
+
} else {
|
|
2677
|
+
roots.push(node);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
return roots.length ? roots : stack;
|
|
2682
|
+
}
|
|
2683
|
+
function validateNodeIds(node, pathPrefix, issues) {
|
|
2684
|
+
for (const prop of ID_PROPS) {
|
|
2685
|
+
const value = node[prop];
|
|
2686
|
+
if (value === void 0) continue;
|
|
2687
|
+
const validated = (0, import_core5.validateId)(value, prop);
|
|
2688
|
+
if (!validated.ok) {
|
|
2689
|
+
issues.push({
|
|
2690
|
+
path: `${pathPrefix}.${prop}`,
|
|
2691
|
+
message: validated.issues[0]?.message ?? `invalid ${prop}`
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
node.children?.forEach((child, index) => {
|
|
2696
|
+
validateNodeIds(child, `${pathPrefix}.children[${index}]`, issues);
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
function validateBlockTreeIds(tree) {
|
|
2700
|
+
const issues = [];
|
|
2701
|
+
tree.blocks.forEach((block, index) => {
|
|
2702
|
+
validateNodeIds(block, `blocks[${index}]`, issues);
|
|
2703
|
+
});
|
|
2704
|
+
return issues;
|
|
2705
|
+
}
|
|
2706
|
+
function extractBlockTree(options) {
|
|
2707
|
+
const blockTypes = new Set(loadCatalogBlockTypes(options.blockTypes));
|
|
2708
|
+
const sources = options.appSources ?? collectSourceUnderSrc2(options.projectRoot);
|
|
2709
|
+
const blocks = [];
|
|
2710
|
+
for (const rel of sources) {
|
|
2711
|
+
const abs = (0, import_node_path12.join)(options.projectRoot, rel);
|
|
2712
|
+
if (!(0, import_node_fs6.existsSync)(abs)) continue;
|
|
2713
|
+
const source = (0, import_node_fs6.readFileSync)(abs, "utf8");
|
|
2714
|
+
const parsed = parseJsxBlocks(source, blockTypes);
|
|
2715
|
+
blocks.push(...parsed);
|
|
2716
|
+
}
|
|
2717
|
+
return {
|
|
2718
|
+
schemaVersion: 1,
|
|
2719
|
+
sources,
|
|
2720
|
+
blocks
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
// src/lkcourse/export.ts
|
|
2725
|
+
var import_promises3 = require("fs/promises");
|
|
2726
|
+
var import_node_module2 = require("module");
|
|
2727
|
+
var import_node_path13 = require("path");
|
|
1854
2728
|
var import_validators2 = require("@lxpack/validators");
|
|
2729
|
+
var import_meta2 = {};
|
|
2730
|
+
function resolveLessonkitVersion(explicit) {
|
|
2731
|
+
if (explicit?.trim()) return explicit.trim();
|
|
2732
|
+
try {
|
|
2733
|
+
const require2 = (0, import_node_module2.createRequire)(import_meta2.url);
|
|
2734
|
+
const pkg = require2("../../package.json");
|
|
2735
|
+
return pkg.version ?? "0.0.0";
|
|
2736
|
+
} catch {
|
|
2737
|
+
return "0.0.0";
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
async function exportLkcourse(options) {
|
|
2741
|
+
const projectRoot = (0, import_node_path13.resolve)(options.projectRoot);
|
|
2742
|
+
const manifest = options.manifest;
|
|
2743
|
+
const spaDistDir = (0, import_node_path13.join)(projectRoot, manifest.paths.spaDistDir);
|
|
2744
|
+
try {
|
|
2745
|
+
assertRealPathUnderRoot(projectRoot, spaDistDir);
|
|
2746
|
+
await assertSpaDistContentsSafe({ main: spaDistDir }, projectRoot);
|
|
2747
|
+
} catch (err) {
|
|
2748
|
+
return {
|
|
2749
|
+
ok: false,
|
|
2750
|
+
issues: [
|
|
2751
|
+
{
|
|
2752
|
+
path: manifest.paths.spaDistDir,
|
|
2753
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2754
|
+
}
|
|
2755
|
+
]
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
const interchange = descriptorToInterchange(manifest.course);
|
|
2759
|
+
const interchangeParsed = (0, import_validators2.parseLessonkitInterchange)(interchange);
|
|
2760
|
+
if (!interchangeParsed.ok) {
|
|
2761
|
+
return {
|
|
2762
|
+
ok: false,
|
|
2763
|
+
issues: interchangeParsed.issues.map((i) => ({
|
|
2764
|
+
path: `interchange.${i.path ?? ""}`.replace(/\.$/, ""),
|
|
2765
|
+
message: i.message
|
|
2766
|
+
}))
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
const validatedInterchange = interchangeParsed.data;
|
|
2770
|
+
const interchangeCourseId = validatedInterchange.course?.id;
|
|
2771
|
+
if (!interchangeCourseId) {
|
|
2772
|
+
return {
|
|
2773
|
+
ok: false,
|
|
2774
|
+
issues: [{ path: "interchange.course.id", message: "missing course id in interchange" }]
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
if (manifest.course.courseId !== interchangeCourseId) {
|
|
2778
|
+
return {
|
|
2779
|
+
ok: false,
|
|
2780
|
+
issues: [
|
|
2781
|
+
{
|
|
2782
|
+
path: "course.courseId",
|
|
2783
|
+
message: `descriptor courseId "${manifest.course.courseId}" does not match interchange course.id "${interchangeCourseId}"`
|
|
2784
|
+
}
|
|
2785
|
+
]
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2788
|
+
const zipEntries = /* @__PURE__ */ new Map();
|
|
2789
|
+
const interchangeJson = JSON.stringify(interchange, null, 2);
|
|
2790
|
+
zipEntries.set("interchange.json", utf8ToEntry(interchangeJson));
|
|
2791
|
+
let blockTreeJson;
|
|
2792
|
+
if (options.includeBlockTree) {
|
|
2793
|
+
const blockTree = extractBlockTree({ projectRoot });
|
|
2794
|
+
const blockTreeIssues = validateBlockTreeIds(blockTree);
|
|
2795
|
+
if (blockTreeIssues.length) {
|
|
2796
|
+
return {
|
|
2797
|
+
ok: false,
|
|
2798
|
+
issues: blockTreeIssues.map((issue) => ({
|
|
2799
|
+
path: `block-tree.${issue.path}`,
|
|
2800
|
+
message: issue.message
|
|
2801
|
+
}))
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
blockTreeJson = JSON.stringify(blockTree, null, 2);
|
|
2805
|
+
zipEntries.set("block-tree.json", utf8ToEntry(blockTreeJson));
|
|
2806
|
+
}
|
|
2807
|
+
let distEntries;
|
|
2808
|
+
try {
|
|
2809
|
+
distEntries = await collectDistEntries(spaDistDir, manifest.paths.spaDistDir);
|
|
2810
|
+
} catch (err) {
|
|
2811
|
+
return {
|
|
2812
|
+
ok: false,
|
|
2813
|
+
issues: [
|
|
2814
|
+
{
|
|
2815
|
+
path: manifest.paths.spaDistDir,
|
|
2816
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2817
|
+
}
|
|
2818
|
+
]
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
if (!distEntries.has(`${manifest.paths.spaDistDir}/index.html`.replace(/\\/g, "/"))) {
|
|
2822
|
+
return {
|
|
2823
|
+
ok: false,
|
|
2824
|
+
issues: [
|
|
2825
|
+
{
|
|
2826
|
+
path: `${manifest.paths.spaDistDir}/index.html`,
|
|
2827
|
+
message: "dist must contain index.html before export"
|
|
2828
|
+
}
|
|
2829
|
+
]
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
for (const [path, data] of distEntries) {
|
|
2833
|
+
zipEntries.set(path, data);
|
|
2834
|
+
}
|
|
2835
|
+
const entryPaths = [...zipEntries.keys()].sort();
|
|
2836
|
+
const envelope = {
|
|
2837
|
+
format: "lkcourse",
|
|
2838
|
+
schemaVersion: 1,
|
|
2839
|
+
lessonkitVersion: resolveLessonkitVersion(options.lessonkitVersion),
|
|
2840
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2841
|
+
sourceManifest: manifest,
|
|
2842
|
+
entries: entryPaths
|
|
2843
|
+
};
|
|
2844
|
+
const envelopeCheck = parseLkcourseEnvelope(envelope);
|
|
2845
|
+
if (!envelopeCheck.ok) {
|
|
2846
|
+
return { ok: false, issues: envelopeCheck.issues };
|
|
2847
|
+
}
|
|
2848
|
+
zipEntries.set("manifest.json", utf8ToEntry(JSON.stringify(envelope, null, 2)));
|
|
2849
|
+
const archivePath = (0, import_node_path13.resolve)(
|
|
2850
|
+
projectRoot,
|
|
2851
|
+
options.outPath ?? `${manifest.name}.lkcourse`
|
|
2852
|
+
);
|
|
2853
|
+
try {
|
|
2854
|
+
assertRealPathUnderRoot(projectRoot, archivePath);
|
|
2855
|
+
} catch (err) {
|
|
2856
|
+
return {
|
|
2857
|
+
ok: false,
|
|
2858
|
+
issues: [
|
|
2859
|
+
{
|
|
2860
|
+
path: options.outPath ?? `${manifest.name}.lkcourse`,
|
|
2861
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2862
|
+
}
|
|
2863
|
+
]
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2866
|
+
if (!isSafeZipEntryPath(options.outPath ?? `${manifest.name}.lkcourse`)) {
|
|
2867
|
+
return {
|
|
2868
|
+
ok: false,
|
|
2869
|
+
issues: [{ path: "outPath", message: "output path must be a safe relative path" }]
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2872
|
+
try {
|
|
2873
|
+
await (0, import_promises3.mkdir)((0, import_node_path13.dirname)(archivePath), { recursive: true });
|
|
2874
|
+
const zipped = createZip(zipEntries);
|
|
2875
|
+
await (0, import_promises3.writeFile)(archivePath, zipped);
|
|
2876
|
+
} catch (err) {
|
|
2877
|
+
return {
|
|
2878
|
+
ok: false,
|
|
2879
|
+
issues: [
|
|
2880
|
+
{
|
|
2881
|
+
path: archivePath,
|
|
2882
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2883
|
+
}
|
|
2884
|
+
]
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
return {
|
|
2888
|
+
ok: true,
|
|
2889
|
+
archivePath,
|
|
2890
|
+
fileCount: zipEntries.size,
|
|
2891
|
+
includeBlockTree: Boolean(options.includeBlockTree)
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
// src/lkcourse/validate.ts
|
|
2896
|
+
var import_validators3 = require("@lxpack/validators");
|
|
2897
|
+
function validateLkcourseArchiveEntries(entries, _archiveLabel) {
|
|
2898
|
+
const issues = [];
|
|
2899
|
+
const manifestData = entries.get("manifest.json");
|
|
2900
|
+
if (!manifestData) {
|
|
2901
|
+
return {
|
|
2902
|
+
ok: false,
|
|
2903
|
+
issues: [{ path: "manifest.json", message: "required file missing from archive" }]
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
let envelopeRaw;
|
|
2907
|
+
try {
|
|
2908
|
+
envelopeRaw = JSON.parse(entryToUtf8(manifestData));
|
|
2909
|
+
} catch {
|
|
2910
|
+
return {
|
|
2911
|
+
ok: false,
|
|
2912
|
+
issues: [{ path: "manifest.json", message: "invalid JSON" }]
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
const envelopeParsed = parseLkcourseEnvelope(envelopeRaw, "manifest.json");
|
|
2916
|
+
if (!envelopeParsed.ok) {
|
|
2917
|
+
return { ok: false, issues: envelopeParsed.issues };
|
|
2918
|
+
}
|
|
2919
|
+
const envelope = envelopeParsed.envelope;
|
|
2920
|
+
const interchangeData = entries.get("interchange.json");
|
|
2921
|
+
if (!interchangeData) {
|
|
2922
|
+
issues.push({ path: "interchange.json", message: "required file missing from archive" });
|
|
2923
|
+
}
|
|
2924
|
+
const spaDistDir = envelope.sourceManifest.paths.spaDistDir.replace(/\\/g, "/");
|
|
2925
|
+
const spaIndexPath = `${spaDistDir}/index.html`;
|
|
2926
|
+
if (!entries.has(spaIndexPath)) {
|
|
2927
|
+
issues.push({ path: spaIndexPath, message: "required file missing from archive" });
|
|
2928
|
+
}
|
|
2929
|
+
for (const entryPath of envelope.entries) {
|
|
2930
|
+
if (!entries.has(entryPath)) {
|
|
2931
|
+
issues.push({
|
|
2932
|
+
path: entryPath,
|
|
2933
|
+
message: "listed in manifest.entries but missing from archive"
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
if (issues.length) return { ok: false, issues };
|
|
2938
|
+
let interchangeRaw;
|
|
2939
|
+
try {
|
|
2940
|
+
interchangeRaw = JSON.parse(entryToUtf8(interchangeData));
|
|
2941
|
+
} catch {
|
|
2942
|
+
return {
|
|
2943
|
+
ok: false,
|
|
2944
|
+
issues: [{ path: "interchange.json", message: "invalid JSON" }]
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
const interchangeParsed = (0, import_validators3.parseLessonkitInterchange)(interchangeRaw);
|
|
2948
|
+
if (!interchangeParsed.ok) {
|
|
2949
|
+
return {
|
|
2950
|
+
ok: false,
|
|
2951
|
+
issues: interchangeParsed.issues.map((i) => ({
|
|
2952
|
+
path: `interchange.${i.path ?? ""}`.replace(/\.$/, ""),
|
|
2953
|
+
message: i.message
|
|
2954
|
+
}))
|
|
2955
|
+
};
|
|
2956
|
+
}
|
|
2957
|
+
const interchange = interchangeParsed.data;
|
|
2958
|
+
const interchangeCourseId = interchange.course?.id;
|
|
2959
|
+
if (!interchangeCourseId) {
|
|
2960
|
+
issues.push({
|
|
2961
|
+
path: "interchange.course.id",
|
|
2962
|
+
message: "missing course id in interchange"
|
|
2963
|
+
});
|
|
2964
|
+
} else if (envelope.sourceManifest.course.courseId !== interchangeCourseId) {
|
|
2965
|
+
issues.push({
|
|
2966
|
+
path: "sourceManifest.course.courseId",
|
|
2967
|
+
message: `does not match interchange.course.id (${interchangeCourseId})`
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2970
|
+
if (issues.length) return { ok: false, issues };
|
|
2971
|
+
const blockTreeData = entries.get("block-tree.json");
|
|
2972
|
+
if (blockTreeData) {
|
|
2973
|
+
let blockTreeRaw;
|
|
2974
|
+
try {
|
|
2975
|
+
blockTreeRaw = JSON.parse(entryToUtf8(blockTreeData));
|
|
2976
|
+
} catch {
|
|
2977
|
+
return {
|
|
2978
|
+
ok: false,
|
|
2979
|
+
issues: [{ path: "block-tree.json", message: "invalid JSON" }]
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
const blockTree = blockTreeRaw;
|
|
2983
|
+
if (Array.isArray(blockTree?.blocks)) {
|
|
2984
|
+
const blockTreeIssues = validateBlockTreeIds(blockTree);
|
|
2985
|
+
if (blockTreeIssues.length) {
|
|
2986
|
+
return {
|
|
2987
|
+
ok: false,
|
|
2988
|
+
issues: blockTreeIssues.map((issue) => ({
|
|
2989
|
+
path: `block-tree.${issue.path}`,
|
|
2990
|
+
message: issue.message
|
|
2991
|
+
}))
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
return {
|
|
2997
|
+
ok: true,
|
|
2998
|
+
envelope,
|
|
2999
|
+
interchange
|
|
3000
|
+
};
|
|
3001
|
+
}
|
|
3002
|
+
function validateLkcourse(archivePath) {
|
|
3003
|
+
const read = readZip(archivePath);
|
|
3004
|
+
if (!read.ok) return read;
|
|
3005
|
+
return validateLkcourseArchiveEntries(read.entries, archivePath);
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
// src/lkcourse/import.ts
|
|
3009
|
+
var import_promises4 = require("fs/promises");
|
|
3010
|
+
var import_node_path14 = require("path");
|
|
3011
|
+
var IMPORT_ARTIFACTS = ["lessonkit.json", "dist"];
|
|
3012
|
+
async function pathExists2(path) {
|
|
3013
|
+
try {
|
|
3014
|
+
await (0, import_promises4.access)(path);
|
|
3015
|
+
return true;
|
|
3016
|
+
} catch {
|
|
3017
|
+
return false;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
async function renameOrCopy2(from, to, opts) {
|
|
3021
|
+
const renameFn = opts?.renameFn ?? import_promises4.rename;
|
|
3022
|
+
try {
|
|
3023
|
+
await renameFn(from, to);
|
|
3024
|
+
} catch (err) {
|
|
3025
|
+
const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
|
|
3026
|
+
if (code !== "EXDEV") throw err;
|
|
3027
|
+
await (0, import_promises4.cp)(from, to, { recursive: true });
|
|
3028
|
+
await (0, import_promises4.rm)(from, { recursive: true, force: true });
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
async function writeImportTree(stagingDir, manifest, entries, spaDistDir) {
|
|
3032
|
+
let fileCount = 0;
|
|
3033
|
+
await (0, import_promises4.writeFile)(
|
|
3034
|
+
(0, import_node_path14.join)(stagingDir, "lessonkit.json"),
|
|
3035
|
+
`${JSON.stringify(manifest, null, 2)}
|
|
3036
|
+
`,
|
|
3037
|
+
"utf8"
|
|
3038
|
+
);
|
|
3039
|
+
fileCount += 1;
|
|
3040
|
+
for (const [entryPath, data] of entries) {
|
|
3041
|
+
const normalized = entryPath.replace(/\\/g, "/");
|
|
3042
|
+
if (!normalized.startsWith(`${spaDistDir}/`)) continue;
|
|
3043
|
+
const relativeUnderSpa = normalized.slice(spaDistDir.length + 1);
|
|
3044
|
+
const outPath = (0, import_node_path14.join)(stagingDir, spaDistDir, relativeUnderSpa);
|
|
3045
|
+
const resolvedOut = (0, import_node_path14.resolve)(outPath);
|
|
3046
|
+
assertRealPathUnderRoot(stagingDir, resolvedOut);
|
|
3047
|
+
if (!isSafeZipEntryPath((0, import_node_path14.join)(spaDistDir, relativeUnderSpa))) {
|
|
3048
|
+
throw new Error(`unsafe extraction path: ${entryPath}`);
|
|
3049
|
+
}
|
|
3050
|
+
await (0, import_promises4.mkdir)((0, import_node_path14.dirname)(resolvedOut), { recursive: true });
|
|
3051
|
+
await (0, import_promises4.writeFile)(resolvedOut, data);
|
|
3052
|
+
fileCount += 1;
|
|
3053
|
+
}
|
|
3054
|
+
return fileCount;
|
|
3055
|
+
}
|
|
3056
|
+
async function backupImportArtifacts(targetDir) {
|
|
3057
|
+
const existing = [];
|
|
3058
|
+
for (const name of IMPORT_ARTIFACTS) {
|
|
3059
|
+
if (await pathExists2((0, import_node_path14.join)(targetDir, name))) {
|
|
3060
|
+
existing.push(name);
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
if (!existing.length) return void 0;
|
|
3064
|
+
const backupDir = await (0, import_promises4.mkdtemp)((0, import_node_path14.join)(targetDir, ".lkcourse-backup-"));
|
|
3065
|
+
for (const name of existing) {
|
|
3066
|
+
await renameOrCopy2((0, import_node_path14.join)(targetDir, name), (0, import_node_path14.join)(backupDir, name));
|
|
3067
|
+
}
|
|
3068
|
+
return backupDir;
|
|
3069
|
+
}
|
|
3070
|
+
async function restoreImportBackup(targetDir, backupDir) {
|
|
3071
|
+
for (const name of IMPORT_ARTIFACTS) {
|
|
3072
|
+
const backupPath = (0, import_node_path14.join)(backupDir, name);
|
|
3073
|
+
if (!await pathExists2(backupPath)) continue;
|
|
3074
|
+
const destPath = (0, import_node_path14.join)(targetDir, name);
|
|
3075
|
+
if (await pathExists2(destPath)) {
|
|
3076
|
+
await (0, import_promises4.rm)(destPath, { recursive: true, force: true });
|
|
3077
|
+
}
|
|
3078
|
+
await renameOrCopy2(backupPath, destPath);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
async function promoteImportStaging(stagingDir, targetDir) {
|
|
3082
|
+
const entries = await (0, import_promises4.readdir)(stagingDir, { withFileTypes: true });
|
|
3083
|
+
for (const entry of entries) {
|
|
3084
|
+
const srcPath = (0, import_node_path14.join)(stagingDir, entry.name);
|
|
3085
|
+
const destPath = (0, import_node_path14.join)(targetDir, entry.name);
|
|
3086
|
+
if (entry.isDirectory()) {
|
|
3087
|
+
await (0, import_promises4.cp)(srcPath, destPath, { recursive: true, force: true });
|
|
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);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
var promoteImportStagingImpl = promoteImportStaging;
|
|
3095
|
+
async function importLkcourse(options) {
|
|
3096
|
+
const archivePath = (0, import_node_path14.resolve)(options.archivePath);
|
|
3097
|
+
const targetDir = (0, import_node_path14.resolve)(options.targetDir);
|
|
3098
|
+
const validated = validateLkcourse(archivePath);
|
|
3099
|
+
if (!validated.ok) return validated;
|
|
3100
|
+
const { envelope, interchange } = validated;
|
|
3101
|
+
const manifest = envelope.sourceManifest;
|
|
3102
|
+
const spaDistDir = manifest.paths.spaDistDir.replace(/\\/g, "/");
|
|
3103
|
+
try {
|
|
3104
|
+
await (0, import_promises4.mkdir)(targetDir, { recursive: true });
|
|
3105
|
+
assertRealPathUnderRoot(targetDir, targetDir);
|
|
3106
|
+
} catch (err) {
|
|
3107
|
+
return {
|
|
3108
|
+
ok: false,
|
|
3109
|
+
issues: [
|
|
3110
|
+
{
|
|
3111
|
+
path: targetDir,
|
|
3112
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3113
|
+
}
|
|
3114
|
+
]
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
const read = readZip(archivePath);
|
|
3118
|
+
if (!read.ok) return read;
|
|
3119
|
+
let stagingDir;
|
|
3120
|
+
let backupDir;
|
|
3121
|
+
try {
|
|
3122
|
+
stagingDir = await (0, import_promises4.mkdtemp)((0, import_node_path14.join)(targetDir, ".lkcourse-import-"));
|
|
3123
|
+
const fileCount = await writeImportTree(stagingDir, manifest, read.entries, spaDistDir);
|
|
3124
|
+
backupDir = await backupImportArtifacts(targetDir);
|
|
3125
|
+
try {
|
|
3126
|
+
await promoteImportStagingImpl(stagingDir, targetDir);
|
|
3127
|
+
} catch (promoteError) {
|
|
3128
|
+
if (backupDir) {
|
|
3129
|
+
await restoreImportBackup(targetDir, backupDir);
|
|
3130
|
+
}
|
|
3131
|
+
throw promoteError;
|
|
3132
|
+
}
|
|
3133
|
+
if (backupDir) {
|
|
3134
|
+
await (0, import_promises4.rm)(backupDir, { recursive: true, force: true }).catch(() => void 0);
|
|
3135
|
+
backupDir = void 0;
|
|
3136
|
+
}
|
|
3137
|
+
await (0, import_promises4.rm)(stagingDir, { recursive: true, force: true });
|
|
3138
|
+
stagingDir = void 0;
|
|
3139
|
+
return {
|
|
3140
|
+
ok: true,
|
|
3141
|
+
targetDir,
|
|
3142
|
+
manifest,
|
|
3143
|
+
interchange,
|
|
3144
|
+
fileCount
|
|
3145
|
+
};
|
|
3146
|
+
} catch (err) {
|
|
3147
|
+
if (backupDir) {
|
|
3148
|
+
await restoreImportBackup(targetDir, backupDir).catch(() => void 0);
|
|
3149
|
+
await (0, import_promises4.rm)(backupDir, { recursive: true, force: true }).catch(() => void 0);
|
|
3150
|
+
}
|
|
3151
|
+
if (stagingDir) {
|
|
3152
|
+
await (0, import_promises4.rm)(stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
3153
|
+
}
|
|
3154
|
+
return {
|
|
3155
|
+
ok: false,
|
|
3156
|
+
issues: [
|
|
3157
|
+
{
|
|
3158
|
+
path: targetDir,
|
|
3159
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3160
|
+
}
|
|
3161
|
+
]
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
1855
3165
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1856
3166
|
0 && (module.exports = {
|
|
1857
3167
|
LESSONKIT_TELEMETRY_EVENTS,
|
|
3168
|
+
assertSpaDistContentsSafe,
|
|
1858
3169
|
assessmentDescriptorToLxpack,
|
|
1859
3170
|
buildLessonkitProject,
|
|
1860
3171
|
buildStagingPackage,
|
|
1861
3172
|
descriptorToInterchange,
|
|
1862
3173
|
ensureOutDirParent,
|
|
3174
|
+
escapeShellText,
|
|
3175
|
+
exportLkcourse,
|
|
1863
3176
|
extractAssessments,
|
|
3177
|
+
extractBlockTree,
|
|
3178
|
+
importLkcourse,
|
|
1864
3179
|
lessonkitInterchangeSchema,
|
|
1865
3180
|
loadLessonkitManifestFromFile,
|
|
1866
3181
|
mapLessonkitIds,
|
|
@@ -1870,6 +3185,7 @@ var import_validators2 = require("@lxpack/validators");
|
|
|
1870
3185
|
packageLessonkitCourse,
|
|
1871
3186
|
parseLessonkitInterchange,
|
|
1872
3187
|
parseLessonkitManifest,
|
|
3188
|
+
parseLkcourseEnvelope,
|
|
1873
3189
|
promoteStagingToOutDir,
|
|
1874
3190
|
remapArtifactPaths,
|
|
1875
3191
|
resolveSafePackageOutputOverride,
|
|
@@ -1879,6 +3195,8 @@ var import_validators2 = require("@lxpack/validators");
|
|
|
1879
3195
|
validateDescriptor,
|
|
1880
3196
|
validateDescriptorForTarget,
|
|
1881
3197
|
validateLessonkitProject,
|
|
3198
|
+
validateLkcourse,
|
|
3199
|
+
validateLkcourseArchiveEntries,
|
|
1882
3200
|
validatePackageInputs,
|
|
1883
3201
|
validateProjectPaths,
|
|
1884
3202
|
validateReactManifestParity,
|