@lessonkit/lxpack 1.3.0 → 1.4.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 +2 -2
- package/dist/bridge.d.cts +3 -2
- package/dist/bridge.d.ts +3 -2
- package/dist/index.cjs +295 -88
- package/dist/index.d.cts +24 -10
- package/dist/index.d.ts +24 -10
- package/dist/index.js +264 -58
- package/package.json +8 -8
package/dist/index.cjs
CHANGED
|
@@ -57,6 +57,7 @@ __export(index_exports, {
|
|
|
57
57
|
validateLessonkitProject: () => validateLessonkitProject,
|
|
58
58
|
validatePackageInputs: () => validatePackageInputs,
|
|
59
59
|
validateProjectPaths: () => validateProjectPaths,
|
|
60
|
+
validateReactManifestParity: () => validateReactManifestParity,
|
|
60
61
|
writeLxpackProject: () => writeLxpackProject
|
|
61
62
|
});
|
|
62
63
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -374,6 +375,10 @@ var validateMcqLike = (assessment, path, issues) => {
|
|
|
374
375
|
} else if (trimmedChoices.length && !trimmedChoices.includes(assessment.answer.trim())) {
|
|
375
376
|
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
376
377
|
}
|
|
378
|
+
const uniqueChoices = new Set(trimmedChoices);
|
|
379
|
+
if (trimmedChoices.length !== uniqueChoices.size) {
|
|
380
|
+
issues.push({ path: `${path}.choices`, message: "choices must be unique" });
|
|
381
|
+
}
|
|
377
382
|
};
|
|
378
383
|
function countStarDelimitedBlanks(template) {
|
|
379
384
|
const matches = template.match(/\*[^*]+\*/g);
|
|
@@ -641,15 +646,8 @@ function assessmentDescriptorToLxpack(assessment) {
|
|
|
641
646
|
if (kind === "fillInBlanks") {
|
|
642
647
|
return null;
|
|
643
648
|
}
|
|
644
|
-
if (kind === "findHotspot"
|
|
645
|
-
return
|
|
646
|
-
kind: "mcq",
|
|
647
|
-
checkId: assessment.checkId,
|
|
648
|
-
question: assessment.question,
|
|
649
|
-
choices: [assessment.correctTargetId, "other"],
|
|
650
|
-
answer: assessment.correctTargetId,
|
|
651
|
-
passingScore: assessment.passingScore
|
|
652
|
-
});
|
|
649
|
+
if (kind === "findHotspot") {
|
|
650
|
+
return null;
|
|
653
651
|
}
|
|
654
652
|
if (kind === "findMultipleHotspots") {
|
|
655
653
|
return null;
|
|
@@ -663,6 +661,20 @@ function extractAssessments(descriptor) {
|
|
|
663
661
|
return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack).filter((a) => a !== null);
|
|
664
662
|
}
|
|
665
663
|
|
|
664
|
+
// src/descriptor/validateInjectableAssessments.ts
|
|
665
|
+
function validateInjectableAssessments(descriptor) {
|
|
666
|
+
const issues = [];
|
|
667
|
+
(descriptor.assessments ?? []).forEach((assessment, index) => {
|
|
668
|
+
if (assessmentDescriptorToLxpack(assessment) === null) {
|
|
669
|
+
issues.push({
|
|
670
|
+
path: `assessments[${index}]`,
|
|
671
|
+
message: `assessment kind "${assessment.kind ?? "mcq"}" (checkId "${assessment.checkId}") is not injected into LMS shell quizzes`
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
return issues;
|
|
676
|
+
}
|
|
677
|
+
|
|
666
678
|
// src/descriptor/validateForTarget.ts
|
|
667
679
|
var LMS_SHELL_TARGETS = /* @__PURE__ */ new Set([
|
|
668
680
|
"scorm12",
|
|
@@ -677,20 +689,21 @@ function validateDescriptorForExportTarget(descriptor, target) {
|
|
|
677
689
|
const activityIri = descriptor.tracking?.xapi?.activityIri?.trim();
|
|
678
690
|
if (!activityIri) {
|
|
679
691
|
issues.push({
|
|
680
|
-
path: "
|
|
692
|
+
path: "tracking.xapi.activityIri",
|
|
681
693
|
message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
|
|
682
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
|
+
});
|
|
683
700
|
}
|
|
684
701
|
}
|
|
685
702
|
if (LMS_SHELL_TARGETS.has(target)) {
|
|
686
|
-
(descriptor
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
message: `assessment kind "${assessment.kind ?? "mcq"}" (checkId "${assessment.checkId}") is not injected into LMS shell quizzes for target "${target}"`
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
});
|
|
703
|
+
issues.push(...validateInjectableAssessments(descriptor).map((issue) => ({
|
|
704
|
+
...issue,
|
|
705
|
+
message: `${issue.message} for target "${target}"`
|
|
706
|
+
})));
|
|
694
707
|
}
|
|
695
708
|
return issues;
|
|
696
709
|
}
|
|
@@ -718,8 +731,122 @@ function validateDescriptorForTarget(input, target) {
|
|
|
718
731
|
return result;
|
|
719
732
|
}
|
|
720
733
|
|
|
721
|
-
// src/
|
|
734
|
+
// src/validateReactParity.ts
|
|
735
|
+
var import_node_fs2 = require("fs");
|
|
722
736
|
var import_node_path2 = require("path");
|
|
737
|
+
var SCANNABLE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
738
|
+
function collectSourceUnderSrc(projectRoot) {
|
|
739
|
+
const srcDir = (0, import_node_path2.join)(projectRoot, "src");
|
|
740
|
+
if (!(0, import_node_fs2.existsSync)(srcDir)) return [];
|
|
741
|
+
const results = [];
|
|
742
|
+
const walk = (dir) => {
|
|
743
|
+
for (const entry of (0, import_node_fs2.readdirSync)(dir)) {
|
|
744
|
+
const abs = (0, import_node_path2.join)(dir, entry);
|
|
745
|
+
if ((0, import_node_fs2.statSync)(abs).isDirectory()) {
|
|
746
|
+
walk(abs);
|
|
747
|
+
} else if (SCANNABLE_EXTENSIONS.some((ext) => entry.endsWith(ext))) {
|
|
748
|
+
results.push((0, import_node_path2.relative)(projectRoot, abs));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
walk(srcDir);
|
|
753
|
+
return results;
|
|
754
|
+
}
|
|
755
|
+
function readAppSources(projectRoot, appSources) {
|
|
756
|
+
return appSources.map((rel) => (0, import_node_path2.join)(projectRoot, rel)).filter((abs) => (0, import_node_fs2.existsSync)(abs)).map((abs) => (0, import_node_fs2.readFileSync)(abs, "utf8")).join("\n");
|
|
757
|
+
}
|
|
758
|
+
function stripComments(source) {
|
|
759
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
|
|
760
|
+
}
|
|
761
|
+
function idPropPatterns(prop, id) {
|
|
762
|
+
return [
|
|
763
|
+
`${prop}="${id}"`,
|
|
764
|
+
`${prop}='${id}'`,
|
|
765
|
+
`${prop}={'${id}'}`,
|
|
766
|
+
`${prop}={"${id}"}`,
|
|
767
|
+
`${prop}={\`${id}\`}`
|
|
768
|
+
];
|
|
769
|
+
}
|
|
770
|
+
function extractStringConstants(source) {
|
|
771
|
+
const stripped = stripComments(source);
|
|
772
|
+
const map = /* @__PURE__ */ new Map();
|
|
773
|
+
const re = /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(["'`])((?:\\.|(?!\2).)*)\2/g;
|
|
774
|
+
for (const match of stripped.matchAll(re)) {
|
|
775
|
+
map.set(match[1], match[3]);
|
|
776
|
+
}
|
|
777
|
+
return map;
|
|
778
|
+
}
|
|
779
|
+
function idUsedViaConstant(stripped, prop, id, constants) {
|
|
780
|
+
for (const [name, value] of constants) {
|
|
781
|
+
if (value !== id) continue;
|
|
782
|
+
const jsxPatterns = [
|
|
783
|
+
`${prop}={${name}}`,
|
|
784
|
+
`${prop}={ ${name} }`,
|
|
785
|
+
`${prop}={${name} }`,
|
|
786
|
+
`${prop}={ ${name}}`
|
|
787
|
+
];
|
|
788
|
+
if (jsxPatterns.some((p) => stripped.includes(p))) return true;
|
|
789
|
+
const objPatterns = [`${prop}: ${name}`, `${prop}:${name}`];
|
|
790
|
+
if (objPatterns.some((p) => stripped.includes(p))) return true;
|
|
791
|
+
}
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
function courseIdPresent(source, courseId) {
|
|
795
|
+
const stripped = stripComments(source);
|
|
796
|
+
if (idPropPatterns("courseId", courseId).some((p) => stripped.includes(p))) return true;
|
|
797
|
+
return idUsedViaConstant(stripped, "courseId", courseId, extractStringConstants(source));
|
|
798
|
+
}
|
|
799
|
+
function checkIdPresent(source, checkId) {
|
|
800
|
+
const stripped = stripComments(source);
|
|
801
|
+
if (idPropPatterns("checkId", checkId).some((p) => stripped.includes(p))) return true;
|
|
802
|
+
return idUsedViaConstant(stripped, "checkId", checkId, extractStringConstants(source));
|
|
803
|
+
}
|
|
804
|
+
var ID_SYNC_DOC = "https://lessonkit.readthedocs.io/en/latest/guides/react-developers/quickstart.html#keep-react-ids-in-sync-with-lessonkitjson";
|
|
805
|
+
function parityHint(message) {
|
|
806
|
+
return `${message} See ${ID_SYNC_DOC}`;
|
|
807
|
+
}
|
|
808
|
+
function validateReactManifestParity(opts) {
|
|
809
|
+
const appSources = opts.appSources ?? collectSourceUnderSrc(opts.projectRoot);
|
|
810
|
+
const source = readAppSources(opts.projectRoot, appSources);
|
|
811
|
+
const hasDescriptorIds = Boolean(opts.descriptor.courseId) || (opts.descriptor.assessments?.length ?? 0) > 0;
|
|
812
|
+
if (!source.trim()) {
|
|
813
|
+
return [
|
|
814
|
+
{
|
|
815
|
+
path: appSources.length > 0 ? appSources.join(", ") : "src/",
|
|
816
|
+
message: hasDescriptorIds ? "React app source not found for ID parity check" : "React app source not found for ID parity check",
|
|
817
|
+
severity: hasDescriptorIds ? "error" : "warning"
|
|
818
|
+
}
|
|
819
|
+
];
|
|
820
|
+
}
|
|
821
|
+
const issues = [];
|
|
822
|
+
const courseId = opts.descriptor.courseId;
|
|
823
|
+
if (!courseIdPresent(source, courseId)) {
|
|
824
|
+
issues.push({
|
|
825
|
+
path: "course.courseId",
|
|
826
|
+
message: parityHint(
|
|
827
|
+
`React app source does not reference courseId="${courseId}" from lessonkit.json.`
|
|
828
|
+
),
|
|
829
|
+
severity: "error"
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
for (const assessment of opts.descriptor.assessments ?? []) {
|
|
833
|
+
const checkId = assessment.checkId;
|
|
834
|
+
if (!checkId) continue;
|
|
835
|
+
if (!checkIdPresent(source, checkId)) {
|
|
836
|
+
issues.push({
|
|
837
|
+
path: `assessments.checkId:${checkId}`,
|
|
838
|
+
message: parityHint(
|
|
839
|
+
`React app source missing checkId="${checkId}" declared in lessonkit.json.`
|
|
840
|
+
),
|
|
841
|
+
severity: "error"
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return issues;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// src/validateProjectPaths.ts
|
|
849
|
+
var import_node_path3 = require("path");
|
|
723
850
|
function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
724
851
|
if (!isSafeRelativeSpaPath(value)) {
|
|
725
852
|
issues.push({
|
|
@@ -729,7 +856,7 @@ function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
|
729
856
|
return;
|
|
730
857
|
}
|
|
731
858
|
try {
|
|
732
|
-
assertRealPathUnderRoot(projectRoot, (0,
|
|
859
|
+
assertRealPathUnderRoot(projectRoot, (0, import_node_path3.resolve)(projectRoot, value));
|
|
733
860
|
} catch {
|
|
734
861
|
issues.push({
|
|
735
862
|
path: fieldPath,
|
|
@@ -739,7 +866,7 @@ function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
|
739
866
|
}
|
|
740
867
|
function validateProjectPaths(projectRoot, paths) {
|
|
741
868
|
const issues = [];
|
|
742
|
-
const root = (0,
|
|
869
|
+
const root = (0, import_node_path3.resolve)(projectRoot);
|
|
743
870
|
if (paths.spaDistDir?.trim()) {
|
|
744
871
|
validatePathField(paths.spaDistDir.trim(), "paths.spaDistDir", root, issues);
|
|
745
872
|
}
|
|
@@ -752,20 +879,20 @@ function validateProjectPaths(projectRoot, paths) {
|
|
|
752
879
|
return issues;
|
|
753
880
|
}
|
|
754
881
|
function resolveSafePackageOutputOverride(projectRoot, override) {
|
|
755
|
-
const root = (0,
|
|
882
|
+
const root = (0, import_node_path3.resolve)(projectRoot);
|
|
756
883
|
const trimmed = override.trim();
|
|
757
884
|
if (!trimmed) {
|
|
758
885
|
throw new Error("output override must be a non-empty path");
|
|
759
886
|
}
|
|
760
|
-
if ((0,
|
|
761
|
-
const resolved2 = (0,
|
|
887
|
+
if ((0, import_node_path3.isAbsolute)(trimmed)) {
|
|
888
|
+
const resolved2 = (0, import_node_path3.resolve)(trimmed);
|
|
762
889
|
assertRealPathUnderRoot(root, resolved2);
|
|
763
890
|
return resolved2;
|
|
764
891
|
}
|
|
765
892
|
if (!isSafeRelativeSpaPath(trimmed)) {
|
|
766
893
|
throw new Error(`unsafe output path: ${override}`);
|
|
767
894
|
}
|
|
768
|
-
const resolved = (0,
|
|
895
|
+
const resolved = (0, import_node_path3.resolve)(root, trimmed);
|
|
769
896
|
assertRealPathUnderRoot(root, resolved);
|
|
770
897
|
return resolved;
|
|
771
898
|
}
|
|
@@ -841,21 +968,21 @@ function descriptorToInterchange(descriptor) {
|
|
|
841
968
|
}
|
|
842
969
|
|
|
843
970
|
// src/writeProject.ts
|
|
844
|
-
var
|
|
971
|
+
var import_node_path6 = require("path");
|
|
845
972
|
var import_validators = require("@lxpack/validators");
|
|
846
973
|
|
|
847
974
|
// src/spaDirs.ts
|
|
848
975
|
var import_promises = require("fs/promises");
|
|
849
|
-
var
|
|
976
|
+
var import_node_path4 = require("path");
|
|
850
977
|
async function resolveSpaDirs(options) {
|
|
851
978
|
const { descriptor, spaDistDir, lessonSpaDirs, projectRoot } = options;
|
|
852
979
|
const spaLessons = resolveSpaLessons(descriptor);
|
|
853
980
|
if (descriptor.layout === "single-spa") {
|
|
854
981
|
const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? /* v8 ignore next */
|
|
855
982
|
"dist";
|
|
856
|
-
const srcDist = projectRoot ? (0,
|
|
983
|
+
const srcDist = projectRoot ? (0, import_node_path4.resolve)(projectRoot, spaDistRelative) : (0, import_node_path4.resolve)(spaDistRelative);
|
|
857
984
|
if (projectRoot) {
|
|
858
|
-
assertRealPathUnderRoot((0,
|
|
985
|
+
assertRealPathUnderRoot((0, import_node_path4.resolve)(projectRoot), srcDist);
|
|
859
986
|
}
|
|
860
987
|
try {
|
|
861
988
|
await (0, import_promises.access)(srcDist);
|
|
@@ -863,9 +990,9 @@ async function resolveSpaDirs(options) {
|
|
|
863
990
|
throw new Error(`spaDistDir not found: ${srcDist}`);
|
|
864
991
|
}
|
|
865
992
|
try {
|
|
866
|
-
await (0, import_promises.access)((0,
|
|
993
|
+
await (0, import_promises.access)((0, import_node_path4.join)(srcDist, "index.html"));
|
|
867
994
|
} catch {
|
|
868
|
-
throw new Error(`spaDistDir must contain index.html: ${(0,
|
|
995
|
+
throw new Error(`spaDistDir must contain index.html: ${(0, import_node_path4.join)(srcDist, "index.html")}`);
|
|
869
996
|
}
|
|
870
997
|
const lessonId = spaLessons[0]?.id ?? /* v8 ignore next */
|
|
871
998
|
"main";
|
|
@@ -878,9 +1005,9 @@ async function resolveSpaDirs(options) {
|
|
|
878
1005
|
if (!src) {
|
|
879
1006
|
throw new Error(`lessonSpaDirs missing build output for lesson "${lesson.id}"`);
|
|
880
1007
|
}
|
|
881
|
-
const resolved = projectRoot ? (0,
|
|
1008
|
+
const resolved = projectRoot ? (0, import_node_path4.resolve)(projectRoot, src) : (0, import_node_path4.resolve)(src);
|
|
882
1009
|
if (projectRoot) {
|
|
883
|
-
assertRealPathUnderRoot((0,
|
|
1010
|
+
assertRealPathUnderRoot((0, import_node_path4.resolve)(projectRoot), resolved);
|
|
884
1011
|
}
|
|
885
1012
|
try {
|
|
886
1013
|
await (0, import_promises.access)(resolved);
|
|
@@ -888,10 +1015,10 @@ async function resolveSpaDirs(options) {
|
|
|
888
1015
|
throw new Error(`lessonSpaDirs path not found for lesson "${lesson.id}": ${resolved}`);
|
|
889
1016
|
}
|
|
890
1017
|
try {
|
|
891
|
-
await (0, import_promises.access)((0,
|
|
1018
|
+
await (0, import_promises.access)((0, import_node_path4.join)(resolved, "index.html"));
|
|
892
1019
|
} catch {
|
|
893
1020
|
throw new Error(
|
|
894
|
-
`lessonSpaDirs must contain index.html for lesson "${lesson.id}": ${(0,
|
|
1021
|
+
`lessonSpaDirs must contain index.html for lesson "${lesson.id}": ${(0, import_node_path4.join)(resolved, "index.html")}`
|
|
895
1022
|
);
|
|
896
1023
|
}
|
|
897
1024
|
dirs[lesson.id] = resolved;
|
|
@@ -899,6 +1026,59 @@ async function resolveSpaDirs(options) {
|
|
|
899
1026
|
return dirs;
|
|
900
1027
|
}
|
|
901
1028
|
|
|
1029
|
+
// src/spaDistValidation.ts
|
|
1030
|
+
var import_promises2 = require("fs/promises");
|
|
1031
|
+
var import_node_fs3 = require("fs");
|
|
1032
|
+
var import_node_path5 = require("path");
|
|
1033
|
+
async function assertSpaDistContentsSafe(spaDirs, projectRoot) {
|
|
1034
|
+
for (const [label, dir] of Object.entries(spaDirs)) {
|
|
1035
|
+
const dirResolved = resolveComparablePath(dir);
|
|
1036
|
+
const dirStat = await (0, import_promises2.lstat)(dirResolved);
|
|
1037
|
+
if (dirStat.isSymbolicLink()) {
|
|
1038
|
+
throw new Error(`spa dist for "${label}" cannot be a symlink: ${dir}`);
|
|
1039
|
+
}
|
|
1040
|
+
let rootReal;
|
|
1041
|
+
try {
|
|
1042
|
+
rootReal = (0, import_node_fs3.realpathSync)(dirResolved);
|
|
1043
|
+
} catch {
|
|
1044
|
+
throw new Error(`spa dist for "${label}" is not readable: ${dir}`);
|
|
1045
|
+
}
|
|
1046
|
+
if (projectRoot) {
|
|
1047
|
+
assertRealPathUnderRoot(projectRoot, dir);
|
|
1048
|
+
}
|
|
1049
|
+
assertResolvedPathUnderRoot(rootReal, rootReal);
|
|
1050
|
+
await walkDistDir(rootReal, rootReal, label);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
async function walkDistDir(rootReal, current, label) {
|
|
1054
|
+
let entries;
|
|
1055
|
+
try {
|
|
1056
|
+
entries = await (0, import_promises2.readdir)(current, { withFileTypes: true });
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
throw new Error(
|
|
1059
|
+
`spa dist for "${label}" is not readable: ${err instanceof Error ? err.message : String(err)}`,
|
|
1060
|
+
{ cause: err }
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
for (const entry of entries) {
|
|
1064
|
+
const entryPath = (0, import_node_path5.join)(current, entry.name);
|
|
1065
|
+
const stat2 = await (0, import_promises2.lstat)(entryPath);
|
|
1066
|
+
if (stat2.isSymbolicLink()) {
|
|
1067
|
+
throw new Error(`spa dist for "${label}" contains symlink: ${entryPath}`);
|
|
1068
|
+
}
|
|
1069
|
+
let entryReal;
|
|
1070
|
+
try {
|
|
1071
|
+
entryReal = (0, import_node_fs3.realpathSync)(entryPath);
|
|
1072
|
+
} catch {
|
|
1073
|
+
entryReal = entryPath;
|
|
1074
|
+
}
|
|
1075
|
+
assertResolvedPathUnderRoot(rootReal, entryReal);
|
|
1076
|
+
if (stat2.isDirectory()) {
|
|
1077
|
+
await walkDistDir(rootReal, entryPath, label);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
902
1082
|
// src/writeProject.ts
|
|
903
1083
|
async function writeLxpackProject(options) {
|
|
904
1084
|
const validation = validateDescriptor(options.descriptor);
|
|
@@ -908,11 +1088,16 @@ async function writeLxpackProject(options) {
|
|
|
908
1088
|
);
|
|
909
1089
|
}
|
|
910
1090
|
const descriptor = validation.descriptor;
|
|
911
|
-
const
|
|
1091
|
+
const injectableIssues = validateInjectableAssessments(descriptor);
|
|
1092
|
+
if (injectableIssues.length > 0) {
|
|
1093
|
+
throw new Error(injectableIssues.map((i) => `${i.path}: ${i.message}`).join("; "));
|
|
1094
|
+
}
|
|
1095
|
+
const outDir = (0, import_node_path6.resolve)(options.outDir);
|
|
912
1096
|
if (options.projectRoot) {
|
|
913
|
-
assertRealPathUnderRoot((0,
|
|
1097
|
+
assertRealPathUnderRoot((0, import_node_path6.resolve)(options.projectRoot), outDir);
|
|
914
1098
|
}
|
|
915
1099
|
const spaDirs = await resolveSpaDirs({ ...options, descriptor });
|
|
1100
|
+
await assertSpaDistContentsSafe(spaDirs, options.projectRoot);
|
|
916
1101
|
const interchange = descriptorToInterchange(descriptor);
|
|
917
1102
|
const materialized = await (0, import_validators.materializeLessonkitProject)({
|
|
918
1103
|
interchange,
|
|
@@ -928,21 +1113,21 @@ async function writeLxpackProject(options) {
|
|
|
928
1113
|
const courseDir = materialized.courseDir;
|
|
929
1114
|
return {
|
|
930
1115
|
outDir: courseDir,
|
|
931
|
-
courseYamlPath: (0,
|
|
932
|
-
lessonkitJsonPath: (0,
|
|
1116
|
+
courseYamlPath: (0, import_node_path6.join)(courseDir, "course.yaml"),
|
|
1117
|
+
lessonkitJsonPath: (0, import_node_path6.join)(courseDir, "lessonkit.json")
|
|
933
1118
|
};
|
|
934
1119
|
}
|
|
935
1120
|
|
|
936
1121
|
// src/packageCourse.ts
|
|
937
|
-
var
|
|
1122
|
+
var import_node_path10 = require("path");
|
|
938
1123
|
var fsp3 = __toESM(require("fs/promises"), 1);
|
|
939
1124
|
var import_api2 = require("@lxpack/api");
|
|
940
1125
|
|
|
941
1126
|
// src/packaging/validateInputs.ts
|
|
942
|
-
var
|
|
1127
|
+
var import_node_path7 = require("path");
|
|
943
1128
|
function validatePackageInputs(options) {
|
|
944
1129
|
const { target, output, outputBaseDir } = options;
|
|
945
|
-
const outDir = (0,
|
|
1130
|
+
const outDir = (0, import_node_path7.resolve)(options.outDir);
|
|
946
1131
|
if (!options.projectRoot) {
|
|
947
1132
|
return {
|
|
948
1133
|
ok: false,
|
|
@@ -951,7 +1136,7 @@ function validatePackageInputs(options) {
|
|
|
951
1136
|
issues: [{ path: "projectRoot", message: "projectRoot is required for packageLessonkitCourse" }]
|
|
952
1137
|
};
|
|
953
1138
|
}
|
|
954
|
-
const projectRoot = (0,
|
|
1139
|
+
const projectRoot = (0, import_node_path7.resolve)(options.projectRoot);
|
|
955
1140
|
try {
|
|
956
1141
|
assertRealPathUnderRoot(projectRoot, outDir);
|
|
957
1142
|
} catch (err) {
|
|
@@ -979,9 +1164,9 @@ function validatePackageInputs(options) {
|
|
|
979
1164
|
};
|
|
980
1165
|
}
|
|
981
1166
|
if (output && !isSafeRelativeSpaPath(output)) {
|
|
982
|
-
if ((0,
|
|
1167
|
+
if ((0, import_node_path7.isAbsolute)(output)) {
|
|
983
1168
|
try {
|
|
984
|
-
assertRealPathUnderRoot(projectRoot, (0,
|
|
1169
|
+
assertRealPathUnderRoot(projectRoot, (0, import_node_path7.resolve)(output));
|
|
985
1170
|
} catch (err) {
|
|
986
1171
|
return {
|
|
987
1172
|
ok: false,
|
|
@@ -1008,7 +1193,7 @@ function validatePackageInputs(options) {
|
|
|
1008
1193
|
}
|
|
1009
1194
|
}
|
|
1010
1195
|
if (outputBaseDir) {
|
|
1011
|
-
const resolvedOutputBase = (0,
|
|
1196
|
+
const resolvedOutputBase = (0, import_node_path7.resolve)(projectRoot, outputBaseDir);
|
|
1012
1197
|
try {
|
|
1013
1198
|
assertRealPathUnderRoot(projectRoot, resolvedOutputBase);
|
|
1014
1199
|
} catch (err) {
|
|
@@ -1029,7 +1214,7 @@ function validatePackageInputs(options) {
|
|
|
1029
1214
|
}
|
|
1030
1215
|
}
|
|
1031
1216
|
if (output) {
|
|
1032
|
-
const resolvedOutput = (0,
|
|
1217
|
+
const resolvedOutput = (0, import_node_path7.isAbsolute)(output) ? (0, import_node_path7.resolve)(output) : (0, import_node_path7.resolve)(projectRoot, output);
|
|
1033
1218
|
try {
|
|
1034
1219
|
assertRealPathUnderRoot(projectRoot, resolvedOutput);
|
|
1035
1220
|
} catch (err) {
|
|
@@ -1069,20 +1254,20 @@ function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
|
|
|
1069
1254
|
throw new Error(`${artifactPath} is outside the staging directory`);
|
|
1070
1255
|
}
|
|
1071
1256
|
const rel = relativePathUnderRoot(stagingRoot, resolved);
|
|
1072
|
-
if (rel.startsWith("..") || (0,
|
|
1257
|
+
if (rel.startsWith("..") || (0, import_node_path7.isAbsolute)(rel)) {
|
|
1073
1258
|
throw new Error(`${artifactPath} is outside the staging directory`);
|
|
1074
1259
|
}
|
|
1075
1260
|
if (!rel) return outDir;
|
|
1076
1261
|
if (/^[a-zA-Z]:[/\\]/.test(outDir)) {
|
|
1077
|
-
return
|
|
1262
|
+
return import_node_path7.win32.join(outDir, rel.replace(/\//g, import_node_path7.win32.sep));
|
|
1078
1263
|
}
|
|
1079
|
-
return (0,
|
|
1264
|
+
return (0, import_node_path7.join)(outDir, rel);
|
|
1080
1265
|
}
|
|
1081
1266
|
|
|
1082
1267
|
// src/packaging/promote.ts
|
|
1083
1268
|
var fsp = __toESM(require("fs/promises"), 1);
|
|
1084
1269
|
var import_node_crypto = require("crypto");
|
|
1085
|
-
var
|
|
1270
|
+
var import_node_path8 = require("path");
|
|
1086
1271
|
async function pathExists(path) {
|
|
1087
1272
|
try {
|
|
1088
1273
|
await fsp.access(path);
|
|
@@ -1102,31 +1287,32 @@ async function renameOrCopy(from, to) {
|
|
|
1102
1287
|
}
|
|
1103
1288
|
}
|
|
1104
1289
|
function promoteLockPath(outDir) {
|
|
1105
|
-
const parent = (0,
|
|
1106
|
-
const hash = (0, import_node_crypto.createHash)("sha256").update((0,
|
|
1107
|
-
return (0,
|
|
1290
|
+
const parent = (0, import_node_path8.dirname)(outDir);
|
|
1291
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update((0, import_node_path8.resolve)(outDir)).digest("hex").slice(0, 16);
|
|
1292
|
+
return (0, import_node_path8.join)(parent, `.lk-promote-lock-${hash}`);
|
|
1108
1293
|
}
|
|
1109
1294
|
var STALE_LOCK_TTL_MS = 5 * 60 * 1e3;
|
|
1110
1295
|
async function isStalePromoteLock(lockPath) {
|
|
1111
1296
|
try {
|
|
1112
|
-
const stat2 = await fsp.stat(lockPath);
|
|
1113
|
-
if (Date.now() - stat2.mtimeMs > STALE_LOCK_TTL_MS) return true;
|
|
1114
1297
|
const content = await fsp.readFile(lockPath, "utf8");
|
|
1115
1298
|
const pid = Number.parseInt(content.trim(), 10);
|
|
1116
|
-
if (
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1299
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
1300
|
+
try {
|
|
1301
|
+
process.kill(pid, 0);
|
|
1302
|
+
return false;
|
|
1303
|
+
} catch {
|
|
1304
|
+
return true;
|
|
1305
|
+
}
|
|
1122
1306
|
}
|
|
1307
|
+
const stat2 = await fsp.stat(lockPath);
|
|
1308
|
+
return Date.now() - stat2.mtimeMs > STALE_LOCK_TTL_MS;
|
|
1123
1309
|
} catch {
|
|
1124
1310
|
return true;
|
|
1125
1311
|
}
|
|
1126
1312
|
}
|
|
1127
1313
|
async function withPromoteLock(outDir, fn) {
|
|
1128
1314
|
const lockPath = promoteLockPath(outDir);
|
|
1129
|
-
await fsp.mkdir((0,
|
|
1315
|
+
await fsp.mkdir((0, import_node_path8.dirname)(outDir), { recursive: true });
|
|
1130
1316
|
let lockHandle;
|
|
1131
1317
|
for (let attempt = 0; attempt < 200; attempt++) {
|
|
1132
1318
|
try {
|
|
@@ -1178,11 +1364,11 @@ async function assertNoLegacyPromoteArtifacts(outDir) {
|
|
|
1178
1364
|
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
1179
1365
|
return withPromoteLock(outDir, async () => {
|
|
1180
1366
|
await assertNoLegacyPromoteArtifacts(outDir);
|
|
1181
|
-
const parent = (0,
|
|
1182
|
-
const tmpPromote = await fsp.mkdtemp((0,
|
|
1367
|
+
const parent = (0, import_node_path8.dirname)(outDir);
|
|
1368
|
+
const tmpPromote = await fsp.mkdtemp((0, import_node_path8.join)(parent, ".lk-promote-"));
|
|
1183
1369
|
await renameOrCopy(stagingDir, tmpPromote);
|
|
1184
1370
|
const hadOutDir = await pathExists(outDir);
|
|
1185
|
-
const backup = hadOutDir ? await fsp.mkdtemp((0,
|
|
1371
|
+
const backup = hadOutDir ? await fsp.mkdtemp((0, import_node_path8.join)(parent, ".lk-backup-")) : void 0;
|
|
1186
1372
|
if (hadOutDir && backup) {
|
|
1187
1373
|
await renameOrCopy(outDir, backup);
|
|
1188
1374
|
}
|
|
@@ -1193,7 +1379,7 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1193
1379
|
try {
|
|
1194
1380
|
await renameOrCopy(backup, outDir);
|
|
1195
1381
|
} catch (restoreError) {
|
|
1196
|
-
const failedPromote2 = (0,
|
|
1382
|
+
const failedPromote2 = (0, import_node_path8.join)(parent, `.lk-failed-promote-${(0, import_node_crypto.randomUUID)()}`);
|
|
1197
1383
|
try {
|
|
1198
1384
|
await renameOrCopy(tmpPromote, failedPromote2);
|
|
1199
1385
|
} catch {
|
|
@@ -1205,7 +1391,8 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1205
1391
|
const promoteMsg = promoteError instanceof Error ? promoteError.message : String(promoteError);
|
|
1206
1392
|
const restoreMsg = restoreError instanceof Error ? restoreError.message : String(restoreError);
|
|
1207
1393
|
throw new Error(
|
|
1208
|
-
`[lessonkit/lxpack] promote failed (${promoteMsg}) and could not restore ${outDir} (${restoreMsg}). Recovery: previous output may be in ${backup}; staged package may be in ${failedPromote2}
|
|
1394
|
+
`[lessonkit/lxpack] promote failed (${promoteMsg}) and could not restore ${outDir} (${restoreMsg}). Recovery: previous output may be in ${backup}; staged package may be in ${failedPromote2}.`,
|
|
1395
|
+
{ cause: restoreError }
|
|
1209
1396
|
);
|
|
1210
1397
|
}
|
|
1211
1398
|
} else {
|
|
@@ -1223,7 +1410,7 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1223
1410
|
}
|
|
1224
1411
|
throw promoteError;
|
|
1225
1412
|
}
|
|
1226
|
-
const failedPromote = (0,
|
|
1413
|
+
const failedPromote = (0, import_node_path8.join)(parent, `.lk-failed-promote-${(0, import_node_crypto.randomUUID)()}`);
|
|
1227
1414
|
try {
|
|
1228
1415
|
await renameOrCopy(tmpPromote, failedPromote);
|
|
1229
1416
|
} catch {
|
|
@@ -1245,16 +1432,17 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1245
1432
|
|
|
1246
1433
|
// src/packaging/staging.ts
|
|
1247
1434
|
var fsp2 = __toESM(require("fs/promises"), 1);
|
|
1248
|
-
var
|
|
1435
|
+
var import_node_path9 = require("path");
|
|
1249
1436
|
var import_node_os = require("os");
|
|
1250
1437
|
var import_api = require("@lxpack/api");
|
|
1251
1438
|
async function buildStagingPackage(options) {
|
|
1252
1439
|
const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
|
|
1253
|
-
const stagingDir = await fsp2.mkdtemp((0,
|
|
1440
|
+
const stagingDir = await fsp2.mkdtemp((0, import_node_path9.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
|
|
1254
1441
|
try {
|
|
1255
1442
|
let spaDirs;
|
|
1256
1443
|
try {
|
|
1257
1444
|
spaDirs = await resolveSpaDirs({ ...writeOpts, descriptor });
|
|
1445
|
+
await assertSpaDistContentsSafe(spaDirs, writeOpts.projectRoot);
|
|
1258
1446
|
} catch (err) {
|
|
1259
1447
|
return {
|
|
1260
1448
|
ok: false,
|
|
@@ -1267,10 +1455,21 @@ async function buildStagingPackage(options) {
|
|
|
1267
1455
|
]
|
|
1268
1456
|
};
|
|
1269
1457
|
}
|
|
1458
|
+
const injectableIssues = validateInjectableAssessments(descriptor);
|
|
1459
|
+
if (injectableIssues.length > 0) {
|
|
1460
|
+
return {
|
|
1461
|
+
ok: false,
|
|
1462
|
+
stagingDir,
|
|
1463
|
+
issues: injectableIssues.map((i) => ({
|
|
1464
|
+
path: i.path,
|
|
1465
|
+
message: i.message
|
|
1466
|
+
}))
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1270
1469
|
const interchange = descriptorToInterchange(descriptor);
|
|
1271
1470
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
1272
|
-
await fsp2.mkdir((0,
|
|
1273
|
-
const defaultOutput = output ?? (dir ? (0,
|
|
1471
|
+
await fsp2.mkdir((0, import_node_path9.join)(stagingDir, outputBase), { recursive: true });
|
|
1472
|
+
const defaultOutput = output ?? (dir ? (0, import_node_path9.join)(outputBase, target) : (0, import_node_path9.join)(outputBase, `course-${target}.zip`));
|
|
1274
1473
|
const build = await (0, import_api.packageLessonkit)({
|
|
1275
1474
|
interchange,
|
|
1276
1475
|
spaDirs,
|
|
@@ -1310,7 +1509,7 @@ async function buildStagingPackage(options) {
|
|
|
1310
1509
|
}
|
|
1311
1510
|
}
|
|
1312
1511
|
async function ensureOutDirParent(outDir) {
|
|
1313
|
-
await fsp2.mkdir((0,
|
|
1512
|
+
await fsp2.mkdir((0, import_node_path9.dirname)(outDir), { recursive: true });
|
|
1314
1513
|
}
|
|
1315
1514
|
|
|
1316
1515
|
// src/packaging/issueSeverity.ts
|
|
@@ -1325,13 +1524,13 @@ function findPackagingErrorIssues(issues) {
|
|
|
1325
1524
|
// src/packageCourse.ts
|
|
1326
1525
|
async function validateLessonkitProject(options) {
|
|
1327
1526
|
return (0, import_api2.validateCourse)({
|
|
1328
|
-
courseDir: (0,
|
|
1527
|
+
courseDir: (0, import_node_path10.resolve)(options.courseDir),
|
|
1329
1528
|
target: options.target
|
|
1330
1529
|
});
|
|
1331
1530
|
}
|
|
1332
1531
|
async function buildLessonkitProject(options) {
|
|
1333
1532
|
const buildOptions = {
|
|
1334
|
-
courseDir: (0,
|
|
1533
|
+
courseDir: (0, import_node_path10.resolve)(options.courseDir),
|
|
1335
1534
|
target: options.target,
|
|
1336
1535
|
output: options.output,
|
|
1337
1536
|
dir: options.dir,
|
|
@@ -1362,7 +1561,7 @@ async function packageLessonkitCourse(options) {
|
|
|
1362
1561
|
if (!descriptorValidation.ok) {
|
|
1363
1562
|
return {
|
|
1364
1563
|
ok: false,
|
|
1365
|
-
courseDir: (0,
|
|
1564
|
+
courseDir: (0, import_node_path10.resolve)(writeOpts.outDir),
|
|
1366
1565
|
target,
|
|
1367
1566
|
issues: descriptorValidation.issues.map((i) => ({
|
|
1368
1567
|
path: i.path,
|
|
@@ -1371,17 +1570,24 @@ async function packageLessonkitCourse(options) {
|
|
|
1371
1570
|
};
|
|
1372
1571
|
}
|
|
1373
1572
|
const descriptor = descriptorValidation.descriptor;
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1573
|
+
if (writeOpts.projectRoot) {
|
|
1574
|
+
const parityIssues = validateReactManifestParity({
|
|
1575
|
+
projectRoot: writeOpts.projectRoot,
|
|
1576
|
+
descriptor
|
|
1577
|
+
});
|
|
1578
|
+
const parityErrors = parityIssues.filter((i) => i.severity === "error");
|
|
1579
|
+
if (parityErrors.length > 0) {
|
|
1580
|
+
return {
|
|
1581
|
+
ok: false,
|
|
1582
|
+
courseDir: outDir,
|
|
1583
|
+
target,
|
|
1584
|
+
issues: parityErrors.map((i) => ({
|
|
1585
|
+
path: i.path,
|
|
1586
|
+
message: i.message,
|
|
1587
|
+
severity: i.severity
|
|
1588
|
+
}))
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1385
1591
|
}
|
|
1386
1592
|
const staged = await buildStagingPackage({
|
|
1387
1593
|
...writeOpts,
|
|
@@ -1675,5 +1881,6 @@ var import_validators2 = require("@lxpack/validators");
|
|
|
1675
1881
|
validateLessonkitProject,
|
|
1676
1882
|
validatePackageInputs,
|
|
1677
1883
|
validateProjectPaths,
|
|
1884
|
+
validateReactManifestParity,
|
|
1678
1885
|
writeLxpackProject
|
|
1679
1886
|
});
|