@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 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/validateProjectPaths.ts
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, import_node_path2.resolve)(projectRoot, value));
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, import_node_path2.resolve)(projectRoot);
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, import_node_path2.resolve)(projectRoot);
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, import_node_path2.isAbsolute)(trimmed)) {
761
- const resolved2 = (0, import_node_path2.resolve)(trimmed);
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, import_node_path2.resolve)(root, trimmed);
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 import_node_path4 = require("path");
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 import_node_path3 = require("path");
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, import_node_path3.resolve)(projectRoot, spaDistRelative) : (0, import_node_path3.resolve)(spaDistRelative);
963
+ const srcDist = projectRoot ? (0, import_node_path4.resolve)(projectRoot, spaDistRelative) : (0, import_node_path4.resolve)(spaDistRelative);
857
964
  if (projectRoot) {
858
- assertRealPathUnderRoot((0, import_node_path3.resolve)(projectRoot), srcDist);
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, import_node_path3.join)(srcDist, "index.html"));
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, import_node_path3.join)(srcDist, "index.html")}`);
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, import_node_path3.resolve)(projectRoot, src) : (0, import_node_path3.resolve)(src);
988
+ const resolved = projectRoot ? (0, import_node_path4.resolve)(projectRoot, src) : (0, import_node_path4.resolve)(src);
882
989
  if (projectRoot) {
883
- assertRealPathUnderRoot((0, import_node_path3.resolve)(projectRoot), resolved);
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, import_node_path3.join)(resolved, "index.html"));
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, import_node_path3.join)(resolved, "index.html")}`
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, import_node_path4.resolve)(options.outDir);
1018
+ const outDir = (0, import_node_path5.resolve)(options.outDir);
912
1019
  if (options.projectRoot) {
913
- assertRealPathUnderRoot((0, import_node_path4.resolve)(options.projectRoot), outDir);
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, import_node_path4.join)(courseDir, "course.yaml"),
932
- lessonkitJsonPath: (0, import_node_path4.join)(courseDir, "lessonkit.json")
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 import_node_path8 = require("path");
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 import_node_path5 = require("path");
1049
+ var import_node_path6 = require("path");
943
1050
  function validatePackageInputs(options) {
944
1051
  const { target, output, outputBaseDir } = options;
945
- const outDir = (0, import_node_path5.resolve)(options.outDir);
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, import_node_path5.resolve)(options.projectRoot);
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, import_node_path5.isAbsolute)(output)) {
1089
+ if ((0, import_node_path6.isAbsolute)(output)) {
983
1090
  try {
984
- assertRealPathUnderRoot(projectRoot, (0, import_node_path5.resolve)(output));
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, import_node_path5.resolve)(projectRoot, outputBaseDir);
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, import_node_path5.isAbsolute)(output) ? (0, import_node_path5.resolve)(output) : (0, import_node_path5.resolve)(projectRoot, output);
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, import_node_path5.isAbsolute)(rel)) {
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 import_node_path5.win32.join(outDir, rel.replace(/\//g, import_node_path5.win32.sep));
1184
+ return import_node_path6.win32.join(outDir, rel.replace(/\//g, import_node_path6.win32.sep));
1078
1185
  }
1079
- return (0, import_node_path5.join)(outDir, rel);
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 import_node_path6 = require("path");
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, import_node_path6.dirname)(outDir);
1106
- const hash = (0, import_node_crypto.createHash)("sha256").update((0, import_node_path6.resolve)(outDir)).digest("hex").slice(0, 16);
1107
- return (0, import_node_path6.join)(parent, `.lk-promote-lock-${hash}`);
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 (!Number.isFinite(pid) || pid <= 0) return true;
1117
- try {
1118
- process.kill(pid, 0);
1119
- return false;
1120
- } catch {
1121
- return true;
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, import_node_path6.dirname)(outDir), { recursive: true });
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, import_node_path6.dirname)(outDir);
1182
- const tmpPromote = await fsp.mkdtemp((0, import_node_path6.join)(parent, ".lk-promote-"));
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, import_node_path6.join)(parent, ".lk-backup-")) : void 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, import_node_path6.join)(parent, `.lk-failed-promote-${(0, import_node_crypto.randomUUID)()}`);
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, import_node_path6.join)(parent, `.lk-failed-promote-${(0, import_node_crypto.randomUUID)()}`);
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 import_node_path7 = require("path");
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, import_node_path7.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
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, import_node_path7.join)(stagingDir, outputBase), { recursive: true });
1273
- const defaultOutput = output ?? (dir ? (0, import_node_path7.join)(outputBase, target) : (0, import_node_path7.join)(outputBase, `course-${target}.zip`));
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, import_node_path7.dirname)(outDir), { recursive: true });
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, import_node_path8.resolve)(options.courseDir),
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, import_node_path8.resolve)(options.courseDir),
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, import_node_path8.resolve)(writeOpts.outDir),
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 join3, resolve as resolve4 } from "path";
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 join2, resolve as resolve3 } from "path";
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(join2(srcDist, "index.html"));
913
+ await access(join3(srcDist, "index.html"));
808
914
  } catch {
809
- throw new Error(`spaDistDir must contain index.html: ${join2(srcDist, "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(join2(resolved, "index.html"));
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}": ${join2(resolved, "index.html")}`
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: join3(courseDir, "course.yaml"),
873
- lessonkitJsonPath: join3(courseDir, "lessonkit.json")
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 join4, resolve as resolve5, win32 as win322 } from "path";
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 join4(outDir, rel);
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 join5, resolve as resolve6 } from "path";
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 join5(parent, `.lk-promote-lock-${hash}`);
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 (!Number.isFinite(pid) || pid <= 0) return true;
1061
- try {
1062
- process.kill(pid, 0);
1063
- return false;
1064
- } catch {
1065
- return true;
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(join5(parent, ".lk-promote-"));
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(join5(parent, ".lk-backup-")) : void 0;
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 = join5(parent, `.lk-failed-promote-${randomUUID()}`);
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 = join5(parent, `.lk-failed-promote-${randomUUID()}`);
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 join6 } from "path";
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(join6(tmpdir(), "lessonkit-lxpack-"));
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(join6(stagingDir, outputBase), { recursive: true });
1217
- const defaultOutput = output ?? (dir ? join6(outputBase, target) : join6(outputBase, `course-${target}.zip`));
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.0",
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.0",
59
- "@lessonkit/themes": "1.3.0",
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",