@lessonkit/lxpack 1.4.0 → 1.5.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 +35 -7
- package/dist/bridge.cjs +160 -41
- package/dist/bridge.d.cts +27 -9
- package/dist/bridge.d.ts +27 -9
- package/dist/bridge.js +96 -27
- package/dist/chunk-HTZR4CF3.js +94 -0
- package/dist/index.cjs +434 -111
- package/dist/index.d.cts +23 -5
- package/dist/index.d.ts +23 -5
- package/dist/index.js +401 -98
- package/dist/telemetry-0fIWoomS.d.cts +17 -0
- package/dist/telemetry-0fIWoomS.d.ts +17 -0
- package/package.json +5 -5
- 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
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
buildStagingPackage: () => buildStagingPackage,
|
|
37
37
|
descriptorToInterchange: () => descriptorToInterchange,
|
|
38
38
|
ensureOutDirParent: () => ensureOutDirParent,
|
|
39
|
+
escapeShellText: () => escapeShellText,
|
|
39
40
|
extractAssessments: () => extractAssessments,
|
|
40
41
|
lessonkitInterchangeSchema: () => import_validators2.lessonkitInterchangeSchema,
|
|
41
42
|
loadLessonkitManifestFromFile: () => loadLessonkitManifestFromFile,
|
|
@@ -404,8 +405,30 @@ var ASSESSMENT_VALIDATORS = {
|
|
|
404
405
|
}
|
|
405
406
|
},
|
|
406
407
|
fillInBlanks: (assessment, path, issues) => {
|
|
407
|
-
if (assessment.kind
|
|
408
|
+
if (assessment.kind !== "fillInBlanks") return;
|
|
409
|
+
if (!assessment.template?.trim()) {
|
|
408
410
|
issues.push({ path: `${path}.template`, message: "template is required for fillInBlanks" });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const templateBlankCount = countStarDelimitedBlanks(assessment.template);
|
|
414
|
+
if (templateBlankCount === 0) {
|
|
415
|
+
issues.push({
|
|
416
|
+
path: `${path}.template`,
|
|
417
|
+
message: "template must include at least one blank wrapped in asterisks for fillInBlanks"
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
const explicitBlanks = assessment.blanks?.map((b) => ({ id: b.id?.trim() ?? "", answer: b.answer?.trim() ?? "" })).filter((b) => b.id.length > 0 && b.answer.length > 0) ?? [];
|
|
421
|
+
if (assessment.blanks !== void 0 && explicitBlanks.length === 0) {
|
|
422
|
+
issues.push({
|
|
423
|
+
path: `${path}.blanks`,
|
|
424
|
+
message: "blanks must include at least one entry with non-empty id and answer"
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
if (explicitBlanks.length > 0 && explicitBlanks.length !== templateBlankCount) {
|
|
428
|
+
issues.push({
|
|
429
|
+
path: `${path}.blanks`,
|
|
430
|
+
message: `blanks length (${explicitBlanks.length}) must match template blank count (${templateBlankCount})`
|
|
431
|
+
});
|
|
409
432
|
}
|
|
410
433
|
},
|
|
411
434
|
findHotspot: (assessment, path, issues) => {
|
|
@@ -603,27 +626,47 @@ function validateCourseDescriptor(input) {
|
|
|
603
626
|
}
|
|
604
627
|
|
|
605
628
|
// src/assessments.ts
|
|
629
|
+
function escapeShellText(text) {
|
|
630
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
631
|
+
}
|
|
632
|
+
function decodeShellEntities(text) {
|
|
633
|
+
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)));
|
|
634
|
+
}
|
|
635
|
+
function containsUnsafeShellMarkup(text) {
|
|
636
|
+
const decoded = decodeShellEntities(text);
|
|
637
|
+
return /<\/script/i.test(decoded) || /<!--/.test(decoded) || /</.test(decoded);
|
|
638
|
+
}
|
|
639
|
+
function sanitizeShellField(text) {
|
|
640
|
+
if (containsUnsafeShellMarkup(text)) return null;
|
|
641
|
+
return escapeShellText(text);
|
|
642
|
+
}
|
|
606
643
|
function slugChoiceId(text, index) {
|
|
607
644
|
const base = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
|
|
608
645
|
const stem = base.length ? base : "choice";
|
|
609
646
|
return `${stem}-${index + 1}`;
|
|
610
647
|
}
|
|
611
648
|
function mcqToLxpack(assessment) {
|
|
649
|
+
const checkId = sanitizeShellField(assessment.checkId);
|
|
650
|
+
const prompt = sanitizeShellField(assessment.question);
|
|
651
|
+
if (!checkId || !prompt) return null;
|
|
612
652
|
const choices = assessment.choices.map((text, index) => {
|
|
653
|
+
const sanitizedText = sanitizeShellField(text);
|
|
654
|
+
if (!sanitizedText) return null;
|
|
613
655
|
const id = slugChoiceId(text, index);
|
|
614
656
|
return {
|
|
615
657
|
id,
|
|
616
|
-
text,
|
|
658
|
+
text: sanitizedText,
|
|
617
659
|
correct: text === assessment.answer
|
|
618
660
|
};
|
|
619
661
|
});
|
|
662
|
+
if (choices.some((choice) => choice === null)) return null;
|
|
620
663
|
return {
|
|
621
|
-
id:
|
|
664
|
+
id: checkId,
|
|
622
665
|
passingScore: assessment.passingScore ?? 1,
|
|
623
666
|
questions: [
|
|
624
667
|
{
|
|
625
668
|
id: "q1",
|
|
626
|
-
prompt
|
|
669
|
+
prompt,
|
|
627
670
|
choices
|
|
628
671
|
}
|
|
629
672
|
]
|
|
@@ -683,22 +726,29 @@ var LMS_SHELL_TARGETS = /* @__PURE__ */ new Set([
|
|
|
683
726
|
"xapi",
|
|
684
727
|
"cmi5"
|
|
685
728
|
]);
|
|
729
|
+
function appendActivityIriIssues(issues, descriptor, target) {
|
|
730
|
+
const hasXapiTracking = Boolean(descriptor.tracking?.xapi);
|
|
731
|
+
const requiresForTarget = target === "xapi" || target === "cmi5";
|
|
732
|
+
if (!hasXapiTracking && !requiresForTarget) return;
|
|
733
|
+
const activityIri = descriptor.tracking?.xapi?.activityIri?.trim();
|
|
734
|
+
const targetSuffix = target === "xapi" || target === "cmi5" ? ` for ${target} export targets` : " when tracking.xapi is configured";
|
|
735
|
+
if (!activityIri) {
|
|
736
|
+
issues.push({
|
|
737
|
+
path: "tracking.xapi.activityIri",
|
|
738
|
+
message: `tracking.xapi.activityIri is required${targetSuffix}`
|
|
739
|
+
});
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (!/^https:\/\/.+/i.test(activityIri)) {
|
|
743
|
+
issues.push({
|
|
744
|
+
path: "tracking.xapi.activityIri",
|
|
745
|
+
message: `tracking.xapi.activityIri must be an HTTPS URL${targetSuffix}`
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
686
749
|
function validateDescriptorForExportTarget(descriptor, target) {
|
|
687
750
|
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
|
-
}
|
|
751
|
+
appendActivityIriIssues(issues, descriptor, target);
|
|
702
752
|
if (LMS_SHELL_TARGETS.has(target)) {
|
|
703
753
|
issues.push(...validateInjectableAssessments(descriptor).map((issue) => ({
|
|
704
754
|
...issue,
|
|
@@ -735,16 +785,50 @@ function validateDescriptorForTarget(input, target) {
|
|
|
735
785
|
var import_node_fs2 = require("fs");
|
|
736
786
|
var import_node_path2 = require("path");
|
|
737
787
|
var SCANNABLE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
738
|
-
function collectSourceUnderSrc(projectRoot) {
|
|
788
|
+
function collectSourceUnderSrc(projectRoot, issues) {
|
|
739
789
|
const srcDir = (0, import_node_path2.join)(projectRoot, "src");
|
|
740
790
|
if (!(0, import_node_fs2.existsSync)(srcDir)) return [];
|
|
741
791
|
const results = [];
|
|
742
792
|
const walk = (dir) => {
|
|
743
793
|
for (const entry of (0, import_node_fs2.readdirSync)(dir)) {
|
|
744
794
|
const abs = (0, import_node_path2.join)(dir, entry);
|
|
745
|
-
|
|
795
|
+
let stat2;
|
|
796
|
+
try {
|
|
797
|
+
stat2 = (0, import_node_fs2.lstatSync)(abs);
|
|
798
|
+
} catch {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (stat2.isSymbolicLink()) {
|
|
802
|
+
issues.push({
|
|
803
|
+
path: (0, import_node_path2.relative)(projectRoot, abs),
|
|
804
|
+
message: `Source tree contains symlink (rejected for parity scan): ${(0, import_node_path2.relative)(projectRoot, abs)}`,
|
|
805
|
+
severity: "error"
|
|
806
|
+
});
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
if (stat2.isDirectory()) {
|
|
810
|
+
try {
|
|
811
|
+
assertRealPathUnderRoot(projectRoot, abs);
|
|
812
|
+
} catch {
|
|
813
|
+
issues.push({
|
|
814
|
+
path: (0, import_node_path2.relative)(projectRoot, abs),
|
|
815
|
+
message: `Source directory escapes project root: ${(0, import_node_path2.relative)(projectRoot, abs)}`,
|
|
816
|
+
severity: "error"
|
|
817
|
+
});
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
746
820
|
walk(abs);
|
|
747
821
|
} else if (SCANNABLE_EXTENSIONS.some((ext) => entry.endsWith(ext))) {
|
|
822
|
+
try {
|
|
823
|
+
assertRealPathUnderRoot(projectRoot, abs);
|
|
824
|
+
} catch {
|
|
825
|
+
issues.push({
|
|
826
|
+
path: (0, import_node_path2.relative)(projectRoot, abs),
|
|
827
|
+
message: `Source file escapes project root: ${(0, import_node_path2.relative)(projectRoot, abs)}`,
|
|
828
|
+
severity: "error"
|
|
829
|
+
});
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
748
832
|
results.push((0, import_node_path2.relative)(projectRoot, abs));
|
|
749
833
|
}
|
|
750
834
|
}
|
|
@@ -752,20 +836,69 @@ function collectSourceUnderSrc(projectRoot) {
|
|
|
752
836
|
walk(srcDir);
|
|
753
837
|
return results;
|
|
754
838
|
}
|
|
755
|
-
function readAppSources(projectRoot, appSources) {
|
|
756
|
-
return appSources.map((rel) =>
|
|
839
|
+
function readAppSources(projectRoot, appSources, issues, customSourcesProvided) {
|
|
840
|
+
return appSources.map((rel) => {
|
|
841
|
+
if (!isSafeRelativeSpaPath(rel)) {
|
|
842
|
+
if (customSourcesProvided) {
|
|
843
|
+
issues.push({
|
|
844
|
+
path: rel,
|
|
845
|
+
message: `Unsafe appSources path skipped: ${rel}`,
|
|
846
|
+
severity: "warning"
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
const abs = (0, import_node_path2.join)(projectRoot, rel);
|
|
852
|
+
try {
|
|
853
|
+
assertRealPathUnderRoot(projectRoot, abs);
|
|
854
|
+
if ((0, import_node_fs2.existsSync)(abs) && (0, import_node_fs2.lstatSync)(abs).isSymbolicLink()) {
|
|
855
|
+
issues.push({
|
|
856
|
+
path: rel,
|
|
857
|
+
message: `appSources path is a symlink: ${rel}`,
|
|
858
|
+
severity: "error"
|
|
859
|
+
});
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
} catch {
|
|
863
|
+
issues.push({
|
|
864
|
+
path: rel,
|
|
865
|
+
message: `appSources path escapes project root: ${rel}`,
|
|
866
|
+
severity: "error"
|
|
867
|
+
});
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
if (!(0, import_node_fs2.existsSync)(abs)) return null;
|
|
871
|
+
return (0, import_node_fs2.readFileSync)(abs, "utf8");
|
|
872
|
+
}).filter((content) => content != null).join("\n");
|
|
757
873
|
}
|
|
758
874
|
function stripComments(source) {
|
|
759
875
|
return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
|
|
760
876
|
}
|
|
761
|
-
function
|
|
762
|
-
return [
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
877
|
+
function maskUnrelatedStringLiterals(source) {
|
|
878
|
+
return source.replace(/(["'`])(?:\\.|(?!\1).)*\1/g, (match, _quote, offset, full) => {
|
|
879
|
+
const before = full.slice(Math.max(0, offset - 24), offset);
|
|
880
|
+
if (/\b(?:courseId|checkId|lessonId)\s*=\s*$/.test(before)) {
|
|
881
|
+
return match;
|
|
882
|
+
}
|
|
883
|
+
return '""';
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
function idPropPresent(source, prop, id) {
|
|
887
|
+
const stripped = stripComments(source);
|
|
888
|
+
const masked = maskUnrelatedStringLiterals(stripped);
|
|
889
|
+
return jsxPropRegex(prop, id).test(masked);
|
|
890
|
+
}
|
|
891
|
+
function escapeRegExp(value) {
|
|
892
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
893
|
+
}
|
|
894
|
+
function jsxPropRegex(prop, id) {
|
|
895
|
+
const escapedId = escapeRegExp(id);
|
|
896
|
+
return new RegExp(
|
|
897
|
+
`(?<![A-Za-z0-9_$])${prop}\\s*=\\s*(?:"${escapedId}"|'${escapedId}'|\\{\\s*["'\`]${escapedId}["'\`]\\s*\\}|\\{\\s*\`${escapedId}\`\\s*\\})`
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
function maskStringLiterals(source) {
|
|
901
|
+
return source.replace(/(["'`])(?:\\.|(?!\1).)*\1/g, '""');
|
|
769
902
|
}
|
|
770
903
|
function extractStringConstants(source) {
|
|
771
904
|
const stripped = stripComments(source);
|
|
@@ -776,7 +909,9 @@ function extractStringConstants(source) {
|
|
|
776
909
|
}
|
|
777
910
|
return map;
|
|
778
911
|
}
|
|
779
|
-
function idUsedViaConstant(
|
|
912
|
+
function idUsedViaConstant(source, prop, id, constants) {
|
|
913
|
+
const stripped = stripComments(source);
|
|
914
|
+
const masked = maskStringLiterals(stripped);
|
|
780
915
|
for (const [name, value] of constants) {
|
|
781
916
|
if (value !== id) continue;
|
|
782
917
|
const jsxPatterns = [
|
|
@@ -785,40 +920,61 @@ function idUsedViaConstant(stripped, prop, id, constants) {
|
|
|
785
920
|
`${prop}={${name} }`,
|
|
786
921
|
`${prop}={ ${name}}`
|
|
787
922
|
];
|
|
788
|
-
if (jsxPatterns.some((p) =>
|
|
789
|
-
const objPatterns = [`${prop}: ${name}`, `${prop}:${name}`];
|
|
790
|
-
if (objPatterns.some((p) => stripped.includes(p))) return true;
|
|
923
|
+
if (jsxPatterns.some((p) => masked.includes(p))) return true;
|
|
791
924
|
}
|
|
792
925
|
return false;
|
|
793
926
|
}
|
|
794
|
-
function
|
|
927
|
+
function lessonIdInDataLiteral(source, lessonId) {
|
|
795
928
|
const stripped = stripComments(source);
|
|
796
|
-
|
|
797
|
-
return
|
|
929
|
+
const escaped = escapeRegExp(lessonId);
|
|
930
|
+
return new RegExp(`\\bid\\s*:\\s*["'\`]${escaped}["'\`]`).test(stripped);
|
|
798
931
|
}
|
|
799
|
-
function
|
|
932
|
+
function lessonIdPresent(source, lessonId) {
|
|
933
|
+
if (idPropPresent(source, "lessonId", lessonId)) return true;
|
|
934
|
+
if (idUsedViaConstant(source, "lessonId", lessonId, extractStringConstants(source))) return true;
|
|
935
|
+
return lessonIdInDataLiteral(source, lessonId);
|
|
936
|
+
}
|
|
937
|
+
function courseConfigCourseIdPresent(source, courseId) {
|
|
800
938
|
const stripped = stripComments(source);
|
|
801
|
-
|
|
802
|
-
|
|
939
|
+
const escaped = escapeRegExp(courseId);
|
|
940
|
+
const literalPattern = new RegExp(
|
|
941
|
+
`(?<![A-Za-z0-9_$])courseId\\s*:\\s*(?:"${escaped}"|'${escaped}')`
|
|
942
|
+
);
|
|
943
|
+
if (literalPattern.test(stripped)) return true;
|
|
944
|
+
return idUsedViaConstant(source, "courseId", courseId, extractStringConstants(source));
|
|
945
|
+
}
|
|
946
|
+
function courseIdPresent(source, courseId) {
|
|
947
|
+
if (idPropPresent(source, "courseId", courseId)) return true;
|
|
948
|
+
if (idUsedViaConstant(source, "courseId", courseId, extractStringConstants(source))) return true;
|
|
949
|
+
return courseConfigCourseIdPresent(source, courseId);
|
|
950
|
+
}
|
|
951
|
+
function checkIdPresent(source, checkId) {
|
|
952
|
+
if (idPropPresent(source, "checkId", checkId)) return true;
|
|
953
|
+
return idUsedViaConstant(source, "checkId", checkId, extractStringConstants(source));
|
|
803
954
|
}
|
|
804
955
|
var ID_SYNC_DOC = "https://lessonkit.readthedocs.io/en/latest/guides/react-developers/quickstart.html#keep-react-ids-in-sync-with-lessonkitjson";
|
|
805
956
|
function parityHint(message) {
|
|
806
957
|
return `${message} See ${ID_SYNC_DOC}`;
|
|
807
958
|
}
|
|
808
959
|
function validateReactManifestParity(opts) {
|
|
809
|
-
const
|
|
810
|
-
const
|
|
960
|
+
const issues = [];
|
|
961
|
+
const customSourcesProvided = opts.appSources !== void 0;
|
|
962
|
+
const appSources = opts.appSources ?? collectSourceUnderSrc(opts.projectRoot, issues);
|
|
963
|
+
const source = readAppSources(
|
|
964
|
+
opts.projectRoot,
|
|
965
|
+
appSources,
|
|
966
|
+
issues,
|
|
967
|
+
customSourcesProvided
|
|
968
|
+
);
|
|
811
969
|
const hasDescriptorIds = Boolean(opts.descriptor.courseId) || (opts.descriptor.assessments?.length ?? 0) > 0;
|
|
812
970
|
if (!source.trim()) {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
];
|
|
971
|
+
issues.push({
|
|
972
|
+
path: appSources.length > 0 ? appSources.join(", ") : "src/",
|
|
973
|
+
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",
|
|
974
|
+
severity: hasDescriptorIds ? "error" : "warning"
|
|
975
|
+
});
|
|
976
|
+
return issues;
|
|
820
977
|
}
|
|
821
|
-
const issues = [];
|
|
822
978
|
const courseId = opts.descriptor.courseId;
|
|
823
979
|
if (!courseIdPresent(source, courseId)) {
|
|
824
980
|
issues.push({
|
|
@@ -829,6 +985,19 @@ function validateReactManifestParity(opts) {
|
|
|
829
985
|
severity: "error"
|
|
830
986
|
});
|
|
831
987
|
}
|
|
988
|
+
for (const lesson of opts.descriptor.lessons ?? []) {
|
|
989
|
+
const lessonId = lesson.id;
|
|
990
|
+
if (!lessonId) continue;
|
|
991
|
+
if (!lessonIdPresent(source, lessonId)) {
|
|
992
|
+
issues.push({
|
|
993
|
+
path: `lessons.id:${lessonId}`,
|
|
994
|
+
message: parityHint(
|
|
995
|
+
`React app source missing lessonId="${lessonId}" declared in lessonkit.json.`
|
|
996
|
+
),
|
|
997
|
+
severity: "error"
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
832
1001
|
for (const assessment of opts.descriptor.assessments ?? []) {
|
|
833
1002
|
const checkId = assessment.checkId;
|
|
834
1003
|
if (!checkId) continue;
|
|
@@ -847,7 +1016,13 @@ function validateReactManifestParity(opts) {
|
|
|
847
1016
|
|
|
848
1017
|
// src/validateProjectPaths.ts
|
|
849
1018
|
var import_node_path3 = require("path");
|
|
850
|
-
|
|
1019
|
+
var RESERVED_OUTPUT_SEGMENTS = /* @__PURE__ */ new Set([".git", "node_modules", ".github"]);
|
|
1020
|
+
function isReservedOutputPath(value) {
|
|
1021
|
+
const normalized = value.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
1022
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
1023
|
+
return segments.some((segment) => RESERVED_OUTPUT_SEGMENTS.has(segment));
|
|
1024
|
+
}
|
|
1025
|
+
function validatePathField(value, fieldPath, projectRoot, issues, options) {
|
|
851
1026
|
if (!isSafeRelativeSpaPath(value)) {
|
|
852
1027
|
issues.push({
|
|
853
1028
|
path: fieldPath,
|
|
@@ -855,6 +1030,13 @@ function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
|
855
1030
|
});
|
|
856
1031
|
return;
|
|
857
1032
|
}
|
|
1033
|
+
if (options?.rejectReserved && isReservedOutputPath(value)) {
|
|
1034
|
+
issues.push({
|
|
1035
|
+
path: fieldPath,
|
|
1036
|
+
message: "path must not target reserved directories (.git, node_modules, .github)"
|
|
1037
|
+
});
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
858
1040
|
try {
|
|
859
1041
|
assertRealPathUnderRoot(projectRoot, (0, import_node_path3.resolve)(projectRoot, value));
|
|
860
1042
|
} catch {
|
|
@@ -871,10 +1053,14 @@ function validateProjectPaths(projectRoot, paths) {
|
|
|
871
1053
|
validatePathField(paths.spaDistDir.trim(), "paths.spaDistDir", root, issues);
|
|
872
1054
|
}
|
|
873
1055
|
if (paths.lxpackOutDir?.trim()) {
|
|
874
|
-
validatePathField(paths.lxpackOutDir.trim(), "paths.lxpackOutDir", root, issues
|
|
1056
|
+
validatePathField(paths.lxpackOutDir.trim(), "paths.lxpackOutDir", root, issues, {
|
|
1057
|
+
rejectReserved: true
|
|
1058
|
+
});
|
|
875
1059
|
}
|
|
876
1060
|
if (paths.outputBaseDir?.trim()) {
|
|
877
|
-
validatePathField(paths.outputBaseDir.trim(), "paths.outputBaseDir", root, issues
|
|
1061
|
+
validatePathField(paths.outputBaseDir.trim(), "paths.outputBaseDir", root, issues, {
|
|
1062
|
+
rejectReserved: true
|
|
1063
|
+
});
|
|
878
1064
|
}
|
|
879
1065
|
return issues;
|
|
880
1066
|
}
|
|
@@ -887,11 +1073,17 @@ function resolveSafePackageOutputOverride(projectRoot, override) {
|
|
|
887
1073
|
if ((0, import_node_path3.isAbsolute)(trimmed)) {
|
|
888
1074
|
const resolved2 = (0, import_node_path3.resolve)(trimmed);
|
|
889
1075
|
assertRealPathUnderRoot(root, resolved2);
|
|
1076
|
+
if (isReservedOutputPath(trimmed)) {
|
|
1077
|
+
throw new Error(`unsafe output path: ${override} targets a reserved directory`);
|
|
1078
|
+
}
|
|
890
1079
|
return resolved2;
|
|
891
1080
|
}
|
|
892
1081
|
if (!isSafeRelativeSpaPath(trimmed)) {
|
|
893
1082
|
throw new Error(`unsafe output path: ${override}`);
|
|
894
1083
|
}
|
|
1084
|
+
if (isReservedOutputPath(trimmed)) {
|
|
1085
|
+
throw new Error(`unsafe output path: ${override} targets a reserved directory`);
|
|
1086
|
+
}
|
|
895
1087
|
const resolved = (0, import_node_path3.resolve)(root, trimmed);
|
|
896
1088
|
assertRealPathUnderRoot(root, resolved);
|
|
897
1089
|
return resolved;
|
|
@@ -1069,8 +1261,11 @@ async function walkDistDir(rootReal, current, label) {
|
|
|
1069
1261
|
let entryReal;
|
|
1070
1262
|
try {
|
|
1071
1263
|
entryReal = (0, import_node_fs3.realpathSync)(entryPath);
|
|
1072
|
-
} catch {
|
|
1073
|
-
|
|
1264
|
+
} catch (err) {
|
|
1265
|
+
throw new Error(
|
|
1266
|
+
`spa dist for "${label}" could not resolve path: ${entryPath}`,
|
|
1267
|
+
{ cause: err }
|
|
1268
|
+
);
|
|
1074
1269
|
}
|
|
1075
1270
|
assertResolvedPathUnderRoot(rootReal, entryReal);
|
|
1076
1271
|
if (stat2.isDirectory()) {
|
|
@@ -1093,9 +1288,7 @@ async function writeLxpackProject(options) {
|
|
|
1093
1288
|
throw new Error(injectableIssues.map((i) => `${i.path}: ${i.message}`).join("; "));
|
|
1094
1289
|
}
|
|
1095
1290
|
const outDir = (0, import_node_path6.resolve)(options.outDir);
|
|
1096
|
-
|
|
1097
|
-
assertRealPathUnderRoot((0, import_node_path6.resolve)(options.projectRoot), outDir);
|
|
1098
|
-
}
|
|
1291
|
+
assertRealPathUnderRoot((0, import_node_path6.resolve)(options.projectRoot), outDir);
|
|
1099
1292
|
const spaDirs = await resolveSpaDirs({ ...options, descriptor });
|
|
1100
1293
|
await assertSpaDistContentsSafe(spaDirs, options.projectRoot);
|
|
1101
1294
|
const interchange = descriptorToInterchange(descriptor);
|
|
@@ -1291,21 +1484,36 @@ function promoteLockPath(outDir) {
|
|
|
1291
1484
|
const hash = (0, import_node_crypto.createHash)("sha256").update((0, import_node_path8.resolve)(outDir)).digest("hex").slice(0, 16);
|
|
1292
1485
|
return (0, import_node_path8.join)(parent, `.lk-promote-lock-${hash}`);
|
|
1293
1486
|
}
|
|
1294
|
-
var
|
|
1487
|
+
var STALE_ARTIFACT_TTL_MS = 5 * 60 * 1e3;
|
|
1488
|
+
var MAX_LOCK_AGE_MS = 30 * 60 * 1e3;
|
|
1489
|
+
var LOCK_TOKEN_RE = /^(\d+)\n([0-9a-f-]{36})(?:\n(\d+))?\n?$/i;
|
|
1295
1490
|
async function isStalePromoteLock(lockPath) {
|
|
1296
1491
|
try {
|
|
1492
|
+
const stat2 = await fsp.stat(lockPath);
|
|
1297
1493
|
const content = await fsp.readFile(lockPath, "utf8");
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
return true;
|
|
1494
|
+
const match = content.match(LOCK_TOKEN_RE);
|
|
1495
|
+
let lockAgeMs = Date.now() - stat2.mtimeMs;
|
|
1496
|
+
if (match?.[3]) {
|
|
1497
|
+
const startedAt = Number.parseInt(match[3], 10);
|
|
1498
|
+
if (Number.isFinite(startedAt) && startedAt > 0) {
|
|
1499
|
+
lockAgeMs = Date.now() - startedAt;
|
|
1305
1500
|
}
|
|
1306
1501
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1502
|
+
if (lockAgeMs > MAX_LOCK_AGE_MS) {
|
|
1503
|
+
return true;
|
|
1504
|
+
}
|
|
1505
|
+
if (match) {
|
|
1506
|
+
const pid = Number.parseInt(match[1], 10);
|
|
1507
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
1508
|
+
try {
|
|
1509
|
+
process.kill(pid, 0);
|
|
1510
|
+
return false;
|
|
1511
|
+
} catch {
|
|
1512
|
+
return true;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
return lockAgeMs > STALE_ARTIFACT_TTL_MS;
|
|
1309
1517
|
} catch {
|
|
1310
1518
|
return true;
|
|
1311
1519
|
}
|
|
@@ -1318,6 +1526,8 @@ async function withPromoteLock(outDir, fn) {
|
|
|
1318
1526
|
try {
|
|
1319
1527
|
lockHandle = await fsp.open(lockPath, "wx");
|
|
1320
1528
|
await lockHandle.writeFile(`${process.pid}
|
|
1529
|
+
${(0, import_node_crypto.randomUUID)()}
|
|
1530
|
+
${Date.now()}
|
|
1321
1531
|
`, "utf8");
|
|
1322
1532
|
break;
|
|
1323
1533
|
} catch (err) {
|
|
@@ -1349,22 +1559,77 @@ async function withPromoteLock(outDir, fn) {
|
|
|
1349
1559
|
);
|
|
1350
1560
|
}
|
|
1351
1561
|
}
|
|
1352
|
-
async function
|
|
1562
|
+
async function removeStaleLegacyPromoteArtifacts(outDir) {
|
|
1353
1563
|
const legacyTmp = `${outDir}.tmp-promote`;
|
|
1354
1564
|
const legacyBak = `${outDir}.bak`;
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1565
|
+
const blocked = [];
|
|
1566
|
+
for (const legacyPath of [legacyTmp, legacyBak]) {
|
|
1567
|
+
if (!await pathExists(legacyPath)) continue;
|
|
1568
|
+
try {
|
|
1569
|
+
const stat2 = await fsp.stat(legacyPath);
|
|
1570
|
+
if (Date.now() - stat2.mtimeMs > STALE_ARTIFACT_TTL_MS) {
|
|
1571
|
+
await fsp.rm(legacyPath, { recursive: true, force: true }).catch(
|
|
1572
|
+
/* v8 ignore next */
|
|
1573
|
+
() => void 0
|
|
1574
|
+
);
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
} catch {
|
|
1578
|
+
}
|
|
1579
|
+
blocked.push(legacyPath);
|
|
1580
|
+
}
|
|
1581
|
+
if (blocked.length) {
|
|
1582
|
+
const rmHint = blocked.map((p) => `rm -rf ${JSON.stringify(p)}`).join("; ");
|
|
1359
1583
|
throw new Error(
|
|
1360
|
-
`[lessonkit/lxpack] cannot promote: remove stale packaging artifacts from a previous failed run: ${
|
|
1584
|
+
`[lessonkit/lxpack] cannot promote: remove stale packaging artifacts from a previous failed run: ${blocked.join(", ")}. Try: ${rmHint}`
|
|
1361
1585
|
);
|
|
1362
1586
|
}
|
|
1363
1587
|
}
|
|
1364
|
-
async function
|
|
1588
|
+
async function listRelativePaths(root, dir = root) {
|
|
1589
|
+
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
1590
|
+
const paths = [];
|
|
1591
|
+
for (const entry of entries) {
|
|
1592
|
+
const full = (0, import_node_path8.join)(dir, entry.name);
|
|
1593
|
+
if (entry.isDirectory()) {
|
|
1594
|
+
paths.push(...await listRelativePaths(root, full));
|
|
1595
|
+
} else if (entry.isFile()) {
|
|
1596
|
+
paths.push(full.slice(root.length + 1));
|
|
1597
|
+
} else {
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
return paths;
|
|
1601
|
+
}
|
|
1602
|
+
async function mergePreservedOutArtifacts(priorArtifactsDir, destArtifactsDir, newArtifactPaths) {
|
|
1603
|
+
if (!await pathExists(priorArtifactsDir)) return;
|
|
1604
|
+
for (const rel of await listRelativePaths(priorArtifactsDir)) {
|
|
1605
|
+
if (newArtifactPaths.has(rel)) continue;
|
|
1606
|
+
const src = (0, import_node_path8.join)(priorArtifactsDir, rel);
|
|
1607
|
+
const dest = (0, import_node_path8.join)(destArtifactsDir, rel);
|
|
1608
|
+
await fsp.mkdir((0, import_node_path8.dirname)(dest), { recursive: true });
|
|
1609
|
+
await fsp.cp(src, dest, { force: true });
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
async function promoteStagingToOutDir(stagingDir, outDir, options) {
|
|
1613
|
+
const outputBaseDir = options?.outputBaseDir ?? ".lxpack/out";
|
|
1614
|
+
if (options?.projectRoot) {
|
|
1615
|
+
assertRealPathUnderRoot((0, import_node_path8.resolve)(options.projectRoot), (0, import_node_path8.resolve)(outDir));
|
|
1616
|
+
}
|
|
1365
1617
|
return withPromoteLock(outDir, async () => {
|
|
1366
|
-
await
|
|
1618
|
+
await removeStaleLegacyPromoteArtifacts(outDir);
|
|
1619
|
+
const stagingArtifactsDir = (0, import_node_path8.join)(stagingDir, outputBaseDir);
|
|
1620
|
+
const newArtifactPaths = /* @__PURE__ */ new Set();
|
|
1621
|
+
if (await pathExists(stagingArtifactsDir)) {
|
|
1622
|
+
for (const rel of await listRelativePaths(stagingArtifactsDir)) {
|
|
1623
|
+
newArtifactPaths.add(rel);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1367
1626
|
const parent = (0, import_node_path8.dirname)(outDir);
|
|
1627
|
+
let priorArtifactsBackup;
|
|
1628
|
+
const existingArtifactsDir = (0, import_node_path8.join)(outDir, outputBaseDir);
|
|
1629
|
+
if (await pathExists(outDir) && await pathExists(existingArtifactsDir)) {
|
|
1630
|
+
priorArtifactsBackup = await fsp.mkdtemp((0, import_node_path8.join)(parent, ".lk-prior-out-"));
|
|
1631
|
+
await fsp.cp(existingArtifactsDir, (0, import_node_path8.join)(priorArtifactsBackup, outputBaseDir), { recursive: true });
|
|
1632
|
+
}
|
|
1368
1633
|
const tmpPromote = await fsp.mkdtemp((0, import_node_path8.join)(parent, ".lk-promote-"));
|
|
1369
1634
|
await renameOrCopy(stagingDir, tmpPromote);
|
|
1370
1635
|
const hadOutDir = await pathExists(outDir);
|
|
@@ -1421,6 +1686,20 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1421
1686
|
}
|
|
1422
1687
|
throw promoteError;
|
|
1423
1688
|
}
|
|
1689
|
+
if (priorArtifactsBackup) {
|
|
1690
|
+
try {
|
|
1691
|
+
await mergePreservedOutArtifacts(
|
|
1692
|
+
(0, import_node_path8.join)(priorArtifactsBackup, outputBaseDir),
|
|
1693
|
+
(0, import_node_path8.join)(outDir, outputBaseDir),
|
|
1694
|
+
newArtifactPaths
|
|
1695
|
+
);
|
|
1696
|
+
} finally {
|
|
1697
|
+
await fsp.rm(priorArtifactsBackup, { recursive: true, force: true }).catch(
|
|
1698
|
+
/* v8 ignore next */
|
|
1699
|
+
() => void 0
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1424
1703
|
if (backup) {
|
|
1425
1704
|
await fsp.rm(backup, { recursive: true, force: true }).catch(
|
|
1426
1705
|
/* v8 ignore next */
|
|
@@ -1438,6 +1717,7 @@ var import_api = require("@lxpack/api");
|
|
|
1438
1717
|
async function buildStagingPackage(options) {
|
|
1439
1718
|
const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
|
|
1440
1719
|
const stagingDir = await fsp2.mkdtemp((0, import_node_path9.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
|
|
1720
|
+
let succeeded = false;
|
|
1441
1721
|
try {
|
|
1442
1722
|
let spaDirs;
|
|
1443
1723
|
try {
|
|
@@ -1493,6 +1773,7 @@ async function buildStagingPackage(options) {
|
|
|
1493
1773
|
}))
|
|
1494
1774
|
};
|
|
1495
1775
|
}
|
|
1776
|
+
succeeded = true;
|
|
1496
1777
|
return {
|
|
1497
1778
|
ok: true,
|
|
1498
1779
|
stagingDir,
|
|
@@ -1506,6 +1787,13 @@ async function buildStagingPackage(options) {
|
|
|
1506
1787
|
() => void 0
|
|
1507
1788
|
);
|
|
1508
1789
|
throw err;
|
|
1790
|
+
} finally {
|
|
1791
|
+
if (!succeeded) {
|
|
1792
|
+
await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1793
|
+
/* v8 ignore next */
|
|
1794
|
+
() => void 0
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1509
1797
|
}
|
|
1510
1798
|
}
|
|
1511
1799
|
async function ensureOutDirParent(outDir) {
|
|
@@ -1570,24 +1858,22 @@ async function packageLessonkitCourse(options) {
|
|
|
1570
1858
|
};
|
|
1571
1859
|
}
|
|
1572
1860
|
const descriptor = descriptorValidation.descriptor;
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
};
|
|
1590
|
-
}
|
|
1861
|
+
const parityIssues = validateReactManifestParity({
|
|
1862
|
+
projectRoot: writeOpts.projectRoot,
|
|
1863
|
+
descriptor
|
|
1864
|
+
});
|
|
1865
|
+
const parityFailures = writeOpts.strictParity ? parityIssues : parityIssues.filter((i) => i.severity === "error");
|
|
1866
|
+
if (parityFailures.length > 0) {
|
|
1867
|
+
return {
|
|
1868
|
+
ok: false,
|
|
1869
|
+
courseDir: outDir,
|
|
1870
|
+
target,
|
|
1871
|
+
issues: parityFailures.map((i) => ({
|
|
1872
|
+
path: i.path,
|
|
1873
|
+
message: i.message,
|
|
1874
|
+
severity: i.severity
|
|
1875
|
+
}))
|
|
1876
|
+
};
|
|
1591
1877
|
}
|
|
1592
1878
|
const staged = await buildStagingPackage({
|
|
1593
1879
|
...writeOpts,
|
|
@@ -1646,7 +1932,7 @@ async function packageLessonkitCourse(options) {
|
|
|
1646
1932
|
ok: false,
|
|
1647
1933
|
courseDir: outDir,
|
|
1648
1934
|
target,
|
|
1649
|
-
validation: { ok:
|
|
1935
|
+
validation: { ok: false, manifest: build.manifest, issues: build.issues },
|
|
1650
1936
|
build,
|
|
1651
1937
|
issues: artifactIssues
|
|
1652
1938
|
};
|
|
@@ -1660,7 +1946,10 @@ async function packageLessonkitCourse(options) {
|
|
|
1660
1946
|
};
|
|
1661
1947
|
try {
|
|
1662
1948
|
await ensureOutDirParent(outDir);
|
|
1663
|
-
await promoteStagingToOutDir(stagingDir, outDir
|
|
1949
|
+
await promoteStagingToOutDir(stagingDir, outDir, {
|
|
1950
|
+
outputBaseDir: outputBaseDir ?? ".lxpack/out",
|
|
1951
|
+
projectRoot: writeOpts.projectRoot
|
|
1952
|
+
});
|
|
1664
1953
|
} catch (err) {
|
|
1665
1954
|
await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(
|
|
1666
1955
|
/* v8 ignore next */
|
|
@@ -1783,6 +2072,20 @@ function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
|
|
|
1783
2072
|
message: `"course.spaDistDir" (${courseSpaDistDir}) differs from "paths.spaDistDir" (${paths.spaDistDir}). Use paths.spaDistDir for CLI build and package.`
|
|
1784
2073
|
});
|
|
1785
2074
|
}
|
|
2075
|
+
for (const key of ["spaDistDir", "lxpackOutDir", "outputBaseDir"]) {
|
|
2076
|
+
const value = paths[key];
|
|
2077
|
+
if (!isSafeRelativeSpaPath(value)) {
|
|
2078
|
+
issues.push({
|
|
2079
|
+
path: `paths.${key}`,
|
|
2080
|
+
message: "path must be relative without '..' segments or absolute prefixes"
|
|
2081
|
+
});
|
|
2082
|
+
} else if ((key === "lxpackOutDir" || key === "outputBaseDir") && isReservedOutputPath(value)) {
|
|
2083
|
+
issues.push({
|
|
2084
|
+
path: `paths.${key}`,
|
|
2085
|
+
message: "path must not target reserved directories (.git, node_modules, .github)"
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
1786
2089
|
if (projectRoot) {
|
|
1787
2090
|
const pathIssues = validateProjectPaths(projectRoot, paths);
|
|
1788
2091
|
for (const pi of pathIssues) {
|
|
@@ -1814,37 +2117,56 @@ var import_tracking_schema2 = require("@lxpack/tracking-schema");
|
|
|
1814
2117
|
|
|
1815
2118
|
// src/telemetry.ts
|
|
1816
2119
|
var import_tracking_schema = require("@lxpack/tracking-schema");
|
|
1817
|
-
var
|
|
2120
|
+
var BRANCH_TELEMETRY_EVENTS = ["branch_node_viewed", "branch_selected"];
|
|
2121
|
+
var ASSESSMENT_TELEMETRY_EVENTS = ["assessment_answered"];
|
|
2122
|
+
var SUPPORTED = /* @__PURE__ */ new Set([
|
|
2123
|
+
...import_tracking_schema.LESSONKIT_TELEMETRY_EVENTS,
|
|
2124
|
+
...BRANCH_TELEMETRY_EVENTS,
|
|
2125
|
+
...ASSESSMENT_TELEMETRY_EVENTS
|
|
2126
|
+
]);
|
|
1818
2127
|
function isQuizAnsweredData(data) {
|
|
1819
|
-
return typeof data === "object" && data !== null && typeof data.checkId === "string";
|
|
2128
|
+
return typeof data === "object" && data !== null && typeof data.checkId === "string" && data.checkId.length > 0;
|
|
1820
2129
|
}
|
|
1821
2130
|
function isQuizCompletedData(data) {
|
|
1822
|
-
return typeof data === "object" && data !== null && typeof data.checkId === "string";
|
|
2131
|
+
return typeof data === "object" && data !== null && typeof data.checkId === "string" && data.checkId.length > 0;
|
|
2132
|
+
}
|
|
2133
|
+
function isAssessmentAnsweredData(data) {
|
|
2134
|
+
return typeof data === "object" && data !== null && typeof data.checkId === "string" && data.checkId.length > 0;
|
|
1823
2135
|
}
|
|
1824
2136
|
function isInteractionData(data) {
|
|
1825
2137
|
return typeof data === "object" && data !== null;
|
|
1826
2138
|
}
|
|
2139
|
+
function isBranchNodeViewedData(data) {
|
|
2140
|
+
return typeof data === "object" && data !== null && typeof data.blockId === "string" && typeof data.nodeId === "string";
|
|
2141
|
+
}
|
|
2142
|
+
function isBranchSelectedData(data) {
|
|
2143
|
+
return typeof data === "object" && data !== null && typeof data.blockId === "string" && typeof data.fromNodeId === "string" && typeof data.toNodeId === "string";
|
|
2144
|
+
}
|
|
1827
2145
|
function telemetryEventToLessonkit(event) {
|
|
1828
2146
|
if (!SUPPORTED.has(event.name)) {
|
|
1829
2147
|
return null;
|
|
1830
2148
|
}
|
|
1831
|
-
const name = event.name;
|
|
1832
2149
|
const mapped = {
|
|
1833
|
-
name,
|
|
2150
|
+
name: event.name,
|
|
1834
2151
|
lessonId: event.lessonId
|
|
1835
2152
|
};
|
|
1836
|
-
if (name === "quiz_completed" || name === "quiz_answered") {
|
|
2153
|
+
if (event.name === "quiz_completed" || event.name === "quiz_answered" || event.name === "assessment_answered") {
|
|
1837
2154
|
const data = event.data;
|
|
1838
|
-
if (isQuizAnsweredData(data)
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
mapped.
|
|
2155
|
+
if (!isQuizAnsweredData(data) && !isQuizCompletedData(data) && !isAssessmentAnsweredData(data)) {
|
|
2156
|
+
return null;
|
|
2157
|
+
}
|
|
2158
|
+
mapped.assessmentId = data.checkId;
|
|
2159
|
+
if ("score" in data) {
|
|
2160
|
+
mapped.score = data.score;
|
|
2161
|
+
mapped.maxScore = data.maxScore;
|
|
2162
|
+
mapped.passingScore = data.passingScore;
|
|
1846
2163
|
}
|
|
1847
|
-
|
|
2164
|
+
mapped.data = data;
|
|
2165
|
+
} else if (mapped.name === "interaction" && event.data && isInteractionData(event.data)) {
|
|
2166
|
+
mapped.data = event.data;
|
|
2167
|
+
} else if (event.name === "branch_node_viewed" && isBranchNodeViewedData(event.data)) {
|
|
2168
|
+
mapped.data = event.data;
|
|
2169
|
+
} else if (event.name === "branch_selected" && isBranchSelectedData(event.data)) {
|
|
1848
2170
|
mapped.data = event.data;
|
|
1849
2171
|
}
|
|
1850
2172
|
return mapped;
|
|
@@ -1860,6 +2182,7 @@ var import_validators2 = require("@lxpack/validators");
|
|
|
1860
2182
|
buildStagingPackage,
|
|
1861
2183
|
descriptorToInterchange,
|
|
1862
2184
|
ensureOutDirParent,
|
|
2185
|
+
escapeShellText,
|
|
1863
2186
|
extractAssessments,
|
|
1864
2187
|
lessonkitInterchangeSchema,
|
|
1865
2188
|
loadLessonkitManifestFromFile,
|