@lessonkit/lxpack 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +186 -58
- package/dist/index.d.cts +18 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +155 -28
- package/package.json +3 -3
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);
|
|
@@ -718,8 +719,114 @@ function validateDescriptorForTarget(input, target) {
|
|
|
718
719
|
return result;
|
|
719
720
|
}
|
|
720
721
|
|
|
721
|
-
// src/
|
|
722
|
+
// src/validateReactParity.ts
|
|
723
|
+
var import_node_fs2 = require("fs");
|
|
722
724
|
var import_node_path2 = require("path");
|
|
725
|
+
var SCANNABLE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
726
|
+
function collectSourceUnderSrc(projectRoot) {
|
|
727
|
+
const srcDir = (0, import_node_path2.join)(projectRoot, "src");
|
|
728
|
+
if (!(0, import_node_fs2.existsSync)(srcDir)) return [];
|
|
729
|
+
const results = [];
|
|
730
|
+
const walk = (dir) => {
|
|
731
|
+
for (const entry of (0, import_node_fs2.readdirSync)(dir)) {
|
|
732
|
+
const abs = (0, import_node_path2.join)(dir, entry);
|
|
733
|
+
if ((0, import_node_fs2.statSync)(abs).isDirectory()) {
|
|
734
|
+
walk(abs);
|
|
735
|
+
} else if (SCANNABLE_EXTENSIONS.some((ext) => entry.endsWith(ext))) {
|
|
736
|
+
results.push((0, import_node_path2.relative)(projectRoot, abs));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
walk(srcDir);
|
|
741
|
+
return results;
|
|
742
|
+
}
|
|
743
|
+
function readAppSources(projectRoot, appSources) {
|
|
744
|
+
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");
|
|
745
|
+
}
|
|
746
|
+
function stripComments(source) {
|
|
747
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
|
|
748
|
+
}
|
|
749
|
+
function idPropPatterns(prop, id) {
|
|
750
|
+
return [
|
|
751
|
+
`${prop}="${id}"`,
|
|
752
|
+
`${prop}='${id}'`,
|
|
753
|
+
`${prop}={'${id}'}`,
|
|
754
|
+
`${prop}={"${id}"}`,
|
|
755
|
+
`${prop}={\`${id}\`}`
|
|
756
|
+
];
|
|
757
|
+
}
|
|
758
|
+
function extractStringConstants(source) {
|
|
759
|
+
const stripped = stripComments(source);
|
|
760
|
+
const map = /* @__PURE__ */ new Map();
|
|
761
|
+
const re = /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(["'`])((?:\\.|(?!\2).)*)\2/g;
|
|
762
|
+
for (const match of stripped.matchAll(re)) {
|
|
763
|
+
map.set(match[1], match[3]);
|
|
764
|
+
}
|
|
765
|
+
return map;
|
|
766
|
+
}
|
|
767
|
+
function idUsedViaConstant(stripped, prop, id, constants) {
|
|
768
|
+
for (const [name, value] of constants) {
|
|
769
|
+
if (value !== id) continue;
|
|
770
|
+
const jsxPatterns = [
|
|
771
|
+
`${prop}={${name}}`,
|
|
772
|
+
`${prop}={ ${name} }`,
|
|
773
|
+
`${prop}={${name} }`,
|
|
774
|
+
`${prop}={ ${name}}`
|
|
775
|
+
];
|
|
776
|
+
if (jsxPatterns.some((p) => stripped.includes(p))) return true;
|
|
777
|
+
const objPatterns = [`${prop}: ${name}`, `${prop}:${name}`];
|
|
778
|
+
if (objPatterns.some((p) => stripped.includes(p))) return true;
|
|
779
|
+
}
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
function courseIdPresent(source, courseId) {
|
|
783
|
+
const stripped = stripComments(source);
|
|
784
|
+
if (idPropPatterns("courseId", courseId).some((p) => stripped.includes(p))) return true;
|
|
785
|
+
return idUsedViaConstant(stripped, "courseId", courseId, extractStringConstants(source));
|
|
786
|
+
}
|
|
787
|
+
function checkIdPresent(source, checkId) {
|
|
788
|
+
const stripped = stripComments(source);
|
|
789
|
+
if (idPropPatterns("checkId", checkId).some((p) => stripped.includes(p))) return true;
|
|
790
|
+
return idUsedViaConstant(stripped, "checkId", checkId, extractStringConstants(source));
|
|
791
|
+
}
|
|
792
|
+
function validateReactManifestParity(opts) {
|
|
793
|
+
const appSources = opts.appSources ?? collectSourceUnderSrc(opts.projectRoot);
|
|
794
|
+
const source = readAppSources(opts.projectRoot, appSources);
|
|
795
|
+
const hasDescriptorIds = Boolean(opts.descriptor.courseId) || (opts.descriptor.assessments?.length ?? 0) > 0;
|
|
796
|
+
if (!source.trim()) {
|
|
797
|
+
return [
|
|
798
|
+
{
|
|
799
|
+
path: appSources.length > 0 ? appSources.join(", ") : "src/",
|
|
800
|
+
message: hasDescriptorIds ? "React app source not found for ID parity check" : "React app source not found for ID parity check",
|
|
801
|
+
severity: hasDescriptorIds ? "error" : "warning"
|
|
802
|
+
}
|
|
803
|
+
];
|
|
804
|
+
}
|
|
805
|
+
const issues = [];
|
|
806
|
+
const courseId = opts.descriptor.courseId;
|
|
807
|
+
if (!courseIdPresent(source, courseId)) {
|
|
808
|
+
issues.push({
|
|
809
|
+
path: "course.courseId",
|
|
810
|
+
message: `React app source does not reference courseId="${courseId}" from lessonkit.json`,
|
|
811
|
+
severity: "error"
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
for (const assessment of opts.descriptor.assessments ?? []) {
|
|
815
|
+
const checkId = assessment.checkId;
|
|
816
|
+
if (!checkId) continue;
|
|
817
|
+
if (!checkIdPresent(source, checkId)) {
|
|
818
|
+
issues.push({
|
|
819
|
+
path: `assessments.checkId:${checkId}`,
|
|
820
|
+
message: `React app source missing checkId="${checkId}" declared in lessonkit.json`,
|
|
821
|
+
severity: "error"
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return issues;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// src/validateProjectPaths.ts
|
|
829
|
+
var import_node_path3 = require("path");
|
|
723
830
|
function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
724
831
|
if (!isSafeRelativeSpaPath(value)) {
|
|
725
832
|
issues.push({
|
|
@@ -729,7 +836,7 @@ function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
|
729
836
|
return;
|
|
730
837
|
}
|
|
731
838
|
try {
|
|
732
|
-
assertRealPathUnderRoot(projectRoot, (0,
|
|
839
|
+
assertRealPathUnderRoot(projectRoot, (0, import_node_path3.resolve)(projectRoot, value));
|
|
733
840
|
} catch {
|
|
734
841
|
issues.push({
|
|
735
842
|
path: fieldPath,
|
|
@@ -739,7 +846,7 @@ function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
|
739
846
|
}
|
|
740
847
|
function validateProjectPaths(projectRoot, paths) {
|
|
741
848
|
const issues = [];
|
|
742
|
-
const root = (0,
|
|
849
|
+
const root = (0, import_node_path3.resolve)(projectRoot);
|
|
743
850
|
if (paths.spaDistDir?.trim()) {
|
|
744
851
|
validatePathField(paths.spaDistDir.trim(), "paths.spaDistDir", root, issues);
|
|
745
852
|
}
|
|
@@ -752,20 +859,20 @@ function validateProjectPaths(projectRoot, paths) {
|
|
|
752
859
|
return issues;
|
|
753
860
|
}
|
|
754
861
|
function resolveSafePackageOutputOverride(projectRoot, override) {
|
|
755
|
-
const root = (0,
|
|
862
|
+
const root = (0, import_node_path3.resolve)(projectRoot);
|
|
756
863
|
const trimmed = override.trim();
|
|
757
864
|
if (!trimmed) {
|
|
758
865
|
throw new Error("output override must be a non-empty path");
|
|
759
866
|
}
|
|
760
|
-
if ((0,
|
|
761
|
-
const resolved2 = (0,
|
|
867
|
+
if ((0, import_node_path3.isAbsolute)(trimmed)) {
|
|
868
|
+
const resolved2 = (0, import_node_path3.resolve)(trimmed);
|
|
762
869
|
assertRealPathUnderRoot(root, resolved2);
|
|
763
870
|
return resolved2;
|
|
764
871
|
}
|
|
765
872
|
if (!isSafeRelativeSpaPath(trimmed)) {
|
|
766
873
|
throw new Error(`unsafe output path: ${override}`);
|
|
767
874
|
}
|
|
768
|
-
const resolved = (0,
|
|
875
|
+
const resolved = (0, import_node_path3.resolve)(root, trimmed);
|
|
769
876
|
assertRealPathUnderRoot(root, resolved);
|
|
770
877
|
return resolved;
|
|
771
878
|
}
|
|
@@ -841,21 +948,21 @@ function descriptorToInterchange(descriptor) {
|
|
|
841
948
|
}
|
|
842
949
|
|
|
843
950
|
// src/writeProject.ts
|
|
844
|
-
var
|
|
951
|
+
var import_node_path5 = require("path");
|
|
845
952
|
var import_validators = require("@lxpack/validators");
|
|
846
953
|
|
|
847
954
|
// src/spaDirs.ts
|
|
848
955
|
var import_promises = require("fs/promises");
|
|
849
|
-
var
|
|
956
|
+
var import_node_path4 = require("path");
|
|
850
957
|
async function resolveSpaDirs(options) {
|
|
851
958
|
const { descriptor, spaDistDir, lessonSpaDirs, projectRoot } = options;
|
|
852
959
|
const spaLessons = resolveSpaLessons(descriptor);
|
|
853
960
|
if (descriptor.layout === "single-spa") {
|
|
854
961
|
const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? /* v8 ignore next */
|
|
855
962
|
"dist";
|
|
856
|
-
const srcDist = projectRoot ? (0,
|
|
963
|
+
const srcDist = projectRoot ? (0, import_node_path4.resolve)(projectRoot, spaDistRelative) : (0, import_node_path4.resolve)(spaDistRelative);
|
|
857
964
|
if (projectRoot) {
|
|
858
|
-
assertRealPathUnderRoot((0,
|
|
965
|
+
assertRealPathUnderRoot((0, import_node_path4.resolve)(projectRoot), srcDist);
|
|
859
966
|
}
|
|
860
967
|
try {
|
|
861
968
|
await (0, import_promises.access)(srcDist);
|
|
@@ -863,9 +970,9 @@ async function resolveSpaDirs(options) {
|
|
|
863
970
|
throw new Error(`spaDistDir not found: ${srcDist}`);
|
|
864
971
|
}
|
|
865
972
|
try {
|
|
866
|
-
await (0, import_promises.access)((0,
|
|
973
|
+
await (0, import_promises.access)((0, import_node_path4.join)(srcDist, "index.html"));
|
|
867
974
|
} catch {
|
|
868
|
-
throw new Error(`spaDistDir must contain index.html: ${(0,
|
|
975
|
+
throw new Error(`spaDistDir must contain index.html: ${(0, import_node_path4.join)(srcDist, "index.html")}`);
|
|
869
976
|
}
|
|
870
977
|
const lessonId = spaLessons[0]?.id ?? /* v8 ignore next */
|
|
871
978
|
"main";
|
|
@@ -878,9 +985,9 @@ async function resolveSpaDirs(options) {
|
|
|
878
985
|
if (!src) {
|
|
879
986
|
throw new Error(`lessonSpaDirs missing build output for lesson "${lesson.id}"`);
|
|
880
987
|
}
|
|
881
|
-
const resolved = projectRoot ? (0,
|
|
988
|
+
const resolved = projectRoot ? (0, import_node_path4.resolve)(projectRoot, src) : (0, import_node_path4.resolve)(src);
|
|
882
989
|
if (projectRoot) {
|
|
883
|
-
assertRealPathUnderRoot((0,
|
|
990
|
+
assertRealPathUnderRoot((0, import_node_path4.resolve)(projectRoot), resolved);
|
|
884
991
|
}
|
|
885
992
|
try {
|
|
886
993
|
await (0, import_promises.access)(resolved);
|
|
@@ -888,10 +995,10 @@ async function resolveSpaDirs(options) {
|
|
|
888
995
|
throw new Error(`lessonSpaDirs path not found for lesson "${lesson.id}": ${resolved}`);
|
|
889
996
|
}
|
|
890
997
|
try {
|
|
891
|
-
await (0, import_promises.access)((0,
|
|
998
|
+
await (0, import_promises.access)((0, import_node_path4.join)(resolved, "index.html"));
|
|
892
999
|
} catch {
|
|
893
1000
|
throw new Error(
|
|
894
|
-
`lessonSpaDirs must contain index.html for lesson "${lesson.id}": ${(0,
|
|
1001
|
+
`lessonSpaDirs must contain index.html for lesson "${lesson.id}": ${(0, import_node_path4.join)(resolved, "index.html")}`
|
|
895
1002
|
);
|
|
896
1003
|
}
|
|
897
1004
|
dirs[lesson.id] = resolved;
|
|
@@ -908,9 +1015,9 @@ async function writeLxpackProject(options) {
|
|
|
908
1015
|
);
|
|
909
1016
|
}
|
|
910
1017
|
const descriptor = validation.descriptor;
|
|
911
|
-
const outDir = (0,
|
|
1018
|
+
const outDir = (0, import_node_path5.resolve)(options.outDir);
|
|
912
1019
|
if (options.projectRoot) {
|
|
913
|
-
assertRealPathUnderRoot((0,
|
|
1020
|
+
assertRealPathUnderRoot((0, import_node_path5.resolve)(options.projectRoot), outDir);
|
|
914
1021
|
}
|
|
915
1022
|
const spaDirs = await resolveSpaDirs({ ...options, descriptor });
|
|
916
1023
|
const interchange = descriptorToInterchange(descriptor);
|
|
@@ -928,21 +1035,21 @@ async function writeLxpackProject(options) {
|
|
|
928
1035
|
const courseDir = materialized.courseDir;
|
|
929
1036
|
return {
|
|
930
1037
|
outDir: courseDir,
|
|
931
|
-
courseYamlPath: (0,
|
|
932
|
-
lessonkitJsonPath: (0,
|
|
1038
|
+
courseYamlPath: (0, import_node_path5.join)(courseDir, "course.yaml"),
|
|
1039
|
+
lessonkitJsonPath: (0, import_node_path5.join)(courseDir, "lessonkit.json")
|
|
933
1040
|
};
|
|
934
1041
|
}
|
|
935
1042
|
|
|
936
1043
|
// src/packageCourse.ts
|
|
937
|
-
var
|
|
1044
|
+
var import_node_path9 = require("path");
|
|
938
1045
|
var fsp3 = __toESM(require("fs/promises"), 1);
|
|
939
1046
|
var import_api2 = require("@lxpack/api");
|
|
940
1047
|
|
|
941
1048
|
// src/packaging/validateInputs.ts
|
|
942
|
-
var
|
|
1049
|
+
var import_node_path6 = require("path");
|
|
943
1050
|
function validatePackageInputs(options) {
|
|
944
1051
|
const { target, output, outputBaseDir } = options;
|
|
945
|
-
const outDir = (0,
|
|
1052
|
+
const outDir = (0, import_node_path6.resolve)(options.outDir);
|
|
946
1053
|
if (!options.projectRoot) {
|
|
947
1054
|
return {
|
|
948
1055
|
ok: false,
|
|
@@ -951,7 +1058,7 @@ function validatePackageInputs(options) {
|
|
|
951
1058
|
issues: [{ path: "projectRoot", message: "projectRoot is required for packageLessonkitCourse" }]
|
|
952
1059
|
};
|
|
953
1060
|
}
|
|
954
|
-
const projectRoot = (0,
|
|
1061
|
+
const projectRoot = (0, import_node_path6.resolve)(options.projectRoot);
|
|
955
1062
|
try {
|
|
956
1063
|
assertRealPathUnderRoot(projectRoot, outDir);
|
|
957
1064
|
} catch (err) {
|
|
@@ -979,9 +1086,9 @@ function validatePackageInputs(options) {
|
|
|
979
1086
|
};
|
|
980
1087
|
}
|
|
981
1088
|
if (output && !isSafeRelativeSpaPath(output)) {
|
|
982
|
-
if ((0,
|
|
1089
|
+
if ((0, import_node_path6.isAbsolute)(output)) {
|
|
983
1090
|
try {
|
|
984
|
-
assertRealPathUnderRoot(projectRoot, (0,
|
|
1091
|
+
assertRealPathUnderRoot(projectRoot, (0, import_node_path6.resolve)(output));
|
|
985
1092
|
} catch (err) {
|
|
986
1093
|
return {
|
|
987
1094
|
ok: false,
|
|
@@ -1008,7 +1115,7 @@ function validatePackageInputs(options) {
|
|
|
1008
1115
|
}
|
|
1009
1116
|
}
|
|
1010
1117
|
if (outputBaseDir) {
|
|
1011
|
-
const resolvedOutputBase = (0,
|
|
1118
|
+
const resolvedOutputBase = (0, import_node_path6.resolve)(projectRoot, outputBaseDir);
|
|
1012
1119
|
try {
|
|
1013
1120
|
assertRealPathUnderRoot(projectRoot, resolvedOutputBase);
|
|
1014
1121
|
} catch (err) {
|
|
@@ -1029,7 +1136,7 @@ function validatePackageInputs(options) {
|
|
|
1029
1136
|
}
|
|
1030
1137
|
}
|
|
1031
1138
|
if (output) {
|
|
1032
|
-
const resolvedOutput = (0,
|
|
1139
|
+
const resolvedOutput = (0, import_node_path6.isAbsolute)(output) ? (0, import_node_path6.resolve)(output) : (0, import_node_path6.resolve)(projectRoot, output);
|
|
1033
1140
|
try {
|
|
1034
1141
|
assertRealPathUnderRoot(projectRoot, resolvedOutput);
|
|
1035
1142
|
} catch (err) {
|
|
@@ -1069,20 +1176,20 @@ function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
|
|
|
1069
1176
|
throw new Error(`${artifactPath} is outside the staging directory`);
|
|
1070
1177
|
}
|
|
1071
1178
|
const rel = relativePathUnderRoot(stagingRoot, resolved);
|
|
1072
|
-
if (rel.startsWith("..") || (0,
|
|
1179
|
+
if (rel.startsWith("..") || (0, import_node_path6.isAbsolute)(rel)) {
|
|
1073
1180
|
throw new Error(`${artifactPath} is outside the staging directory`);
|
|
1074
1181
|
}
|
|
1075
1182
|
if (!rel) return outDir;
|
|
1076
1183
|
if (/^[a-zA-Z]:[/\\]/.test(outDir)) {
|
|
1077
|
-
return
|
|
1184
|
+
return import_node_path6.win32.join(outDir, rel.replace(/\//g, import_node_path6.win32.sep));
|
|
1078
1185
|
}
|
|
1079
|
-
return (0,
|
|
1186
|
+
return (0, import_node_path6.join)(outDir, rel);
|
|
1080
1187
|
}
|
|
1081
1188
|
|
|
1082
1189
|
// src/packaging/promote.ts
|
|
1083
1190
|
var fsp = __toESM(require("fs/promises"), 1);
|
|
1084
1191
|
var import_node_crypto = require("crypto");
|
|
1085
|
-
var
|
|
1192
|
+
var import_node_path7 = require("path");
|
|
1086
1193
|
async function pathExists(path) {
|
|
1087
1194
|
try {
|
|
1088
1195
|
await fsp.access(path);
|
|
@@ -1102,31 +1209,32 @@ async function renameOrCopy(from, to) {
|
|
|
1102
1209
|
}
|
|
1103
1210
|
}
|
|
1104
1211
|
function promoteLockPath(outDir) {
|
|
1105
|
-
const parent = (0,
|
|
1106
|
-
const hash = (0, import_node_crypto.createHash)("sha256").update((0,
|
|
1107
|
-
return (0,
|
|
1212
|
+
const parent = (0, import_node_path7.dirname)(outDir);
|
|
1213
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update((0, import_node_path7.resolve)(outDir)).digest("hex").slice(0, 16);
|
|
1214
|
+
return (0, import_node_path7.join)(parent, `.lk-promote-lock-${hash}`);
|
|
1108
1215
|
}
|
|
1109
1216
|
var STALE_LOCK_TTL_MS = 5 * 60 * 1e3;
|
|
1110
1217
|
async function isStalePromoteLock(lockPath) {
|
|
1111
1218
|
try {
|
|
1112
|
-
const stat2 = await fsp.stat(lockPath);
|
|
1113
|
-
if (Date.now() - stat2.mtimeMs > STALE_LOCK_TTL_MS) return true;
|
|
1114
1219
|
const content = await fsp.readFile(lockPath, "utf8");
|
|
1115
1220
|
const pid = Number.parseInt(content.trim(), 10);
|
|
1116
|
-
if (
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1221
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
1222
|
+
try {
|
|
1223
|
+
process.kill(pid, 0);
|
|
1224
|
+
return false;
|
|
1225
|
+
} catch {
|
|
1226
|
+
return true;
|
|
1227
|
+
}
|
|
1122
1228
|
}
|
|
1229
|
+
const stat2 = await fsp.stat(lockPath);
|
|
1230
|
+
return Date.now() - stat2.mtimeMs > STALE_LOCK_TTL_MS;
|
|
1123
1231
|
} catch {
|
|
1124
1232
|
return true;
|
|
1125
1233
|
}
|
|
1126
1234
|
}
|
|
1127
1235
|
async function withPromoteLock(outDir, fn) {
|
|
1128
1236
|
const lockPath = promoteLockPath(outDir);
|
|
1129
|
-
await fsp.mkdir((0,
|
|
1237
|
+
await fsp.mkdir((0, import_node_path7.dirname)(outDir), { recursive: true });
|
|
1130
1238
|
let lockHandle;
|
|
1131
1239
|
for (let attempt = 0; attempt < 200; attempt++) {
|
|
1132
1240
|
try {
|
|
@@ -1178,11 +1286,11 @@ async function assertNoLegacyPromoteArtifacts(outDir) {
|
|
|
1178
1286
|
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
1179
1287
|
return withPromoteLock(outDir, async () => {
|
|
1180
1288
|
await assertNoLegacyPromoteArtifacts(outDir);
|
|
1181
|
-
const parent = (0,
|
|
1182
|
-
const tmpPromote = await fsp.mkdtemp((0,
|
|
1289
|
+
const parent = (0, import_node_path7.dirname)(outDir);
|
|
1290
|
+
const tmpPromote = await fsp.mkdtemp((0, import_node_path7.join)(parent, ".lk-promote-"));
|
|
1183
1291
|
await renameOrCopy(stagingDir, tmpPromote);
|
|
1184
1292
|
const hadOutDir = await pathExists(outDir);
|
|
1185
|
-
const backup = hadOutDir ? await fsp.mkdtemp((0,
|
|
1293
|
+
const backup = hadOutDir ? await fsp.mkdtemp((0, import_node_path7.join)(parent, ".lk-backup-")) : void 0;
|
|
1186
1294
|
if (hadOutDir && backup) {
|
|
1187
1295
|
await renameOrCopy(outDir, backup);
|
|
1188
1296
|
}
|
|
@@ -1193,7 +1301,7 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1193
1301
|
try {
|
|
1194
1302
|
await renameOrCopy(backup, outDir);
|
|
1195
1303
|
} catch (restoreError) {
|
|
1196
|
-
const failedPromote2 = (0,
|
|
1304
|
+
const failedPromote2 = (0, import_node_path7.join)(parent, `.lk-failed-promote-${(0, import_node_crypto.randomUUID)()}`);
|
|
1197
1305
|
try {
|
|
1198
1306
|
await renameOrCopy(tmpPromote, failedPromote2);
|
|
1199
1307
|
} catch {
|
|
@@ -1223,7 +1331,7 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1223
1331
|
}
|
|
1224
1332
|
throw promoteError;
|
|
1225
1333
|
}
|
|
1226
|
-
const failedPromote = (0,
|
|
1334
|
+
const failedPromote = (0, import_node_path7.join)(parent, `.lk-failed-promote-${(0, import_node_crypto.randomUUID)()}`);
|
|
1227
1335
|
try {
|
|
1228
1336
|
await renameOrCopy(tmpPromote, failedPromote);
|
|
1229
1337
|
} catch {
|
|
@@ -1245,12 +1353,12 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1245
1353
|
|
|
1246
1354
|
// src/packaging/staging.ts
|
|
1247
1355
|
var fsp2 = __toESM(require("fs/promises"), 1);
|
|
1248
|
-
var
|
|
1356
|
+
var import_node_path8 = require("path");
|
|
1249
1357
|
var import_node_os = require("os");
|
|
1250
1358
|
var import_api = require("@lxpack/api");
|
|
1251
1359
|
async function buildStagingPackage(options) {
|
|
1252
1360
|
const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
|
|
1253
|
-
const stagingDir = await fsp2.mkdtemp((0,
|
|
1361
|
+
const stagingDir = await fsp2.mkdtemp((0, import_node_path8.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
|
|
1254
1362
|
try {
|
|
1255
1363
|
let spaDirs;
|
|
1256
1364
|
try {
|
|
@@ -1269,8 +1377,8 @@ async function buildStagingPackage(options) {
|
|
|
1269
1377
|
}
|
|
1270
1378
|
const interchange = descriptorToInterchange(descriptor);
|
|
1271
1379
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
1272
|
-
await fsp2.mkdir((0,
|
|
1273
|
-
const defaultOutput = output ?? (dir ? (0,
|
|
1380
|
+
await fsp2.mkdir((0, import_node_path8.join)(stagingDir, outputBase), { recursive: true });
|
|
1381
|
+
const defaultOutput = output ?? (dir ? (0, import_node_path8.join)(outputBase, target) : (0, import_node_path8.join)(outputBase, `course-${target}.zip`));
|
|
1274
1382
|
const build = await (0, import_api.packageLessonkit)({
|
|
1275
1383
|
interchange,
|
|
1276
1384
|
spaDirs,
|
|
@@ -1310,7 +1418,7 @@ async function buildStagingPackage(options) {
|
|
|
1310
1418
|
}
|
|
1311
1419
|
}
|
|
1312
1420
|
async function ensureOutDirParent(outDir) {
|
|
1313
|
-
await fsp2.mkdir((0,
|
|
1421
|
+
await fsp2.mkdir((0, import_node_path8.dirname)(outDir), { recursive: true });
|
|
1314
1422
|
}
|
|
1315
1423
|
|
|
1316
1424
|
// src/packaging/issueSeverity.ts
|
|
@@ -1325,13 +1433,13 @@ function findPackagingErrorIssues(issues) {
|
|
|
1325
1433
|
// src/packageCourse.ts
|
|
1326
1434
|
async function validateLessonkitProject(options) {
|
|
1327
1435
|
return (0, import_api2.validateCourse)({
|
|
1328
|
-
courseDir: (0,
|
|
1436
|
+
courseDir: (0, import_node_path9.resolve)(options.courseDir),
|
|
1329
1437
|
target: options.target
|
|
1330
1438
|
});
|
|
1331
1439
|
}
|
|
1332
1440
|
async function buildLessonkitProject(options) {
|
|
1333
1441
|
const buildOptions = {
|
|
1334
|
-
courseDir: (0,
|
|
1442
|
+
courseDir: (0, import_node_path9.resolve)(options.courseDir),
|
|
1335
1443
|
target: options.target,
|
|
1336
1444
|
output: options.output,
|
|
1337
1445
|
dir: options.dir,
|
|
@@ -1362,7 +1470,7 @@ async function packageLessonkitCourse(options) {
|
|
|
1362
1470
|
if (!descriptorValidation.ok) {
|
|
1363
1471
|
return {
|
|
1364
1472
|
ok: false,
|
|
1365
|
-
courseDir: (0,
|
|
1473
|
+
courseDir: (0, import_node_path9.resolve)(writeOpts.outDir),
|
|
1366
1474
|
target,
|
|
1367
1475
|
issues: descriptorValidation.issues.map((i) => ({
|
|
1368
1476
|
path: i.path,
|
|
@@ -1371,6 +1479,25 @@ async function packageLessonkitCourse(options) {
|
|
|
1371
1479
|
};
|
|
1372
1480
|
}
|
|
1373
1481
|
const descriptor = descriptorValidation.descriptor;
|
|
1482
|
+
if (writeOpts.projectRoot) {
|
|
1483
|
+
const parityIssues = validateReactManifestParity({
|
|
1484
|
+
projectRoot: writeOpts.projectRoot,
|
|
1485
|
+
descriptor
|
|
1486
|
+
});
|
|
1487
|
+
const parityErrors = parityIssues.filter((i) => i.severity === "error");
|
|
1488
|
+
if (parityErrors.length > 0) {
|
|
1489
|
+
return {
|
|
1490
|
+
ok: false,
|
|
1491
|
+
courseDir: outDir,
|
|
1492
|
+
target,
|
|
1493
|
+
issues: parityErrors.map((i) => ({
|
|
1494
|
+
path: i.path,
|
|
1495
|
+
message: i.message,
|
|
1496
|
+
severity: i.severity
|
|
1497
|
+
}))
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1374
1501
|
const nonInjectableAssessments = (descriptor.assessments ?? []).map((assessment, index) => ({ assessment, index })).filter(({ assessment }) => assessmentDescriptorToLxpack(assessment) === null);
|
|
1375
1502
|
if (nonInjectableAssessments.length > 0) {
|
|
1376
1503
|
return {
|
|
@@ -1675,5 +1802,6 @@ var import_validators2 = require("@lxpack/validators");
|
|
|
1675
1802
|
validateLessonkitProject,
|
|
1676
1803
|
validatePackageInputs,
|
|
1677
1804
|
validateProjectPaths,
|
|
1805
|
+
validateReactManifestParity,
|
|
1678
1806
|
writeLxpackProject
|
|
1679
1807
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -110,6 +110,23 @@ type DescriptorValidationResult = {
|
|
|
110
110
|
declare function validateDescriptor(input: unknown): DescriptorValidationResult;
|
|
111
111
|
declare function validateDescriptorForTarget(input: unknown, target?: ExportTarget): DescriptorValidationResult;
|
|
112
112
|
|
|
113
|
+
type ReactParityIssue = {
|
|
114
|
+
path: string;
|
|
115
|
+
message: string;
|
|
116
|
+
severity: "error" | "warning";
|
|
117
|
+
};
|
|
118
|
+
type ValidateReactManifestParityOptions = {
|
|
119
|
+
projectRoot: string;
|
|
120
|
+
descriptor: LessonkitCourseDescriptor;
|
|
121
|
+
/** Relative source files to scan (default: all `.tsx` under `src/`). */
|
|
122
|
+
appSources?: string[];
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Validates that React app source references the same courseId and assessment checkIds
|
|
126
|
+
* as the lessonkit.json descriptor (prevents LMS/runtime ID drift at package time).
|
|
127
|
+
*/
|
|
128
|
+
declare function validateReactManifestParity(opts: ValidateReactManifestParityOptions): ReactParityIssue[];
|
|
129
|
+
|
|
113
130
|
type ProjectPathsInput = {
|
|
114
131
|
spaDistDir?: string;
|
|
115
132
|
lxpackOutDir?: string;
|
|
@@ -308,4 +325,4 @@ type ParseManifestResult = {
|
|
|
308
325
|
declare function parseLessonkitManifest(raw: unknown, label?: string, projectRoot?: string): ParseManifestResult;
|
|
309
326
|
declare function loadLessonkitManifestFromFile(readJson: () => Promise<unknown>, label?: string, projectRoot?: string): Promise<ParseManifestResult>;
|
|
310
327
|
|
|
311
|
-
export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type FillInBlanksAssessmentDescriptor, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitManifest, type LessonkitManifestPaths, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type ManifestParseIssue, type MappedLessonkitIds, type McqAssessmentDescriptor, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type PackageValidationIssue, type ParseManifestResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type TrueFalseAssessmentDescriptor, type ValidateLessonkitProjectOptions, type ValidatePackageInputsResult, type ValidationIssue, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, buildStagingPackage, descriptorToInterchange, ensureOutDirParent, extractAssessments, loadLessonkitManifestFromFile, mapLessonkitIds, packageLessonkitCourse, parseLessonkitManifest, promoteStagingToOutDir, remapArtifactPaths, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateDescriptorForTarget, validateLessonkitProject, validatePackageInputs, validateProjectPaths, writeLxpackProject };
|
|
328
|
+
export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type FillInBlanksAssessmentDescriptor, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitManifest, type LessonkitManifestPaths, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type ManifestParseIssue, type MappedLessonkitIds, type McqAssessmentDescriptor, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type PackageValidationIssue, type ParseManifestResult, type ProjectPathsInput, type ReactParityIssue, type SpaLayout, type SpaLessonEntry, type TrueFalseAssessmentDescriptor, type ValidateLessonkitProjectOptions, type ValidatePackageInputsResult, type ValidationIssue, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, buildStagingPackage, descriptorToInterchange, ensureOutDirParent, extractAssessments, loadLessonkitManifestFromFile, mapLessonkitIds, packageLessonkitCourse, parseLessonkitManifest, promoteStagingToOutDir, remapArtifactPaths, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateDescriptorForTarget, validateLessonkitProject, validatePackageInputs, validateProjectPaths, validateReactManifestParity, writeLxpackProject };
|
package/dist/index.d.ts
CHANGED
|
@@ -110,6 +110,23 @@ type DescriptorValidationResult = {
|
|
|
110
110
|
declare function validateDescriptor(input: unknown): DescriptorValidationResult;
|
|
111
111
|
declare function validateDescriptorForTarget(input: unknown, target?: ExportTarget): DescriptorValidationResult;
|
|
112
112
|
|
|
113
|
+
type ReactParityIssue = {
|
|
114
|
+
path: string;
|
|
115
|
+
message: string;
|
|
116
|
+
severity: "error" | "warning";
|
|
117
|
+
};
|
|
118
|
+
type ValidateReactManifestParityOptions = {
|
|
119
|
+
projectRoot: string;
|
|
120
|
+
descriptor: LessonkitCourseDescriptor;
|
|
121
|
+
/** Relative source files to scan (default: all `.tsx` under `src/`). */
|
|
122
|
+
appSources?: string[];
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Validates that React app source references the same courseId and assessment checkIds
|
|
126
|
+
* as the lessonkit.json descriptor (prevents LMS/runtime ID drift at package time).
|
|
127
|
+
*/
|
|
128
|
+
declare function validateReactManifestParity(opts: ValidateReactManifestParityOptions): ReactParityIssue[];
|
|
129
|
+
|
|
113
130
|
type ProjectPathsInput = {
|
|
114
131
|
spaDistDir?: string;
|
|
115
132
|
lxpackOutDir?: string;
|
|
@@ -308,4 +325,4 @@ type ParseManifestResult = {
|
|
|
308
325
|
declare function parseLessonkitManifest(raw: unknown, label?: string, projectRoot?: string): ParseManifestResult;
|
|
309
326
|
declare function loadLessonkitManifestFromFile(readJson: () => Promise<unknown>, label?: string, projectRoot?: string): Promise<ParseManifestResult>;
|
|
310
327
|
|
|
311
|
-
export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type FillInBlanksAssessmentDescriptor, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitManifest, type LessonkitManifestPaths, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type ManifestParseIssue, type MappedLessonkitIds, type McqAssessmentDescriptor, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type PackageValidationIssue, type ParseManifestResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type TrueFalseAssessmentDescriptor, type ValidateLessonkitProjectOptions, type ValidatePackageInputsResult, type ValidationIssue, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, buildStagingPackage, descriptorToInterchange, ensureOutDirParent, extractAssessments, loadLessonkitManifestFromFile, mapLessonkitIds, packageLessonkitCourse, parseLessonkitManifest, promoteStagingToOutDir, remapArtifactPaths, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateDescriptorForTarget, validateLessonkitProject, validatePackageInputs, validateProjectPaths, writeLxpackProject };
|
|
328
|
+
export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type FillInBlanksAssessmentDescriptor, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitManifest, type LessonkitManifestPaths, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type ManifestParseIssue, type MappedLessonkitIds, type McqAssessmentDescriptor, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type PackageValidationIssue, type ParseManifestResult, type ProjectPathsInput, type ReactParityIssue, type SpaLayout, type SpaLessonEntry, type TrueFalseAssessmentDescriptor, type ValidateLessonkitProjectOptions, type ValidatePackageInputsResult, type ValidationIssue, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, buildStagingPackage, descriptorToInterchange, ensureOutDirParent, extractAssessments, loadLessonkitManifestFromFile, mapLessonkitIds, packageLessonkitCourse, parseLessonkitManifest, promoteStagingToOutDir, remapArtifactPaths, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateDescriptorForTarget, validateLessonkitProject, validatePackageInputs, validateProjectPaths, validateReactManifestParity, writeLxpackProject };
|
package/dist/index.js
CHANGED
|
@@ -659,6 +659,112 @@ function validateDescriptorForTarget(input, target) {
|
|
|
659
659
|
return result;
|
|
660
660
|
}
|
|
661
661
|
|
|
662
|
+
// src/validateReactParity.ts
|
|
663
|
+
import { readFileSync, existsSync as existsSync2, readdirSync, statSync } from "fs";
|
|
664
|
+
import { join as join2, relative as relative2 } from "path";
|
|
665
|
+
var SCANNABLE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
666
|
+
function collectSourceUnderSrc(projectRoot) {
|
|
667
|
+
const srcDir = join2(projectRoot, "src");
|
|
668
|
+
if (!existsSync2(srcDir)) return [];
|
|
669
|
+
const results = [];
|
|
670
|
+
const walk = (dir) => {
|
|
671
|
+
for (const entry of readdirSync(dir)) {
|
|
672
|
+
const abs = join2(dir, entry);
|
|
673
|
+
if (statSync(abs).isDirectory()) {
|
|
674
|
+
walk(abs);
|
|
675
|
+
} else if (SCANNABLE_EXTENSIONS.some((ext) => entry.endsWith(ext))) {
|
|
676
|
+
results.push(relative2(projectRoot, abs));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
walk(srcDir);
|
|
681
|
+
return results;
|
|
682
|
+
}
|
|
683
|
+
function readAppSources(projectRoot, appSources) {
|
|
684
|
+
return appSources.map((rel) => join2(projectRoot, rel)).filter((abs) => existsSync2(abs)).map((abs) => readFileSync(abs, "utf8")).join("\n");
|
|
685
|
+
}
|
|
686
|
+
function stripComments(source) {
|
|
687
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
|
|
688
|
+
}
|
|
689
|
+
function idPropPatterns(prop, id) {
|
|
690
|
+
return [
|
|
691
|
+
`${prop}="${id}"`,
|
|
692
|
+
`${prop}='${id}'`,
|
|
693
|
+
`${prop}={'${id}'}`,
|
|
694
|
+
`${prop}={"${id}"}`,
|
|
695
|
+
`${prop}={\`${id}\`}`
|
|
696
|
+
];
|
|
697
|
+
}
|
|
698
|
+
function extractStringConstants(source) {
|
|
699
|
+
const stripped = stripComments(source);
|
|
700
|
+
const map = /* @__PURE__ */ new Map();
|
|
701
|
+
const re = /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(["'`])((?:\\.|(?!\2).)*)\2/g;
|
|
702
|
+
for (const match of stripped.matchAll(re)) {
|
|
703
|
+
map.set(match[1], match[3]);
|
|
704
|
+
}
|
|
705
|
+
return map;
|
|
706
|
+
}
|
|
707
|
+
function idUsedViaConstant(stripped, prop, id, constants) {
|
|
708
|
+
for (const [name, value] of constants) {
|
|
709
|
+
if (value !== id) continue;
|
|
710
|
+
const jsxPatterns = [
|
|
711
|
+
`${prop}={${name}}`,
|
|
712
|
+
`${prop}={ ${name} }`,
|
|
713
|
+
`${prop}={${name} }`,
|
|
714
|
+
`${prop}={ ${name}}`
|
|
715
|
+
];
|
|
716
|
+
if (jsxPatterns.some((p) => stripped.includes(p))) return true;
|
|
717
|
+
const objPatterns = [`${prop}: ${name}`, `${prop}:${name}`];
|
|
718
|
+
if (objPatterns.some((p) => stripped.includes(p))) return true;
|
|
719
|
+
}
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
function courseIdPresent(source, courseId) {
|
|
723
|
+
const stripped = stripComments(source);
|
|
724
|
+
if (idPropPatterns("courseId", courseId).some((p) => stripped.includes(p))) return true;
|
|
725
|
+
return idUsedViaConstant(stripped, "courseId", courseId, extractStringConstants(source));
|
|
726
|
+
}
|
|
727
|
+
function checkIdPresent(source, checkId) {
|
|
728
|
+
const stripped = stripComments(source);
|
|
729
|
+
if (idPropPatterns("checkId", checkId).some((p) => stripped.includes(p))) return true;
|
|
730
|
+
return idUsedViaConstant(stripped, "checkId", checkId, extractStringConstants(source));
|
|
731
|
+
}
|
|
732
|
+
function validateReactManifestParity(opts) {
|
|
733
|
+
const appSources = opts.appSources ?? collectSourceUnderSrc(opts.projectRoot);
|
|
734
|
+
const source = readAppSources(opts.projectRoot, appSources);
|
|
735
|
+
const hasDescriptorIds = Boolean(opts.descriptor.courseId) || (opts.descriptor.assessments?.length ?? 0) > 0;
|
|
736
|
+
if (!source.trim()) {
|
|
737
|
+
return [
|
|
738
|
+
{
|
|
739
|
+
path: appSources.length > 0 ? appSources.join(", ") : "src/",
|
|
740
|
+
message: hasDescriptorIds ? "React app source not found for ID parity check" : "React app source not found for ID parity check",
|
|
741
|
+
severity: hasDescriptorIds ? "error" : "warning"
|
|
742
|
+
}
|
|
743
|
+
];
|
|
744
|
+
}
|
|
745
|
+
const issues = [];
|
|
746
|
+
const courseId = opts.descriptor.courseId;
|
|
747
|
+
if (!courseIdPresent(source, courseId)) {
|
|
748
|
+
issues.push({
|
|
749
|
+
path: "course.courseId",
|
|
750
|
+
message: `React app source does not reference courseId="${courseId}" from lessonkit.json`,
|
|
751
|
+
severity: "error"
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
for (const assessment of opts.descriptor.assessments ?? []) {
|
|
755
|
+
const checkId = assessment.checkId;
|
|
756
|
+
if (!checkId) continue;
|
|
757
|
+
if (!checkIdPresent(source, checkId)) {
|
|
758
|
+
issues.push({
|
|
759
|
+
path: `assessments.checkId:${checkId}`,
|
|
760
|
+
message: `React app source missing checkId="${checkId}" declared in lessonkit.json`,
|
|
761
|
+
severity: "error"
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return issues;
|
|
766
|
+
}
|
|
767
|
+
|
|
662
768
|
// src/validateProjectPaths.ts
|
|
663
769
|
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
664
770
|
function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
@@ -782,12 +888,12 @@ function descriptorToInterchange(descriptor) {
|
|
|
782
888
|
}
|
|
783
889
|
|
|
784
890
|
// src/writeProject.ts
|
|
785
|
-
import { join as
|
|
891
|
+
import { join as join4, resolve as resolve4 } from "path";
|
|
786
892
|
import { materializeLessonkitProject } from "@lxpack/validators";
|
|
787
893
|
|
|
788
894
|
// src/spaDirs.ts
|
|
789
895
|
import { access } from "fs/promises";
|
|
790
|
-
import { join as
|
|
896
|
+
import { join as join3, resolve as resolve3 } from "path";
|
|
791
897
|
async function resolveSpaDirs(options) {
|
|
792
898
|
const { descriptor, spaDistDir, lessonSpaDirs, projectRoot } = options;
|
|
793
899
|
const spaLessons = resolveSpaLessons(descriptor);
|
|
@@ -804,9 +910,9 @@ async function resolveSpaDirs(options) {
|
|
|
804
910
|
throw new Error(`spaDistDir not found: ${srcDist}`);
|
|
805
911
|
}
|
|
806
912
|
try {
|
|
807
|
-
await access(
|
|
913
|
+
await access(join3(srcDist, "index.html"));
|
|
808
914
|
} catch {
|
|
809
|
-
throw new Error(`spaDistDir must contain index.html: ${
|
|
915
|
+
throw new Error(`spaDistDir must contain index.html: ${join3(srcDist, "index.html")}`);
|
|
810
916
|
}
|
|
811
917
|
const lessonId = spaLessons[0]?.id ?? /* v8 ignore next */
|
|
812
918
|
"main";
|
|
@@ -829,10 +935,10 @@ async function resolveSpaDirs(options) {
|
|
|
829
935
|
throw new Error(`lessonSpaDirs path not found for lesson "${lesson.id}": ${resolved}`);
|
|
830
936
|
}
|
|
831
937
|
try {
|
|
832
|
-
await access(
|
|
938
|
+
await access(join3(resolved, "index.html"));
|
|
833
939
|
} catch {
|
|
834
940
|
throw new Error(
|
|
835
|
-
`lessonSpaDirs must contain index.html for lesson "${lesson.id}": ${
|
|
941
|
+
`lessonSpaDirs must contain index.html for lesson "${lesson.id}": ${join3(resolved, "index.html")}`
|
|
836
942
|
);
|
|
837
943
|
}
|
|
838
944
|
dirs[lesson.id] = resolved;
|
|
@@ -869,8 +975,8 @@ async function writeLxpackProject(options) {
|
|
|
869
975
|
const courseDir = materialized.courseDir;
|
|
870
976
|
return {
|
|
871
977
|
outDir: courseDir,
|
|
872
|
-
courseYamlPath:
|
|
873
|
-
lessonkitJsonPath:
|
|
978
|
+
courseYamlPath: join4(courseDir, "course.yaml"),
|
|
979
|
+
lessonkitJsonPath: join4(courseDir, "lessonkit.json")
|
|
874
980
|
};
|
|
875
981
|
}
|
|
876
982
|
|
|
@@ -883,7 +989,7 @@ import {
|
|
|
883
989
|
} from "@lxpack/api";
|
|
884
990
|
|
|
885
991
|
// src/packaging/validateInputs.ts
|
|
886
|
-
import { isAbsolute as isAbsolute3, join as
|
|
992
|
+
import { isAbsolute as isAbsolute3, join as join5, resolve as resolve5, win32 as win322 } from "path";
|
|
887
993
|
function validatePackageInputs(options) {
|
|
888
994
|
const { target, output, outputBaseDir } = options;
|
|
889
995
|
const outDir = resolve5(options.outDir);
|
|
@@ -1020,13 +1126,13 @@ function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
|
|
|
1020
1126
|
if (/^[a-zA-Z]:[/\\]/.test(outDir)) {
|
|
1021
1127
|
return win322.join(outDir, rel.replace(/\//g, win322.sep));
|
|
1022
1128
|
}
|
|
1023
|
-
return
|
|
1129
|
+
return join5(outDir, rel);
|
|
1024
1130
|
}
|
|
1025
1131
|
|
|
1026
1132
|
// src/packaging/promote.ts
|
|
1027
1133
|
import * as fsp from "fs/promises";
|
|
1028
1134
|
import { createHash, randomUUID } from "crypto";
|
|
1029
|
-
import { dirname, join as
|
|
1135
|
+
import { dirname, join as join6, resolve as resolve6 } from "path";
|
|
1030
1136
|
async function pathExists(path) {
|
|
1031
1137
|
try {
|
|
1032
1138
|
await fsp.access(path);
|
|
@@ -1048,22 +1154,23 @@ async function renameOrCopy(from, to) {
|
|
|
1048
1154
|
function promoteLockPath(outDir) {
|
|
1049
1155
|
const parent = dirname(outDir);
|
|
1050
1156
|
const hash = createHash("sha256").update(resolve6(outDir)).digest("hex").slice(0, 16);
|
|
1051
|
-
return
|
|
1157
|
+
return join6(parent, `.lk-promote-lock-${hash}`);
|
|
1052
1158
|
}
|
|
1053
1159
|
var STALE_LOCK_TTL_MS = 5 * 60 * 1e3;
|
|
1054
1160
|
async function isStalePromoteLock(lockPath) {
|
|
1055
1161
|
try {
|
|
1056
|
-
const stat2 = await fsp.stat(lockPath);
|
|
1057
|
-
if (Date.now() - stat2.mtimeMs > STALE_LOCK_TTL_MS) return true;
|
|
1058
1162
|
const content = await fsp.readFile(lockPath, "utf8");
|
|
1059
1163
|
const pid = Number.parseInt(content.trim(), 10);
|
|
1060
|
-
if (
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1164
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
1165
|
+
try {
|
|
1166
|
+
process.kill(pid, 0);
|
|
1167
|
+
return false;
|
|
1168
|
+
} catch {
|
|
1169
|
+
return true;
|
|
1170
|
+
}
|
|
1066
1171
|
}
|
|
1172
|
+
const stat2 = await fsp.stat(lockPath);
|
|
1173
|
+
return Date.now() - stat2.mtimeMs > STALE_LOCK_TTL_MS;
|
|
1067
1174
|
} catch {
|
|
1068
1175
|
return true;
|
|
1069
1176
|
}
|
|
@@ -1123,10 +1230,10 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1123
1230
|
return withPromoteLock(outDir, async () => {
|
|
1124
1231
|
await assertNoLegacyPromoteArtifacts(outDir);
|
|
1125
1232
|
const parent = dirname(outDir);
|
|
1126
|
-
const tmpPromote = await fsp.mkdtemp(
|
|
1233
|
+
const tmpPromote = await fsp.mkdtemp(join6(parent, ".lk-promote-"));
|
|
1127
1234
|
await renameOrCopy(stagingDir, tmpPromote);
|
|
1128
1235
|
const hadOutDir = await pathExists(outDir);
|
|
1129
|
-
const backup = hadOutDir ? await fsp.mkdtemp(
|
|
1236
|
+
const backup = hadOutDir ? await fsp.mkdtemp(join6(parent, ".lk-backup-")) : void 0;
|
|
1130
1237
|
if (hadOutDir && backup) {
|
|
1131
1238
|
await renameOrCopy(outDir, backup);
|
|
1132
1239
|
}
|
|
@@ -1137,7 +1244,7 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1137
1244
|
try {
|
|
1138
1245
|
await renameOrCopy(backup, outDir);
|
|
1139
1246
|
} catch (restoreError) {
|
|
1140
|
-
const failedPromote2 =
|
|
1247
|
+
const failedPromote2 = join6(parent, `.lk-failed-promote-${randomUUID()}`);
|
|
1141
1248
|
try {
|
|
1142
1249
|
await renameOrCopy(tmpPromote, failedPromote2);
|
|
1143
1250
|
} catch {
|
|
@@ -1167,7 +1274,7 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1167
1274
|
}
|
|
1168
1275
|
throw promoteError;
|
|
1169
1276
|
}
|
|
1170
|
-
const failedPromote =
|
|
1277
|
+
const failedPromote = join6(parent, `.lk-failed-promote-${randomUUID()}`);
|
|
1171
1278
|
try {
|
|
1172
1279
|
await renameOrCopy(tmpPromote, failedPromote);
|
|
1173
1280
|
} catch {
|
|
@@ -1189,12 +1296,12 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1189
1296
|
|
|
1190
1297
|
// src/packaging/staging.ts
|
|
1191
1298
|
import * as fsp2 from "fs/promises";
|
|
1192
|
-
import { dirname as dirname2, join as
|
|
1299
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
1193
1300
|
import { tmpdir } from "os";
|
|
1194
1301
|
import { packageLessonkit } from "@lxpack/api";
|
|
1195
1302
|
async function buildStagingPackage(options) {
|
|
1196
1303
|
const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
|
|
1197
|
-
const stagingDir = await fsp2.mkdtemp(
|
|
1304
|
+
const stagingDir = await fsp2.mkdtemp(join7(tmpdir(), "lessonkit-lxpack-"));
|
|
1198
1305
|
try {
|
|
1199
1306
|
let spaDirs;
|
|
1200
1307
|
try {
|
|
@@ -1213,8 +1320,8 @@ async function buildStagingPackage(options) {
|
|
|
1213
1320
|
}
|
|
1214
1321
|
const interchange = descriptorToInterchange(descriptor);
|
|
1215
1322
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
1216
|
-
await fsp2.mkdir(
|
|
1217
|
-
const defaultOutput = output ?? (dir ?
|
|
1323
|
+
await fsp2.mkdir(join7(stagingDir, outputBase), { recursive: true });
|
|
1324
|
+
const defaultOutput = output ?? (dir ? join7(outputBase, target) : join7(outputBase, `course-${target}.zip`));
|
|
1218
1325
|
const build = await packageLessonkit({
|
|
1219
1326
|
interchange,
|
|
1220
1327
|
spaDirs,
|
|
@@ -1315,6 +1422,25 @@ async function packageLessonkitCourse(options) {
|
|
|
1315
1422
|
};
|
|
1316
1423
|
}
|
|
1317
1424
|
const descriptor = descriptorValidation.descriptor;
|
|
1425
|
+
if (writeOpts.projectRoot) {
|
|
1426
|
+
const parityIssues = validateReactManifestParity({
|
|
1427
|
+
projectRoot: writeOpts.projectRoot,
|
|
1428
|
+
descriptor
|
|
1429
|
+
});
|
|
1430
|
+
const parityErrors = parityIssues.filter((i) => i.severity === "error");
|
|
1431
|
+
if (parityErrors.length > 0) {
|
|
1432
|
+
return {
|
|
1433
|
+
ok: false,
|
|
1434
|
+
courseDir: outDir,
|
|
1435
|
+
target,
|
|
1436
|
+
issues: parityErrors.map((i) => ({
|
|
1437
|
+
path: i.path,
|
|
1438
|
+
message: i.message,
|
|
1439
|
+
severity: i.severity
|
|
1440
|
+
}))
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1318
1444
|
const nonInjectableAssessments = (descriptor.assessments ?? []).map((assessment, index) => ({ assessment, index })).filter(({ assessment }) => assessmentDescriptorToLxpack(assessment) === null);
|
|
1319
1445
|
if (nonInjectableAssessments.length > 0) {
|
|
1320
1446
|
return {
|
|
@@ -1586,5 +1712,6 @@ export {
|
|
|
1586
1712
|
validateLessonkitProject,
|
|
1587
1713
|
validatePackageInputs,
|
|
1588
1714
|
validateProjectPaths,
|
|
1715
|
+
validateReactManifestParity,
|
|
1589
1716
|
writeLxpackProject
|
|
1590
1717
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/lxpack",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "LXPack export adapter for LessonKit courses (SCORM, standalone, xAPI, cmi5).",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"lint": "echo \"(no lint configured yet)\""
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@lessonkit/core": "1.3.
|
|
59
|
-
"@lessonkit/themes": "1.3.
|
|
58
|
+
"@lessonkit/core": "1.3.1",
|
|
59
|
+
"@lessonkit/themes": "1.3.1",
|
|
60
60
|
"@lxpack/api": "^0.6.2",
|
|
61
61
|
"@lxpack/spa-bridge": "^0.6.2",
|
|
62
62
|
"@lxpack/tracking-schema": "^0.6.2",
|