@lessonkit/lxpack 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/bridge.d.cts +3 -2
- package/dist/bridge.d.ts +3 -2
- package/dist/index.cjs +146 -67
- package/dist/index.d.cts +7 -10
- package/dist/index.d.ts +7 -10
- package/dist/index.js +127 -48
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
[](https://lessonkit.readthedocs.io/en/latest/reference/packaging.html)
|
|
5
5
|
[](https://github.com/eddiethedean/lessonkit/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
Package Vite SPAs for LMS delivery — SCORM 1.2/2004, standalone, xAPI, and cmi5
|
|
7
|
+
Package Vite SPAs for LMS delivery — SCORM 1.2/2004, standalone, xAPI, and cmi5. `@lessonkit/lxpack` bundles [`@lxpack/*`](https://www.npmjs.com/org/lxpack) as direct dependencies (no separate `@lxpack/api` install).
|
|
8
8
|
|
|
9
9
|
Requires Node.js **18+**.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm install @lessonkit/lxpack
|
|
14
|
+
npm install @lessonkit/lxpack
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## Usage
|
package/dist/bridge.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TelemetryEvent, CheckId, LessonId } from '@lessonkit/core';
|
|
1
|
+
import { LmsBridgeMode, TelemetryEvent, CheckId, LessonId } from '@lessonkit/core';
|
|
2
2
|
import { LxpackBridgeV1, LxpackBridgeSubmitAssessmentPayload } from '@lxpack/spa-bridge';
|
|
3
3
|
export { DEFAULT_BRIDGE_PASSING_SCORE, LXPACK_BRIDGE_VERSIONS, LxpackBridgeSubmitAssessmentPayload, LxpackBridgeV1, createLxpackBridgeHost, getLxpackBridge, normalizePassingThreshold, normalizeScore, supportedBridgeVersions } from '@lxpack/spa-bridge';
|
|
4
4
|
import { mapLessonkitTelemetryToBridgeAction } from '@lxpack/tracking-schema';
|
|
@@ -21,7 +21,8 @@ declare function normalizeAssessmentPassingScore(opts?: {
|
|
|
21
21
|
passingScore?: number;
|
|
22
22
|
maxScore?: number;
|
|
23
23
|
}): number;
|
|
24
|
-
|
|
24
|
+
/** @deprecated Use `LmsBridgeMode` from `@lessonkit/core`. */
|
|
25
|
+
type LxpackBridgeMode = LmsBridgeMode;
|
|
25
26
|
/** Apply a mapped bridge action to an LXPack bridge instance. */
|
|
26
27
|
declare function dispatchBridgeAction(bridge: LxpackBridgeV1, action: ReturnType<typeof mapLessonkitTelemetryToBridgeAction>): void;
|
|
27
28
|
declare function forwardTelemetryToBridge(event: TelemetryEvent, mode?: LxpackBridgeMode, parentWindow?: Window): void;
|
package/dist/bridge.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TelemetryEvent, CheckId, LessonId } from '@lessonkit/core';
|
|
1
|
+
import { LmsBridgeMode, TelemetryEvent, CheckId, LessonId } from '@lessonkit/core';
|
|
2
2
|
import { LxpackBridgeV1, LxpackBridgeSubmitAssessmentPayload } from '@lxpack/spa-bridge';
|
|
3
3
|
export { DEFAULT_BRIDGE_PASSING_SCORE, LXPACK_BRIDGE_VERSIONS, LxpackBridgeSubmitAssessmentPayload, LxpackBridgeV1, createLxpackBridgeHost, getLxpackBridge, normalizePassingThreshold, normalizeScore, supportedBridgeVersions } from '@lxpack/spa-bridge';
|
|
4
4
|
import { mapLessonkitTelemetryToBridgeAction } from '@lxpack/tracking-schema';
|
|
@@ -21,7 +21,8 @@ declare function normalizeAssessmentPassingScore(opts?: {
|
|
|
21
21
|
passingScore?: number;
|
|
22
22
|
maxScore?: number;
|
|
23
23
|
}): number;
|
|
24
|
-
|
|
24
|
+
/** @deprecated Use `LmsBridgeMode` from `@lessonkit/core`. */
|
|
25
|
+
type LxpackBridgeMode = LmsBridgeMode;
|
|
25
26
|
/** Apply a mapped bridge action to an LXPack bridge instance. */
|
|
26
27
|
declare function dispatchBridgeAction(bridge: LxpackBridgeV1, action: ReturnType<typeof mapLessonkitTelemetryToBridgeAction>): void;
|
|
27
28
|
declare function forwardTelemetryToBridge(event: TelemetryEvent, mode?: LxpackBridgeMode, parentWindow?: Window): void;
|
package/dist/index.cjs
CHANGED
|
@@ -375,6 +375,10 @@ var validateMcqLike = (assessment, path, issues) => {
|
|
|
375
375
|
} else if (trimmedChoices.length && !trimmedChoices.includes(assessment.answer.trim())) {
|
|
376
376
|
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
377
377
|
}
|
|
378
|
+
const uniqueChoices = new Set(trimmedChoices);
|
|
379
|
+
if (trimmedChoices.length !== uniqueChoices.size) {
|
|
380
|
+
issues.push({ path: `${path}.choices`, message: "choices must be unique" });
|
|
381
|
+
}
|
|
378
382
|
};
|
|
379
383
|
function countStarDelimitedBlanks(template) {
|
|
380
384
|
const matches = template.match(/\*[^*]+\*/g);
|
|
@@ -642,15 +646,8 @@ function assessmentDescriptorToLxpack(assessment) {
|
|
|
642
646
|
if (kind === "fillInBlanks") {
|
|
643
647
|
return null;
|
|
644
648
|
}
|
|
645
|
-
if (kind === "findHotspot"
|
|
646
|
-
return
|
|
647
|
-
kind: "mcq",
|
|
648
|
-
checkId: assessment.checkId,
|
|
649
|
-
question: assessment.question,
|
|
650
|
-
choices: [assessment.correctTargetId, "other"],
|
|
651
|
-
answer: assessment.correctTargetId,
|
|
652
|
-
passingScore: assessment.passingScore
|
|
653
|
-
});
|
|
649
|
+
if (kind === "findHotspot") {
|
|
650
|
+
return null;
|
|
654
651
|
}
|
|
655
652
|
if (kind === "findMultipleHotspots") {
|
|
656
653
|
return null;
|
|
@@ -664,6 +661,20 @@ function extractAssessments(descriptor) {
|
|
|
664
661
|
return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack).filter((a) => a !== null);
|
|
665
662
|
}
|
|
666
663
|
|
|
664
|
+
// src/descriptor/validateInjectableAssessments.ts
|
|
665
|
+
function validateInjectableAssessments(descriptor) {
|
|
666
|
+
const issues = [];
|
|
667
|
+
(descriptor.assessments ?? []).forEach((assessment, index) => {
|
|
668
|
+
if (assessmentDescriptorToLxpack(assessment) === null) {
|
|
669
|
+
issues.push({
|
|
670
|
+
path: `assessments[${index}]`,
|
|
671
|
+
message: `assessment kind "${assessment.kind ?? "mcq"}" (checkId "${assessment.checkId}") is not injected into LMS shell quizzes`
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
return issues;
|
|
676
|
+
}
|
|
677
|
+
|
|
667
678
|
// src/descriptor/validateForTarget.ts
|
|
668
679
|
var LMS_SHELL_TARGETS = /* @__PURE__ */ new Set([
|
|
669
680
|
"scorm12",
|
|
@@ -678,20 +689,21 @@ function validateDescriptorForExportTarget(descriptor, target) {
|
|
|
678
689
|
const activityIri = descriptor.tracking?.xapi?.activityIri?.trim();
|
|
679
690
|
if (!activityIri) {
|
|
680
691
|
issues.push({
|
|
681
|
-
path: "
|
|
692
|
+
path: "tracking.xapi.activityIri",
|
|
682
693
|
message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
|
|
683
694
|
});
|
|
695
|
+
} else if (!/^https:\/\/.+/i.test(activityIri)) {
|
|
696
|
+
issues.push({
|
|
697
|
+
path: "tracking.xapi.activityIri",
|
|
698
|
+
message: "tracking.xapi.activityIri must be an HTTPS URL for xapi and cmi5 export targets"
|
|
699
|
+
});
|
|
684
700
|
}
|
|
685
701
|
}
|
|
686
702
|
if (LMS_SHELL_TARGETS.has(target)) {
|
|
687
|
-
(descriptor
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
message: `assessment kind "${assessment.kind ?? "mcq"}" (checkId "${assessment.checkId}") is not injected into LMS shell quizzes for target "${target}"`
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
});
|
|
703
|
+
issues.push(...validateInjectableAssessments(descriptor).map((issue) => ({
|
|
704
|
+
...issue,
|
|
705
|
+
message: `${issue.message} for target "${target}"`
|
|
706
|
+
})));
|
|
695
707
|
}
|
|
696
708
|
return issues;
|
|
697
709
|
}
|
|
@@ -789,6 +801,10 @@ function checkIdPresent(source, checkId) {
|
|
|
789
801
|
if (idPropPatterns("checkId", checkId).some((p) => stripped.includes(p))) return true;
|
|
790
802
|
return idUsedViaConstant(stripped, "checkId", checkId, extractStringConstants(source));
|
|
791
803
|
}
|
|
804
|
+
var ID_SYNC_DOC = "https://lessonkit.readthedocs.io/en/latest/guides/react-developers/quickstart.html#keep-react-ids-in-sync-with-lessonkitjson";
|
|
805
|
+
function parityHint(message) {
|
|
806
|
+
return `${message} See ${ID_SYNC_DOC}`;
|
|
807
|
+
}
|
|
792
808
|
function validateReactManifestParity(opts) {
|
|
793
809
|
const appSources = opts.appSources ?? collectSourceUnderSrc(opts.projectRoot);
|
|
794
810
|
const source = readAppSources(opts.projectRoot, appSources);
|
|
@@ -807,7 +823,9 @@ function validateReactManifestParity(opts) {
|
|
|
807
823
|
if (!courseIdPresent(source, courseId)) {
|
|
808
824
|
issues.push({
|
|
809
825
|
path: "course.courseId",
|
|
810
|
-
message:
|
|
826
|
+
message: parityHint(
|
|
827
|
+
`React app source does not reference courseId="${courseId}" from lessonkit.json.`
|
|
828
|
+
),
|
|
811
829
|
severity: "error"
|
|
812
830
|
});
|
|
813
831
|
}
|
|
@@ -817,7 +835,9 @@ function validateReactManifestParity(opts) {
|
|
|
817
835
|
if (!checkIdPresent(source, checkId)) {
|
|
818
836
|
issues.push({
|
|
819
837
|
path: `assessments.checkId:${checkId}`,
|
|
820
|
-
message:
|
|
838
|
+
message: parityHint(
|
|
839
|
+
`React app source missing checkId="${checkId}" declared in lessonkit.json.`
|
|
840
|
+
),
|
|
821
841
|
severity: "error"
|
|
822
842
|
});
|
|
823
843
|
}
|
|
@@ -948,7 +968,7 @@ function descriptorToInterchange(descriptor) {
|
|
|
948
968
|
}
|
|
949
969
|
|
|
950
970
|
// src/writeProject.ts
|
|
951
|
-
var
|
|
971
|
+
var import_node_path6 = require("path");
|
|
952
972
|
var import_validators = require("@lxpack/validators");
|
|
953
973
|
|
|
954
974
|
// src/spaDirs.ts
|
|
@@ -1006,6 +1026,59 @@ async function resolveSpaDirs(options) {
|
|
|
1006
1026
|
return dirs;
|
|
1007
1027
|
}
|
|
1008
1028
|
|
|
1029
|
+
// src/spaDistValidation.ts
|
|
1030
|
+
var import_promises2 = require("fs/promises");
|
|
1031
|
+
var import_node_fs3 = require("fs");
|
|
1032
|
+
var import_node_path5 = require("path");
|
|
1033
|
+
async function assertSpaDistContentsSafe(spaDirs, projectRoot) {
|
|
1034
|
+
for (const [label, dir] of Object.entries(spaDirs)) {
|
|
1035
|
+
const dirResolved = resolveComparablePath(dir);
|
|
1036
|
+
const dirStat = await (0, import_promises2.lstat)(dirResolved);
|
|
1037
|
+
if (dirStat.isSymbolicLink()) {
|
|
1038
|
+
throw new Error(`spa dist for "${label}" cannot be a symlink: ${dir}`);
|
|
1039
|
+
}
|
|
1040
|
+
let rootReal;
|
|
1041
|
+
try {
|
|
1042
|
+
rootReal = (0, import_node_fs3.realpathSync)(dirResolved);
|
|
1043
|
+
} catch {
|
|
1044
|
+
throw new Error(`spa dist for "${label}" is not readable: ${dir}`);
|
|
1045
|
+
}
|
|
1046
|
+
if (projectRoot) {
|
|
1047
|
+
assertRealPathUnderRoot(projectRoot, dir);
|
|
1048
|
+
}
|
|
1049
|
+
assertResolvedPathUnderRoot(rootReal, rootReal);
|
|
1050
|
+
await walkDistDir(rootReal, rootReal, label);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
async function walkDistDir(rootReal, current, label) {
|
|
1054
|
+
let entries;
|
|
1055
|
+
try {
|
|
1056
|
+
entries = await (0, import_promises2.readdir)(current, { withFileTypes: true });
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
throw new Error(
|
|
1059
|
+
`spa dist for "${label}" is not readable: ${err instanceof Error ? err.message : String(err)}`,
|
|
1060
|
+
{ cause: err }
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
for (const entry of entries) {
|
|
1064
|
+
const entryPath = (0, import_node_path5.join)(current, entry.name);
|
|
1065
|
+
const stat2 = await (0, import_promises2.lstat)(entryPath);
|
|
1066
|
+
if (stat2.isSymbolicLink()) {
|
|
1067
|
+
throw new Error(`spa dist for "${label}" contains symlink: ${entryPath}`);
|
|
1068
|
+
}
|
|
1069
|
+
let entryReal;
|
|
1070
|
+
try {
|
|
1071
|
+
entryReal = (0, import_node_fs3.realpathSync)(entryPath);
|
|
1072
|
+
} catch {
|
|
1073
|
+
entryReal = entryPath;
|
|
1074
|
+
}
|
|
1075
|
+
assertResolvedPathUnderRoot(rootReal, entryReal);
|
|
1076
|
+
if (stat2.isDirectory()) {
|
|
1077
|
+
await walkDistDir(rootReal, entryPath, label);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1009
1082
|
// src/writeProject.ts
|
|
1010
1083
|
async function writeLxpackProject(options) {
|
|
1011
1084
|
const validation = validateDescriptor(options.descriptor);
|
|
@@ -1015,11 +1088,16 @@ async function writeLxpackProject(options) {
|
|
|
1015
1088
|
);
|
|
1016
1089
|
}
|
|
1017
1090
|
const descriptor = validation.descriptor;
|
|
1018
|
-
const
|
|
1091
|
+
const injectableIssues = validateInjectableAssessments(descriptor);
|
|
1092
|
+
if (injectableIssues.length > 0) {
|
|
1093
|
+
throw new Error(injectableIssues.map((i) => `${i.path}: ${i.message}`).join("; "));
|
|
1094
|
+
}
|
|
1095
|
+
const outDir = (0, import_node_path6.resolve)(options.outDir);
|
|
1019
1096
|
if (options.projectRoot) {
|
|
1020
|
-
assertRealPathUnderRoot((0,
|
|
1097
|
+
assertRealPathUnderRoot((0, import_node_path6.resolve)(options.projectRoot), outDir);
|
|
1021
1098
|
}
|
|
1022
1099
|
const spaDirs = await resolveSpaDirs({ ...options, descriptor });
|
|
1100
|
+
await assertSpaDistContentsSafe(spaDirs, options.projectRoot);
|
|
1023
1101
|
const interchange = descriptorToInterchange(descriptor);
|
|
1024
1102
|
const materialized = await (0, import_validators.materializeLessonkitProject)({
|
|
1025
1103
|
interchange,
|
|
@@ -1035,21 +1113,21 @@ async function writeLxpackProject(options) {
|
|
|
1035
1113
|
const courseDir = materialized.courseDir;
|
|
1036
1114
|
return {
|
|
1037
1115
|
outDir: courseDir,
|
|
1038
|
-
courseYamlPath: (0,
|
|
1039
|
-
lessonkitJsonPath: (0,
|
|
1116
|
+
courseYamlPath: (0, import_node_path6.join)(courseDir, "course.yaml"),
|
|
1117
|
+
lessonkitJsonPath: (0, import_node_path6.join)(courseDir, "lessonkit.json")
|
|
1040
1118
|
};
|
|
1041
1119
|
}
|
|
1042
1120
|
|
|
1043
1121
|
// src/packageCourse.ts
|
|
1044
|
-
var
|
|
1122
|
+
var import_node_path10 = require("path");
|
|
1045
1123
|
var fsp3 = __toESM(require("fs/promises"), 1);
|
|
1046
1124
|
var import_api2 = require("@lxpack/api");
|
|
1047
1125
|
|
|
1048
1126
|
// src/packaging/validateInputs.ts
|
|
1049
|
-
var
|
|
1127
|
+
var import_node_path7 = require("path");
|
|
1050
1128
|
function validatePackageInputs(options) {
|
|
1051
1129
|
const { target, output, outputBaseDir } = options;
|
|
1052
|
-
const outDir = (0,
|
|
1130
|
+
const outDir = (0, import_node_path7.resolve)(options.outDir);
|
|
1053
1131
|
if (!options.projectRoot) {
|
|
1054
1132
|
return {
|
|
1055
1133
|
ok: false,
|
|
@@ -1058,7 +1136,7 @@ function validatePackageInputs(options) {
|
|
|
1058
1136
|
issues: [{ path: "projectRoot", message: "projectRoot is required for packageLessonkitCourse" }]
|
|
1059
1137
|
};
|
|
1060
1138
|
}
|
|
1061
|
-
const projectRoot = (0,
|
|
1139
|
+
const projectRoot = (0, import_node_path7.resolve)(options.projectRoot);
|
|
1062
1140
|
try {
|
|
1063
1141
|
assertRealPathUnderRoot(projectRoot, outDir);
|
|
1064
1142
|
} catch (err) {
|
|
@@ -1086,9 +1164,9 @@ function validatePackageInputs(options) {
|
|
|
1086
1164
|
};
|
|
1087
1165
|
}
|
|
1088
1166
|
if (output && !isSafeRelativeSpaPath(output)) {
|
|
1089
|
-
if ((0,
|
|
1167
|
+
if ((0, import_node_path7.isAbsolute)(output)) {
|
|
1090
1168
|
try {
|
|
1091
|
-
assertRealPathUnderRoot(projectRoot, (0,
|
|
1169
|
+
assertRealPathUnderRoot(projectRoot, (0, import_node_path7.resolve)(output));
|
|
1092
1170
|
} catch (err) {
|
|
1093
1171
|
return {
|
|
1094
1172
|
ok: false,
|
|
@@ -1115,7 +1193,7 @@ function validatePackageInputs(options) {
|
|
|
1115
1193
|
}
|
|
1116
1194
|
}
|
|
1117
1195
|
if (outputBaseDir) {
|
|
1118
|
-
const resolvedOutputBase = (0,
|
|
1196
|
+
const resolvedOutputBase = (0, import_node_path7.resolve)(projectRoot, outputBaseDir);
|
|
1119
1197
|
try {
|
|
1120
1198
|
assertRealPathUnderRoot(projectRoot, resolvedOutputBase);
|
|
1121
1199
|
} catch (err) {
|
|
@@ -1136,7 +1214,7 @@ function validatePackageInputs(options) {
|
|
|
1136
1214
|
}
|
|
1137
1215
|
}
|
|
1138
1216
|
if (output) {
|
|
1139
|
-
const resolvedOutput = (0,
|
|
1217
|
+
const resolvedOutput = (0, import_node_path7.isAbsolute)(output) ? (0, import_node_path7.resolve)(output) : (0, import_node_path7.resolve)(projectRoot, output);
|
|
1140
1218
|
try {
|
|
1141
1219
|
assertRealPathUnderRoot(projectRoot, resolvedOutput);
|
|
1142
1220
|
} catch (err) {
|
|
@@ -1176,20 +1254,20 @@ function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
|
|
|
1176
1254
|
throw new Error(`${artifactPath} is outside the staging directory`);
|
|
1177
1255
|
}
|
|
1178
1256
|
const rel = relativePathUnderRoot(stagingRoot, resolved);
|
|
1179
|
-
if (rel.startsWith("..") || (0,
|
|
1257
|
+
if (rel.startsWith("..") || (0, import_node_path7.isAbsolute)(rel)) {
|
|
1180
1258
|
throw new Error(`${artifactPath} is outside the staging directory`);
|
|
1181
1259
|
}
|
|
1182
1260
|
if (!rel) return outDir;
|
|
1183
1261
|
if (/^[a-zA-Z]:[/\\]/.test(outDir)) {
|
|
1184
|
-
return
|
|
1262
|
+
return import_node_path7.win32.join(outDir, rel.replace(/\//g, import_node_path7.win32.sep));
|
|
1185
1263
|
}
|
|
1186
|
-
return (0,
|
|
1264
|
+
return (0, import_node_path7.join)(outDir, rel);
|
|
1187
1265
|
}
|
|
1188
1266
|
|
|
1189
1267
|
// src/packaging/promote.ts
|
|
1190
1268
|
var fsp = __toESM(require("fs/promises"), 1);
|
|
1191
1269
|
var import_node_crypto = require("crypto");
|
|
1192
|
-
var
|
|
1270
|
+
var import_node_path8 = require("path");
|
|
1193
1271
|
async function pathExists(path) {
|
|
1194
1272
|
try {
|
|
1195
1273
|
await fsp.access(path);
|
|
@@ -1209,9 +1287,9 @@ async function renameOrCopy(from, to) {
|
|
|
1209
1287
|
}
|
|
1210
1288
|
}
|
|
1211
1289
|
function promoteLockPath(outDir) {
|
|
1212
|
-
const parent = (0,
|
|
1213
|
-
const hash = (0, import_node_crypto.createHash)("sha256").update((0,
|
|
1214
|
-
return (0,
|
|
1290
|
+
const parent = (0, import_node_path8.dirname)(outDir);
|
|
1291
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update((0, import_node_path8.resolve)(outDir)).digest("hex").slice(0, 16);
|
|
1292
|
+
return (0, import_node_path8.join)(parent, `.lk-promote-lock-${hash}`);
|
|
1215
1293
|
}
|
|
1216
1294
|
var STALE_LOCK_TTL_MS = 5 * 60 * 1e3;
|
|
1217
1295
|
async function isStalePromoteLock(lockPath) {
|
|
@@ -1234,7 +1312,7 @@ async function isStalePromoteLock(lockPath) {
|
|
|
1234
1312
|
}
|
|
1235
1313
|
async function withPromoteLock(outDir, fn) {
|
|
1236
1314
|
const lockPath = promoteLockPath(outDir);
|
|
1237
|
-
await fsp.mkdir((0,
|
|
1315
|
+
await fsp.mkdir((0, import_node_path8.dirname)(outDir), { recursive: true });
|
|
1238
1316
|
let lockHandle;
|
|
1239
1317
|
for (let attempt = 0; attempt < 200; attempt++) {
|
|
1240
1318
|
try {
|
|
@@ -1286,11 +1364,11 @@ async function assertNoLegacyPromoteArtifacts(outDir) {
|
|
|
1286
1364
|
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
1287
1365
|
return withPromoteLock(outDir, async () => {
|
|
1288
1366
|
await assertNoLegacyPromoteArtifacts(outDir);
|
|
1289
|
-
const parent = (0,
|
|
1290
|
-
const tmpPromote = await fsp.mkdtemp((0,
|
|
1367
|
+
const parent = (0, import_node_path8.dirname)(outDir);
|
|
1368
|
+
const tmpPromote = await fsp.mkdtemp((0, import_node_path8.join)(parent, ".lk-promote-"));
|
|
1291
1369
|
await renameOrCopy(stagingDir, tmpPromote);
|
|
1292
1370
|
const hadOutDir = await pathExists(outDir);
|
|
1293
|
-
const backup = hadOutDir ? await fsp.mkdtemp((0,
|
|
1371
|
+
const backup = hadOutDir ? await fsp.mkdtemp((0, import_node_path8.join)(parent, ".lk-backup-")) : void 0;
|
|
1294
1372
|
if (hadOutDir && backup) {
|
|
1295
1373
|
await renameOrCopy(outDir, backup);
|
|
1296
1374
|
}
|
|
@@ -1301,7 +1379,7 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1301
1379
|
try {
|
|
1302
1380
|
await renameOrCopy(backup, outDir);
|
|
1303
1381
|
} catch (restoreError) {
|
|
1304
|
-
const failedPromote2 = (0,
|
|
1382
|
+
const failedPromote2 = (0, import_node_path8.join)(parent, `.lk-failed-promote-${(0, import_node_crypto.randomUUID)()}`);
|
|
1305
1383
|
try {
|
|
1306
1384
|
await renameOrCopy(tmpPromote, failedPromote2);
|
|
1307
1385
|
} catch {
|
|
@@ -1313,7 +1391,8 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1313
1391
|
const promoteMsg = promoteError instanceof Error ? promoteError.message : String(promoteError);
|
|
1314
1392
|
const restoreMsg = restoreError instanceof Error ? restoreError.message : String(restoreError);
|
|
1315
1393
|
throw new Error(
|
|
1316
|
-
`[lessonkit/lxpack] promote failed (${promoteMsg}) and could not restore ${outDir} (${restoreMsg}). Recovery: previous output may be in ${backup}; staged package may be in ${failedPromote2}
|
|
1394
|
+
`[lessonkit/lxpack] promote failed (${promoteMsg}) and could not restore ${outDir} (${restoreMsg}). Recovery: previous output may be in ${backup}; staged package may be in ${failedPromote2}.`,
|
|
1395
|
+
{ cause: restoreError }
|
|
1317
1396
|
);
|
|
1318
1397
|
}
|
|
1319
1398
|
} else {
|
|
@@ -1331,7 +1410,7 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1331
1410
|
}
|
|
1332
1411
|
throw promoteError;
|
|
1333
1412
|
}
|
|
1334
|
-
const failedPromote = (0,
|
|
1413
|
+
const failedPromote = (0, import_node_path8.join)(parent, `.lk-failed-promote-${(0, import_node_crypto.randomUUID)()}`);
|
|
1335
1414
|
try {
|
|
1336
1415
|
await renameOrCopy(tmpPromote, failedPromote);
|
|
1337
1416
|
} catch {
|
|
@@ -1353,16 +1432,17 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1353
1432
|
|
|
1354
1433
|
// src/packaging/staging.ts
|
|
1355
1434
|
var fsp2 = __toESM(require("fs/promises"), 1);
|
|
1356
|
-
var
|
|
1435
|
+
var import_node_path9 = require("path");
|
|
1357
1436
|
var import_node_os = require("os");
|
|
1358
1437
|
var import_api = require("@lxpack/api");
|
|
1359
1438
|
async function buildStagingPackage(options) {
|
|
1360
1439
|
const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
|
|
1361
|
-
const stagingDir = await fsp2.mkdtemp((0,
|
|
1440
|
+
const stagingDir = await fsp2.mkdtemp((0, import_node_path9.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
|
|
1362
1441
|
try {
|
|
1363
1442
|
let spaDirs;
|
|
1364
1443
|
try {
|
|
1365
1444
|
spaDirs = await resolveSpaDirs({ ...writeOpts, descriptor });
|
|
1445
|
+
await assertSpaDistContentsSafe(spaDirs, writeOpts.projectRoot);
|
|
1366
1446
|
} catch (err) {
|
|
1367
1447
|
return {
|
|
1368
1448
|
ok: false,
|
|
@@ -1375,10 +1455,21 @@ async function buildStagingPackage(options) {
|
|
|
1375
1455
|
]
|
|
1376
1456
|
};
|
|
1377
1457
|
}
|
|
1458
|
+
const injectableIssues = validateInjectableAssessments(descriptor);
|
|
1459
|
+
if (injectableIssues.length > 0) {
|
|
1460
|
+
return {
|
|
1461
|
+
ok: false,
|
|
1462
|
+
stagingDir,
|
|
1463
|
+
issues: injectableIssues.map((i) => ({
|
|
1464
|
+
path: i.path,
|
|
1465
|
+
message: i.message
|
|
1466
|
+
}))
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1378
1469
|
const interchange = descriptorToInterchange(descriptor);
|
|
1379
1470
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
1380
|
-
await fsp2.mkdir((0,
|
|
1381
|
-
const defaultOutput = output ?? (dir ? (0,
|
|
1471
|
+
await fsp2.mkdir((0, import_node_path9.join)(stagingDir, outputBase), { recursive: true });
|
|
1472
|
+
const defaultOutput = output ?? (dir ? (0, import_node_path9.join)(outputBase, target) : (0, import_node_path9.join)(outputBase, `course-${target}.zip`));
|
|
1382
1473
|
const build = await (0, import_api.packageLessonkit)({
|
|
1383
1474
|
interchange,
|
|
1384
1475
|
spaDirs,
|
|
@@ -1418,7 +1509,7 @@ async function buildStagingPackage(options) {
|
|
|
1418
1509
|
}
|
|
1419
1510
|
}
|
|
1420
1511
|
async function ensureOutDirParent(outDir) {
|
|
1421
|
-
await fsp2.mkdir((0,
|
|
1512
|
+
await fsp2.mkdir((0, import_node_path9.dirname)(outDir), { recursive: true });
|
|
1422
1513
|
}
|
|
1423
1514
|
|
|
1424
1515
|
// src/packaging/issueSeverity.ts
|
|
@@ -1433,13 +1524,13 @@ function findPackagingErrorIssues(issues) {
|
|
|
1433
1524
|
// src/packageCourse.ts
|
|
1434
1525
|
async function validateLessonkitProject(options) {
|
|
1435
1526
|
return (0, import_api2.validateCourse)({
|
|
1436
|
-
courseDir: (0,
|
|
1527
|
+
courseDir: (0, import_node_path10.resolve)(options.courseDir),
|
|
1437
1528
|
target: options.target
|
|
1438
1529
|
});
|
|
1439
1530
|
}
|
|
1440
1531
|
async function buildLessonkitProject(options) {
|
|
1441
1532
|
const buildOptions = {
|
|
1442
|
-
courseDir: (0,
|
|
1533
|
+
courseDir: (0, import_node_path10.resolve)(options.courseDir),
|
|
1443
1534
|
target: options.target,
|
|
1444
1535
|
output: options.output,
|
|
1445
1536
|
dir: options.dir,
|
|
@@ -1470,7 +1561,7 @@ async function packageLessonkitCourse(options) {
|
|
|
1470
1561
|
if (!descriptorValidation.ok) {
|
|
1471
1562
|
return {
|
|
1472
1563
|
ok: false,
|
|
1473
|
-
courseDir: (0,
|
|
1564
|
+
courseDir: (0, import_node_path10.resolve)(writeOpts.outDir),
|
|
1474
1565
|
target,
|
|
1475
1566
|
issues: descriptorValidation.issues.map((i) => ({
|
|
1476
1567
|
path: i.path,
|
|
@@ -1498,18 +1589,6 @@ async function packageLessonkitCourse(options) {
|
|
|
1498
1589
|
};
|
|
1499
1590
|
}
|
|
1500
1591
|
}
|
|
1501
|
-
const nonInjectableAssessments = (descriptor.assessments ?? []).map((assessment, index) => ({ assessment, index })).filter(({ assessment }) => assessmentDescriptorToLxpack(assessment) === null);
|
|
1502
|
-
if (nonInjectableAssessments.length > 0) {
|
|
1503
|
-
return {
|
|
1504
|
-
ok: false,
|
|
1505
|
-
courseDir: outDir,
|
|
1506
|
-
target,
|
|
1507
|
-
issues: nonInjectableAssessments.map(({ assessment, index }) => ({
|
|
1508
|
-
path: `assessments[${index}]`,
|
|
1509
|
-
message: `assessment kind "${assessment.kind ?? "mcq"}" (checkId "${assessment.checkId}") is not injected into LMS shell quizzes for target "${target}"`
|
|
1510
|
-
}))
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
1592
|
const staged = await buildStagingPackage({
|
|
1514
1593
|
...writeOpts,
|
|
1515
1594
|
descriptor,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { CheckId, CourseId, LessonId } from '@lessonkit/core';
|
|
1
|
+
import { McqAssessmentProps, CheckId, CourseId, LessonId } from '@lessonkit/core';
|
|
2
|
+
export { LmsBridgeMode, McqAssessmentProps } from '@lessonkit/core';
|
|
2
3
|
import { ThemePresetName, LessonkitThemeV1 } from '@lessonkit/themes';
|
|
3
4
|
import { ExportTarget, BuildCourseResult, ValidateCourseResult } from '@lxpack/api';
|
|
4
5
|
export { ExportTarget } from '@lxpack/api';
|
|
@@ -14,14 +15,8 @@ type LessonDescriptor = {
|
|
|
14
15
|
/** Built SPA folder relative to the LXPack project root (`per-lesson-spa` only). */
|
|
15
16
|
spaPath?: string;
|
|
16
17
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
checkId: CheckId;
|
|
20
|
-
question: string;
|
|
21
|
-
choices: string[];
|
|
22
|
-
answer: string;
|
|
23
|
-
passingScore?: number;
|
|
24
|
-
};
|
|
18
|
+
/** @deprecated Use `McqAssessmentProps` from `@lessonkit/core`. */
|
|
19
|
+
type McqAssessmentDescriptor = McqAssessmentProps;
|
|
25
20
|
type TrueFalseAssessmentDescriptor = {
|
|
26
21
|
kind: "trueFalse";
|
|
27
22
|
checkId: CheckId;
|
|
@@ -235,6 +230,8 @@ type BuildStagingPackageResult = {
|
|
|
235
230
|
declare function buildStagingPackage(options: BuildStagingPackageOptions): Promise<BuildStagingPackageResult>;
|
|
236
231
|
declare function ensureOutDirParent(outDir: string): Promise<void>;
|
|
237
232
|
|
|
233
|
+
/** LessonKit-owned alias for LMS export targets (maps to `@lxpack/api` `ExportTarget`). */
|
|
234
|
+
type LessonkitExportTarget = ExportTarget;
|
|
238
235
|
type ValidateLessonkitProjectOptions = {
|
|
239
236
|
courseDir: string;
|
|
240
237
|
target?: ExportTarget;
|
|
@@ -325,4 +322,4 @@ type ParseManifestResult = {
|
|
|
325
322
|
declare function parseLessonkitManifest(raw: unknown, label?: string, projectRoot?: string): ParseManifestResult;
|
|
326
323
|
declare function loadLessonkitManifestFromFile(readJson: () => Promise<unknown>, label?: string, projectRoot?: string): Promise<ParseManifestResult>;
|
|
327
324
|
|
|
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 };
|
|
325
|
+
export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type FillInBlanksAssessmentDescriptor, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitExportTarget, 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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { CheckId, CourseId, LessonId } from '@lessonkit/core';
|
|
1
|
+
import { McqAssessmentProps, CheckId, CourseId, LessonId } from '@lessonkit/core';
|
|
2
|
+
export { LmsBridgeMode, McqAssessmentProps } from '@lessonkit/core';
|
|
2
3
|
import { ThemePresetName, LessonkitThemeV1 } from '@lessonkit/themes';
|
|
3
4
|
import { ExportTarget, BuildCourseResult, ValidateCourseResult } from '@lxpack/api';
|
|
4
5
|
export { ExportTarget } from '@lxpack/api';
|
|
@@ -14,14 +15,8 @@ type LessonDescriptor = {
|
|
|
14
15
|
/** Built SPA folder relative to the LXPack project root (`per-lesson-spa` only). */
|
|
15
16
|
spaPath?: string;
|
|
16
17
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
checkId: CheckId;
|
|
20
|
-
question: string;
|
|
21
|
-
choices: string[];
|
|
22
|
-
answer: string;
|
|
23
|
-
passingScore?: number;
|
|
24
|
-
};
|
|
18
|
+
/** @deprecated Use `McqAssessmentProps` from `@lessonkit/core`. */
|
|
19
|
+
type McqAssessmentDescriptor = McqAssessmentProps;
|
|
25
20
|
type TrueFalseAssessmentDescriptor = {
|
|
26
21
|
kind: "trueFalse";
|
|
27
22
|
checkId: CheckId;
|
|
@@ -235,6 +230,8 @@ type BuildStagingPackageResult = {
|
|
|
235
230
|
declare function buildStagingPackage(options: BuildStagingPackageOptions): Promise<BuildStagingPackageResult>;
|
|
236
231
|
declare function ensureOutDirParent(outDir: string): Promise<void>;
|
|
237
232
|
|
|
233
|
+
/** LessonKit-owned alias for LMS export targets (maps to `@lxpack/api` `ExportTarget`). */
|
|
234
|
+
type LessonkitExportTarget = ExportTarget;
|
|
238
235
|
type ValidateLessonkitProjectOptions = {
|
|
239
236
|
courseDir: string;
|
|
240
237
|
target?: ExportTarget;
|
|
@@ -325,4 +322,4 @@ type ParseManifestResult = {
|
|
|
325
322
|
declare function parseLessonkitManifest(raw: unknown, label?: string, projectRoot?: string): ParseManifestResult;
|
|
326
323
|
declare function loadLessonkitManifestFromFile(readJson: () => Promise<unknown>, label?: string, projectRoot?: string): Promise<ParseManifestResult>;
|
|
327
324
|
|
|
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 };
|
|
325
|
+
export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type FillInBlanksAssessmentDescriptor, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitExportTarget, 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
|
@@ -315,6 +315,10 @@ var validateMcqLike = (assessment, path, issues) => {
|
|
|
315
315
|
} else if (trimmedChoices.length && !trimmedChoices.includes(assessment.answer.trim())) {
|
|
316
316
|
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
317
317
|
}
|
|
318
|
+
const uniqueChoices = new Set(trimmedChoices);
|
|
319
|
+
if (trimmedChoices.length !== uniqueChoices.size) {
|
|
320
|
+
issues.push({ path: `${path}.choices`, message: "choices must be unique" });
|
|
321
|
+
}
|
|
318
322
|
};
|
|
319
323
|
function countStarDelimitedBlanks(template) {
|
|
320
324
|
const matches = template.match(/\*[^*]+\*/g);
|
|
@@ -582,15 +586,8 @@ function assessmentDescriptorToLxpack(assessment) {
|
|
|
582
586
|
if (kind === "fillInBlanks") {
|
|
583
587
|
return null;
|
|
584
588
|
}
|
|
585
|
-
if (kind === "findHotspot"
|
|
586
|
-
return
|
|
587
|
-
kind: "mcq",
|
|
588
|
-
checkId: assessment.checkId,
|
|
589
|
-
question: assessment.question,
|
|
590
|
-
choices: [assessment.correctTargetId, "other"],
|
|
591
|
-
answer: assessment.correctTargetId,
|
|
592
|
-
passingScore: assessment.passingScore
|
|
593
|
-
});
|
|
589
|
+
if (kind === "findHotspot") {
|
|
590
|
+
return null;
|
|
594
591
|
}
|
|
595
592
|
if (kind === "findMultipleHotspots") {
|
|
596
593
|
return null;
|
|
@@ -604,6 +601,20 @@ function extractAssessments(descriptor) {
|
|
|
604
601
|
return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack).filter((a) => a !== null);
|
|
605
602
|
}
|
|
606
603
|
|
|
604
|
+
// src/descriptor/validateInjectableAssessments.ts
|
|
605
|
+
function validateInjectableAssessments(descriptor) {
|
|
606
|
+
const issues = [];
|
|
607
|
+
(descriptor.assessments ?? []).forEach((assessment, index) => {
|
|
608
|
+
if (assessmentDescriptorToLxpack(assessment) === null) {
|
|
609
|
+
issues.push({
|
|
610
|
+
path: `assessments[${index}]`,
|
|
611
|
+
message: `assessment kind "${assessment.kind ?? "mcq"}" (checkId "${assessment.checkId}") is not injected into LMS shell quizzes`
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
return issues;
|
|
616
|
+
}
|
|
617
|
+
|
|
607
618
|
// src/descriptor/validateForTarget.ts
|
|
608
619
|
var LMS_SHELL_TARGETS = /* @__PURE__ */ new Set([
|
|
609
620
|
"scorm12",
|
|
@@ -618,20 +629,21 @@ function validateDescriptorForExportTarget(descriptor, target) {
|
|
|
618
629
|
const activityIri = descriptor.tracking?.xapi?.activityIri?.trim();
|
|
619
630
|
if (!activityIri) {
|
|
620
631
|
issues.push({
|
|
621
|
-
path: "
|
|
632
|
+
path: "tracking.xapi.activityIri",
|
|
622
633
|
message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
|
|
623
634
|
});
|
|
635
|
+
} else if (!/^https:\/\/.+/i.test(activityIri)) {
|
|
636
|
+
issues.push({
|
|
637
|
+
path: "tracking.xapi.activityIri",
|
|
638
|
+
message: "tracking.xapi.activityIri must be an HTTPS URL for xapi and cmi5 export targets"
|
|
639
|
+
});
|
|
624
640
|
}
|
|
625
641
|
}
|
|
626
642
|
if (LMS_SHELL_TARGETS.has(target)) {
|
|
627
|
-
(descriptor
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
message: `assessment kind "${assessment.kind ?? "mcq"}" (checkId "${assessment.checkId}") is not injected into LMS shell quizzes for target "${target}"`
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
});
|
|
643
|
+
issues.push(...validateInjectableAssessments(descriptor).map((issue) => ({
|
|
644
|
+
...issue,
|
|
645
|
+
message: `${issue.message} for target "${target}"`
|
|
646
|
+
})));
|
|
635
647
|
}
|
|
636
648
|
return issues;
|
|
637
649
|
}
|
|
@@ -729,6 +741,10 @@ function checkIdPresent(source, checkId) {
|
|
|
729
741
|
if (idPropPatterns("checkId", checkId).some((p) => stripped.includes(p))) return true;
|
|
730
742
|
return idUsedViaConstant(stripped, "checkId", checkId, extractStringConstants(source));
|
|
731
743
|
}
|
|
744
|
+
var ID_SYNC_DOC = "https://lessonkit.readthedocs.io/en/latest/guides/react-developers/quickstart.html#keep-react-ids-in-sync-with-lessonkitjson";
|
|
745
|
+
function parityHint(message) {
|
|
746
|
+
return `${message} See ${ID_SYNC_DOC}`;
|
|
747
|
+
}
|
|
732
748
|
function validateReactManifestParity(opts) {
|
|
733
749
|
const appSources = opts.appSources ?? collectSourceUnderSrc(opts.projectRoot);
|
|
734
750
|
const source = readAppSources(opts.projectRoot, appSources);
|
|
@@ -747,7 +763,9 @@ function validateReactManifestParity(opts) {
|
|
|
747
763
|
if (!courseIdPresent(source, courseId)) {
|
|
748
764
|
issues.push({
|
|
749
765
|
path: "course.courseId",
|
|
750
|
-
message:
|
|
766
|
+
message: parityHint(
|
|
767
|
+
`React app source does not reference courseId="${courseId}" from lessonkit.json.`
|
|
768
|
+
),
|
|
751
769
|
severity: "error"
|
|
752
770
|
});
|
|
753
771
|
}
|
|
@@ -757,7 +775,9 @@ function validateReactManifestParity(opts) {
|
|
|
757
775
|
if (!checkIdPresent(source, checkId)) {
|
|
758
776
|
issues.push({
|
|
759
777
|
path: `assessments.checkId:${checkId}`,
|
|
760
|
-
message:
|
|
778
|
+
message: parityHint(
|
|
779
|
+
`React app source missing checkId="${checkId}" declared in lessonkit.json.`
|
|
780
|
+
),
|
|
761
781
|
severity: "error"
|
|
762
782
|
});
|
|
763
783
|
}
|
|
@@ -888,7 +908,7 @@ function descriptorToInterchange(descriptor) {
|
|
|
888
908
|
}
|
|
889
909
|
|
|
890
910
|
// src/writeProject.ts
|
|
891
|
-
import { join as
|
|
911
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
892
912
|
import { materializeLessonkitProject } from "@lxpack/validators";
|
|
893
913
|
|
|
894
914
|
// src/spaDirs.ts
|
|
@@ -946,6 +966,59 @@ async function resolveSpaDirs(options) {
|
|
|
946
966
|
return dirs;
|
|
947
967
|
}
|
|
948
968
|
|
|
969
|
+
// src/spaDistValidation.ts
|
|
970
|
+
import { lstat, readdir } from "fs/promises";
|
|
971
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
972
|
+
import { join as join4 } from "path";
|
|
973
|
+
async function assertSpaDistContentsSafe(spaDirs, projectRoot) {
|
|
974
|
+
for (const [label, dir] of Object.entries(spaDirs)) {
|
|
975
|
+
const dirResolved = resolveComparablePath(dir);
|
|
976
|
+
const dirStat = await lstat(dirResolved);
|
|
977
|
+
if (dirStat.isSymbolicLink()) {
|
|
978
|
+
throw new Error(`spa dist for "${label}" cannot be a symlink: ${dir}`);
|
|
979
|
+
}
|
|
980
|
+
let rootReal;
|
|
981
|
+
try {
|
|
982
|
+
rootReal = realpathSync2(dirResolved);
|
|
983
|
+
} catch {
|
|
984
|
+
throw new Error(`spa dist for "${label}" is not readable: ${dir}`);
|
|
985
|
+
}
|
|
986
|
+
if (projectRoot) {
|
|
987
|
+
assertRealPathUnderRoot(projectRoot, dir);
|
|
988
|
+
}
|
|
989
|
+
assertResolvedPathUnderRoot(rootReal, rootReal);
|
|
990
|
+
await walkDistDir(rootReal, rootReal, label);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
async function walkDistDir(rootReal, current, label) {
|
|
994
|
+
let entries;
|
|
995
|
+
try {
|
|
996
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
997
|
+
} catch (err) {
|
|
998
|
+
throw new Error(
|
|
999
|
+
`spa dist for "${label}" is not readable: ${err instanceof Error ? err.message : String(err)}`,
|
|
1000
|
+
{ cause: err }
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
for (const entry of entries) {
|
|
1004
|
+
const entryPath = join4(current, entry.name);
|
|
1005
|
+
const stat2 = await lstat(entryPath);
|
|
1006
|
+
if (stat2.isSymbolicLink()) {
|
|
1007
|
+
throw new Error(`spa dist for "${label}" contains symlink: ${entryPath}`);
|
|
1008
|
+
}
|
|
1009
|
+
let entryReal;
|
|
1010
|
+
try {
|
|
1011
|
+
entryReal = realpathSync2(entryPath);
|
|
1012
|
+
} catch {
|
|
1013
|
+
entryReal = entryPath;
|
|
1014
|
+
}
|
|
1015
|
+
assertResolvedPathUnderRoot(rootReal, entryReal);
|
|
1016
|
+
if (stat2.isDirectory()) {
|
|
1017
|
+
await walkDistDir(rootReal, entryPath, label);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
949
1022
|
// src/writeProject.ts
|
|
950
1023
|
async function writeLxpackProject(options) {
|
|
951
1024
|
const validation = validateDescriptor(options.descriptor);
|
|
@@ -955,11 +1028,16 @@ async function writeLxpackProject(options) {
|
|
|
955
1028
|
);
|
|
956
1029
|
}
|
|
957
1030
|
const descriptor = validation.descriptor;
|
|
1031
|
+
const injectableIssues = validateInjectableAssessments(descriptor);
|
|
1032
|
+
if (injectableIssues.length > 0) {
|
|
1033
|
+
throw new Error(injectableIssues.map((i) => `${i.path}: ${i.message}`).join("; "));
|
|
1034
|
+
}
|
|
958
1035
|
const outDir = resolve4(options.outDir);
|
|
959
1036
|
if (options.projectRoot) {
|
|
960
1037
|
assertRealPathUnderRoot(resolve4(options.projectRoot), outDir);
|
|
961
1038
|
}
|
|
962
1039
|
const spaDirs = await resolveSpaDirs({ ...options, descriptor });
|
|
1040
|
+
await assertSpaDistContentsSafe(spaDirs, options.projectRoot);
|
|
963
1041
|
const interchange = descriptorToInterchange(descriptor);
|
|
964
1042
|
const materialized = await materializeLessonkitProject({
|
|
965
1043
|
interchange,
|
|
@@ -975,8 +1053,8 @@ async function writeLxpackProject(options) {
|
|
|
975
1053
|
const courseDir = materialized.courseDir;
|
|
976
1054
|
return {
|
|
977
1055
|
outDir: courseDir,
|
|
978
|
-
courseYamlPath:
|
|
979
|
-
lessonkitJsonPath:
|
|
1056
|
+
courseYamlPath: join5(courseDir, "course.yaml"),
|
|
1057
|
+
lessonkitJsonPath: join5(courseDir, "lessonkit.json")
|
|
980
1058
|
};
|
|
981
1059
|
}
|
|
982
1060
|
|
|
@@ -989,7 +1067,7 @@ import {
|
|
|
989
1067
|
} from "@lxpack/api";
|
|
990
1068
|
|
|
991
1069
|
// src/packaging/validateInputs.ts
|
|
992
|
-
import { isAbsolute as isAbsolute3, join as
|
|
1070
|
+
import { isAbsolute as isAbsolute3, join as join6, resolve as resolve5, win32 as win322 } from "path";
|
|
993
1071
|
function validatePackageInputs(options) {
|
|
994
1072
|
const { target, output, outputBaseDir } = options;
|
|
995
1073
|
const outDir = resolve5(options.outDir);
|
|
@@ -1126,13 +1204,13 @@ function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
|
|
|
1126
1204
|
if (/^[a-zA-Z]:[/\\]/.test(outDir)) {
|
|
1127
1205
|
return win322.join(outDir, rel.replace(/\//g, win322.sep));
|
|
1128
1206
|
}
|
|
1129
|
-
return
|
|
1207
|
+
return join6(outDir, rel);
|
|
1130
1208
|
}
|
|
1131
1209
|
|
|
1132
1210
|
// src/packaging/promote.ts
|
|
1133
1211
|
import * as fsp from "fs/promises";
|
|
1134
1212
|
import { createHash, randomUUID } from "crypto";
|
|
1135
|
-
import { dirname, join as
|
|
1213
|
+
import { dirname, join as join7, resolve as resolve6 } from "path";
|
|
1136
1214
|
async function pathExists(path) {
|
|
1137
1215
|
try {
|
|
1138
1216
|
await fsp.access(path);
|
|
@@ -1154,7 +1232,7 @@ async function renameOrCopy(from, to) {
|
|
|
1154
1232
|
function promoteLockPath(outDir) {
|
|
1155
1233
|
const parent = dirname(outDir);
|
|
1156
1234
|
const hash = createHash("sha256").update(resolve6(outDir)).digest("hex").slice(0, 16);
|
|
1157
|
-
return
|
|
1235
|
+
return join7(parent, `.lk-promote-lock-${hash}`);
|
|
1158
1236
|
}
|
|
1159
1237
|
var STALE_LOCK_TTL_MS = 5 * 60 * 1e3;
|
|
1160
1238
|
async function isStalePromoteLock(lockPath) {
|
|
@@ -1230,10 +1308,10 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1230
1308
|
return withPromoteLock(outDir, async () => {
|
|
1231
1309
|
await assertNoLegacyPromoteArtifacts(outDir);
|
|
1232
1310
|
const parent = dirname(outDir);
|
|
1233
|
-
const tmpPromote = await fsp.mkdtemp(
|
|
1311
|
+
const tmpPromote = await fsp.mkdtemp(join7(parent, ".lk-promote-"));
|
|
1234
1312
|
await renameOrCopy(stagingDir, tmpPromote);
|
|
1235
1313
|
const hadOutDir = await pathExists(outDir);
|
|
1236
|
-
const backup = hadOutDir ? await fsp.mkdtemp(
|
|
1314
|
+
const backup = hadOutDir ? await fsp.mkdtemp(join7(parent, ".lk-backup-")) : void 0;
|
|
1237
1315
|
if (hadOutDir && backup) {
|
|
1238
1316
|
await renameOrCopy(outDir, backup);
|
|
1239
1317
|
}
|
|
@@ -1244,7 +1322,7 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1244
1322
|
try {
|
|
1245
1323
|
await renameOrCopy(backup, outDir);
|
|
1246
1324
|
} catch (restoreError) {
|
|
1247
|
-
const failedPromote2 =
|
|
1325
|
+
const failedPromote2 = join7(parent, `.lk-failed-promote-${randomUUID()}`);
|
|
1248
1326
|
try {
|
|
1249
1327
|
await renameOrCopy(tmpPromote, failedPromote2);
|
|
1250
1328
|
} catch {
|
|
@@ -1256,7 +1334,8 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1256
1334
|
const promoteMsg = promoteError instanceof Error ? promoteError.message : String(promoteError);
|
|
1257
1335
|
const restoreMsg = restoreError instanceof Error ? restoreError.message : String(restoreError);
|
|
1258
1336
|
throw new Error(
|
|
1259
|
-
`[lessonkit/lxpack] promote failed (${promoteMsg}) and could not restore ${outDir} (${restoreMsg}). Recovery: previous output may be in ${backup}; staged package may be in ${failedPromote2}
|
|
1337
|
+
`[lessonkit/lxpack] promote failed (${promoteMsg}) and could not restore ${outDir} (${restoreMsg}). Recovery: previous output may be in ${backup}; staged package may be in ${failedPromote2}.`,
|
|
1338
|
+
{ cause: restoreError }
|
|
1260
1339
|
);
|
|
1261
1340
|
}
|
|
1262
1341
|
} else {
|
|
@@ -1274,7 +1353,7 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1274
1353
|
}
|
|
1275
1354
|
throw promoteError;
|
|
1276
1355
|
}
|
|
1277
|
-
const failedPromote =
|
|
1356
|
+
const failedPromote = join7(parent, `.lk-failed-promote-${randomUUID()}`);
|
|
1278
1357
|
try {
|
|
1279
1358
|
await renameOrCopy(tmpPromote, failedPromote);
|
|
1280
1359
|
} catch {
|
|
@@ -1296,16 +1375,17 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
1296
1375
|
|
|
1297
1376
|
// src/packaging/staging.ts
|
|
1298
1377
|
import * as fsp2 from "fs/promises";
|
|
1299
|
-
import { dirname as dirname2, join as
|
|
1378
|
+
import { dirname as dirname2, join as join8 } from "path";
|
|
1300
1379
|
import { tmpdir } from "os";
|
|
1301
1380
|
import { packageLessonkit } from "@lxpack/api";
|
|
1302
1381
|
async function buildStagingPackage(options) {
|
|
1303
1382
|
const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
|
|
1304
|
-
const stagingDir = await fsp2.mkdtemp(
|
|
1383
|
+
const stagingDir = await fsp2.mkdtemp(join8(tmpdir(), "lessonkit-lxpack-"));
|
|
1305
1384
|
try {
|
|
1306
1385
|
let spaDirs;
|
|
1307
1386
|
try {
|
|
1308
1387
|
spaDirs = await resolveSpaDirs({ ...writeOpts, descriptor });
|
|
1388
|
+
await assertSpaDistContentsSafe(spaDirs, writeOpts.projectRoot);
|
|
1309
1389
|
} catch (err) {
|
|
1310
1390
|
return {
|
|
1311
1391
|
ok: false,
|
|
@@ -1318,10 +1398,21 @@ async function buildStagingPackage(options) {
|
|
|
1318
1398
|
]
|
|
1319
1399
|
};
|
|
1320
1400
|
}
|
|
1401
|
+
const injectableIssues = validateInjectableAssessments(descriptor);
|
|
1402
|
+
if (injectableIssues.length > 0) {
|
|
1403
|
+
return {
|
|
1404
|
+
ok: false,
|
|
1405
|
+
stagingDir,
|
|
1406
|
+
issues: injectableIssues.map((i) => ({
|
|
1407
|
+
path: i.path,
|
|
1408
|
+
message: i.message
|
|
1409
|
+
}))
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1321
1412
|
const interchange = descriptorToInterchange(descriptor);
|
|
1322
1413
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
1323
|
-
await fsp2.mkdir(
|
|
1324
|
-
const defaultOutput = output ?? (dir ?
|
|
1414
|
+
await fsp2.mkdir(join8(stagingDir, outputBase), { recursive: true });
|
|
1415
|
+
const defaultOutput = output ?? (dir ? join8(outputBase, target) : join8(outputBase, `course-${target}.zip`));
|
|
1325
1416
|
const build = await packageLessonkit({
|
|
1326
1417
|
interchange,
|
|
1327
1418
|
spaDirs,
|
|
@@ -1441,18 +1532,6 @@ async function packageLessonkitCourse(options) {
|
|
|
1441
1532
|
};
|
|
1442
1533
|
}
|
|
1443
1534
|
}
|
|
1444
|
-
const nonInjectableAssessments = (descriptor.assessments ?? []).map((assessment, index) => ({ assessment, index })).filter(({ assessment }) => assessmentDescriptorToLxpack(assessment) === null);
|
|
1445
|
-
if (nonInjectableAssessments.length > 0) {
|
|
1446
|
-
return {
|
|
1447
|
-
ok: false,
|
|
1448
|
-
courseDir: outDir,
|
|
1449
|
-
target,
|
|
1450
|
-
issues: nonInjectableAssessments.map(({ assessment, index }) => ({
|
|
1451
|
-
path: `assessments[${index}]`,
|
|
1452
|
-
message: `assessment kind "${assessment.kind ?? "mcq"}" (checkId "${assessment.checkId}") is not injected into LMS shell quizzes for target "${target}"`
|
|
1453
|
-
}))
|
|
1454
|
-
};
|
|
1455
|
-
}
|
|
1456
1535
|
const staged = await buildStagingPackage({
|
|
1457
1536
|
...writeOpts,
|
|
1458
1537
|
descriptor,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/lxpack",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "LXPack export adapter for LessonKit courses (SCORM, standalone, xAPI, cmi5).",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -55,15 +55,15 @@
|
|
|
55
55
|
"lint": "echo \"(no lint configured yet)\""
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@lessonkit/core": "1.
|
|
59
|
-
"@lessonkit/themes": "1.
|
|
60
|
-
"@lxpack/api": "
|
|
61
|
-
"@lxpack/spa-bridge": "
|
|
62
|
-
"@lxpack/tracking-schema": "
|
|
63
|
-
"@lxpack/validators": "
|
|
58
|
+
"@lessonkit/core": "1.4.0",
|
|
59
|
+
"@lessonkit/themes": "1.4.0",
|
|
60
|
+
"@lxpack/api": "0.6.4",
|
|
61
|
+
"@lxpack/spa-bridge": "0.6.4",
|
|
62
|
+
"@lxpack/tracking-schema": "0.6.4",
|
|
63
|
+
"@lxpack/validators": "0.6.4"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@types/node": "^
|
|
66
|
+
"@types/node": "^25.9.2",
|
|
67
67
|
"tsup": "^8.5.0",
|
|
68
68
|
"typescript": "^5.8.3",
|
|
69
69
|
"vitest": "^4.1.8"
|