@lessonkit/lxpack 0.9.3 → 1.0.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/README.md +16 -15
- package/dist/bridge.cjs +81 -8
- package/dist/bridge.d.cts +8 -2
- package/dist/bridge.d.ts +8 -2
- package/dist/bridge.js +65 -3
- package/dist/chunk-DYQI222N.js +41 -0
- package/dist/index.cjs +626 -141
- package/dist/index.d.cts +92 -7
- package/dist/index.d.ts +92 -7
- package/dist/index.js +609 -142
- package/lessonkit-manifest.v1.json +100 -0
- package/package.json +12 -10
- package/dist/chunk-PSUSESH3.js +0 -32
package/dist/index.cjs
CHANGED
|
@@ -33,21 +33,29 @@ __export(index_exports, {
|
|
|
33
33
|
LESSONKIT_TELEMETRY_EVENTS: () => import_tracking_schema2.LESSONKIT_TELEMETRY_EVENTS,
|
|
34
34
|
assessmentDescriptorToLxpack: () => assessmentDescriptorToLxpack,
|
|
35
35
|
buildLessonkitProject: () => buildLessonkitProject,
|
|
36
|
+
buildStagingPackage: () => buildStagingPackage,
|
|
36
37
|
descriptorToInterchange: () => descriptorToInterchange,
|
|
38
|
+
ensureOutDirParent: () => ensureOutDirParent,
|
|
37
39
|
extractAssessments: () => extractAssessments,
|
|
38
40
|
lessonkitInterchangeSchema: () => import_validators2.lessonkitInterchangeSchema,
|
|
41
|
+
loadLessonkitManifestFromFile: () => loadLessonkitManifestFromFile,
|
|
39
42
|
mapLessonkitIds: () => mapLessonkitIds,
|
|
40
43
|
mapLessonkitTelemetryToBridgeAction: () => import_tracking_schema2.mapLessonkitTelemetryToBridgeAction,
|
|
41
44
|
mapLessonkitTelemetryToLxpack: () => import_tracking_schema2.mapLessonkitTelemetryToLxpack,
|
|
42
45
|
materializeLessonkitProject: () => import_validators2.materializeLessonkitProject,
|
|
43
46
|
packageLessonkitCourse: () => packageLessonkitCourse,
|
|
44
47
|
parseLessonkitInterchange: () => import_validators2.parseLessonkitInterchange,
|
|
48
|
+
parseLessonkitManifest: () => parseLessonkitManifest,
|
|
49
|
+
promoteStagingToOutDir: () => promoteStagingToOutDir,
|
|
50
|
+
remapArtifactPaths: () => remapArtifactPaths,
|
|
45
51
|
resolveSafePackageOutputOverride: () => resolveSafePackageOutputOverride,
|
|
46
52
|
resolveSpaLessons: () => resolveSpaLessons,
|
|
47
53
|
telemetryEventToLessonkit: () => telemetryEventToLessonkit,
|
|
48
54
|
themeToLxpackRuntime: () => themeToLxpackRuntime,
|
|
49
55
|
validateDescriptor: () => validateDescriptor,
|
|
56
|
+
validateDescriptorForTarget: () => validateDescriptorForTarget,
|
|
50
57
|
validateLessonkitProject: () => validateLessonkitProject,
|
|
58
|
+
validatePackageInputs: () => validatePackageInputs,
|
|
51
59
|
validateProjectPaths: () => validateProjectPaths,
|
|
52
60
|
writeLxpackProject: () => writeLxpackProject
|
|
53
61
|
});
|
|
@@ -57,23 +65,73 @@ module.exports = __toCommonJS(index_exports);
|
|
|
57
65
|
var import_core = require("@lessonkit/core");
|
|
58
66
|
|
|
59
67
|
// src/spaPath.ts
|
|
68
|
+
var import_node_fs = require("fs");
|
|
60
69
|
var import_node_path = require("path");
|
|
70
|
+
function resolveComparablePath(p) {
|
|
71
|
+
if (/^[a-zA-Z]:[/\\]/.test(p)) {
|
|
72
|
+
return import_node_path.win32.resolve(p);
|
|
73
|
+
}
|
|
74
|
+
return (0, import_node_path.resolve)(p);
|
|
75
|
+
}
|
|
61
76
|
function isSafeRelativeSpaPath(spaPath) {
|
|
62
77
|
if (!spaPath.length || spaPath.includes("\0")) return false;
|
|
63
78
|
if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
|
|
64
|
-
if (/^[a-zA-Z]
|
|
65
|
-
|
|
79
|
+
if (/^[a-zA-Z]:/.test(spaPath)) return false;
|
|
80
|
+
if (spaPath === "." || spaPath === "./") return false;
|
|
81
|
+
const segments = spaPath.split(/[/\\]/).filter((s) => s.length > 0 && s !== ".");
|
|
66
82
|
if (segments.some((s) => s === "..")) return false;
|
|
67
|
-
return
|
|
83
|
+
return segments.length > 0;
|
|
68
84
|
}
|
|
69
85
|
function assertResolvedPathUnderRoot(root, target) {
|
|
70
|
-
const rootResolved = (
|
|
71
|
-
const targetResolved = (
|
|
86
|
+
const rootResolved = resolveComparablePath(root);
|
|
87
|
+
const targetResolved = resolveComparablePath(target);
|
|
72
88
|
const prefix = rootResolved.endsWith(import_node_path.sep) ? rootResolved : rootResolved + import_node_path.sep;
|
|
73
|
-
|
|
89
|
+
const win32Prefix = rootResolved.endsWith(import_node_path.win32.sep) ? rootResolved : rootResolved + import_node_path.win32.sep;
|
|
90
|
+
if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix) && !targetResolved.startsWith(win32Prefix)) {
|
|
74
91
|
throw new Error(`unsafe path escapes project root: ${target}`);
|
|
75
92
|
}
|
|
76
93
|
}
|
|
94
|
+
function assertRealPathUnderRoot(root, target) {
|
|
95
|
+
const rootResolved = resolveComparablePath(root);
|
|
96
|
+
const targetResolved = resolveComparablePath(target);
|
|
97
|
+
let rootReal;
|
|
98
|
+
try {
|
|
99
|
+
rootReal = (0, import_node_fs.realpathSync)(rootResolved);
|
|
100
|
+
} catch {
|
|
101
|
+
rootReal = rootResolved;
|
|
102
|
+
}
|
|
103
|
+
let targetCheck;
|
|
104
|
+
try {
|
|
105
|
+
targetCheck = (0, import_node_fs.realpathSync)(targetResolved);
|
|
106
|
+
} catch {
|
|
107
|
+
const rel = (0, import_node_path.relative)(rootResolved, targetResolved);
|
|
108
|
+
if (rel.startsWith("..") || rel.includes(`..${import_node_path.sep}`)) {
|
|
109
|
+
throw new Error(`unsafe path escapes project root: ${target}`);
|
|
110
|
+
}
|
|
111
|
+
targetCheck = (0, import_node_path.resolve)(rootReal, rel);
|
|
112
|
+
}
|
|
113
|
+
assertResolvedPathUnderRoot(rootReal, targetCheck);
|
|
114
|
+
}
|
|
115
|
+
function normalizePathForComparison(p) {
|
|
116
|
+
const resolved = resolveComparablePath(p);
|
|
117
|
+
return /^[a-zA-Z]:[/\\]/.test(resolved) ? resolved.toLowerCase() : resolved;
|
|
118
|
+
}
|
|
119
|
+
function relativePathUnderRoot(root, target) {
|
|
120
|
+
const rootResolved = normalizePathForComparison(root);
|
|
121
|
+
const targetResolved = normalizePathForComparison(target);
|
|
122
|
+
if (/^[a-zA-Z]:[/\\]/.test(rootResolved)) {
|
|
123
|
+
return import_node_path.win32.relative(rootResolved, targetResolved);
|
|
124
|
+
}
|
|
125
|
+
return (0, import_node_path.relative)(rootResolved, targetResolved);
|
|
126
|
+
}
|
|
127
|
+
function isResolvedPathUnderRoot(root, target) {
|
|
128
|
+
const rootResolved = normalizePathForComparison(root);
|
|
129
|
+
const targetResolved = normalizePathForComparison(target);
|
|
130
|
+
if (targetResolved === rootResolved) return true;
|
|
131
|
+
const rel = relativePathUnderRoot(root, target);
|
|
132
|
+
if (!rel) return true;
|
|
133
|
+
return !rel.startsWith("..") && !(0, import_node_path.isAbsolute)(rel);
|
|
134
|
+
}
|
|
77
135
|
|
|
78
136
|
// src/theme.ts
|
|
79
137
|
var import_themes = require("@lessonkit/themes");
|
|
@@ -93,6 +151,70 @@ function themeToLxpackRuntime(input) {
|
|
|
93
151
|
// src/validateDescriptor.ts
|
|
94
152
|
var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
|
|
95
153
|
var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
|
|
154
|
+
function isRecord(value) {
|
|
155
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
156
|
+
}
|
|
157
|
+
function parseLessonDescriptor(raw) {
|
|
158
|
+
if (!isRecord(raw)) {
|
|
159
|
+
return { id: "", title: "" };
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
id: typeof raw.id === "string" ? raw.id : "",
|
|
163
|
+
title: typeof raw.title === "string" ? raw.title : "",
|
|
164
|
+
spaPath: typeof raw.spaPath === "string" ? raw.spaPath : void 0
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function parseAssessmentDescriptor(raw) {
|
|
168
|
+
if (!isRecord(raw)) {
|
|
169
|
+
return { checkId: "", question: "", choices: [], answer: "" };
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
checkId: typeof raw.checkId === "string" ? raw.checkId : "",
|
|
173
|
+
question: typeof raw.question === "string" ? raw.question : "",
|
|
174
|
+
choices: Array.isArray(raw.choices) ? raw.choices.filter((c) => typeof c === "string") : [],
|
|
175
|
+
answer: typeof raw.answer === "string" ? raw.answer : "",
|
|
176
|
+
passingScore: typeof raw.passingScore === "number" ? raw.passingScore : void 0
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function parseCourseDescriptorInput(input) {
|
|
180
|
+
if (!isRecord(input)) return null;
|
|
181
|
+
const trackingRaw = input.tracking;
|
|
182
|
+
let tracking;
|
|
183
|
+
if (isRecord(trackingRaw)) {
|
|
184
|
+
const completionRaw = trackingRaw.completion;
|
|
185
|
+
const xapiRaw = trackingRaw.xapi;
|
|
186
|
+
tracking = {
|
|
187
|
+
completion: isRecord(completionRaw) ? {
|
|
188
|
+
threshold: typeof completionRaw.threshold === "number" ? completionRaw.threshold : void 0
|
|
189
|
+
} : void 0,
|
|
190
|
+
xapi: isRecord(xapiRaw) ? {
|
|
191
|
+
activityIri: typeof xapiRaw.activityIri === "string" ? xapiRaw.activityIri : void 0
|
|
192
|
+
} : void 0
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const themeRaw = input.theme;
|
|
196
|
+
let theme;
|
|
197
|
+
if (isRecord(themeRaw)) {
|
|
198
|
+
theme = {
|
|
199
|
+
preset: typeof themeRaw.preset === "string" ? themeRaw.preset : void 0
|
|
200
|
+
};
|
|
201
|
+
if (isRecord(themeRaw.theme)) {
|
|
202
|
+
theme.theme = themeRaw.theme;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
courseId: typeof input.courseId === "string" ? input.courseId : "",
|
|
207
|
+
title: typeof input.title === "string" ? input.title : "",
|
|
208
|
+
version: typeof input.version === "string" ? input.version : void 0,
|
|
209
|
+
layout: typeof input.layout === "string" ? input.layout : void 0,
|
|
210
|
+
lessons: Array.isArray(input.lessons) ? input.lessons.map(parseLessonDescriptor) : [],
|
|
211
|
+
assessments: Array.isArray(input.assessments) ? input.assessments.map(parseAssessmentDescriptor) : void 0,
|
|
212
|
+
theme,
|
|
213
|
+
tracking,
|
|
214
|
+
spaDistDir: typeof input.spaDistDir === "string" ? input.spaDistDir : void 0,
|
|
215
|
+
spaLessonId: typeof input.spaLessonId === "string" ? input.spaLessonId : void 0
|
|
216
|
+
};
|
|
217
|
+
}
|
|
96
218
|
function normalizeDescriptor(input) {
|
|
97
219
|
const course = (0, import_core.validateId)(input.courseId, "courseId");
|
|
98
220
|
if (!course.ok) throw new Error("normalizeDescriptor called with invalid courseId");
|
|
@@ -126,6 +248,31 @@ function normalizeDescriptor(input) {
|
|
|
126
248
|
};
|
|
127
249
|
}
|
|
128
250
|
function validateDescriptor(input) {
|
|
251
|
+
const parsed = parseCourseDescriptorInput(input);
|
|
252
|
+
if (parsed === null) {
|
|
253
|
+
return { ok: false, issues: [{ path: "course", message: "must be an object" }] };
|
|
254
|
+
}
|
|
255
|
+
return validateDescriptorParsed(parsed);
|
|
256
|
+
}
|
|
257
|
+
function validateDescriptorForTarget(input, target) {
|
|
258
|
+
const result = validateDescriptor(input);
|
|
259
|
+
if (!result.ok || !target) return result;
|
|
260
|
+
if (target !== "xapi" && target !== "cmi5") return result;
|
|
261
|
+
const activityIri = result.descriptor.tracking?.xapi?.activityIri?.trim();
|
|
262
|
+
if (!activityIri) {
|
|
263
|
+
return {
|
|
264
|
+
ok: false,
|
|
265
|
+
issues: [
|
|
266
|
+
{
|
|
267
|
+
path: "course.tracking.xapi.activityIri",
|
|
268
|
+
message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
|
|
269
|
+
}
|
|
270
|
+
]
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
function validateDescriptorParsed(input) {
|
|
129
276
|
const issues = [];
|
|
130
277
|
const course = (0, import_core.validateId)(input.courseId, "courseId");
|
|
131
278
|
if (!course.ok) issues.push(...course.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
@@ -249,7 +396,7 @@ function validateDescriptor(input) {
|
|
|
249
396
|
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
250
397
|
}
|
|
251
398
|
const passingScore = assessment.passingScore;
|
|
252
|
-
if (passingScore !== void 0 && !(passingScore > 0)) {
|
|
399
|
+
if (passingScore !== void 0 && !(Number.isFinite(passingScore) && passingScore > 0)) {
|
|
253
400
|
issues.push({
|
|
254
401
|
path: `${path}.passingScore`,
|
|
255
402
|
message: "passingScore must be greater than 0 (absolute point threshold)"
|
|
@@ -271,7 +418,7 @@ function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
|
271
418
|
return;
|
|
272
419
|
}
|
|
273
420
|
try {
|
|
274
|
-
|
|
421
|
+
assertRealPathUnderRoot(projectRoot, (0, import_node_path2.resolve)(projectRoot, value));
|
|
275
422
|
} catch {
|
|
276
423
|
issues.push({
|
|
277
424
|
path: fieldPath,
|
|
@@ -301,14 +448,14 @@ function resolveSafePackageOutputOverride(projectRoot, override) {
|
|
|
301
448
|
}
|
|
302
449
|
if ((0, import_node_path2.isAbsolute)(trimmed)) {
|
|
303
450
|
const resolved2 = (0, import_node_path2.resolve)(trimmed);
|
|
304
|
-
|
|
451
|
+
assertRealPathUnderRoot(root, resolved2);
|
|
305
452
|
return resolved2;
|
|
306
453
|
}
|
|
307
454
|
if (!isSafeRelativeSpaPath(trimmed)) {
|
|
308
455
|
throw new Error(`unsafe output path: ${override}`);
|
|
309
456
|
}
|
|
310
457
|
const resolved = (0, import_node_path2.resolve)(root, trimmed);
|
|
311
|
-
|
|
458
|
+
assertRealPathUnderRoot(root, resolved);
|
|
312
459
|
return resolved;
|
|
313
460
|
}
|
|
314
461
|
|
|
@@ -355,6 +502,18 @@ function extractAssessments(descriptor) {
|
|
|
355
502
|
}
|
|
356
503
|
|
|
357
504
|
// src/interchange.ts
|
|
505
|
+
function mapDescriptorTracking(tracking) {
|
|
506
|
+
if (!tracking) return void 0;
|
|
507
|
+
const mapped = {};
|
|
508
|
+
if (tracking.completion?.threshold !== void 0) {
|
|
509
|
+
mapped.completion = { threshold: tracking.completion.threshold };
|
|
510
|
+
}
|
|
511
|
+
const activityIri = tracking.xapi?.activityIri?.trim();
|
|
512
|
+
if (activityIri) {
|
|
513
|
+
mapped.xapi = { activityIri };
|
|
514
|
+
}
|
|
515
|
+
return Object.keys(mapped).length > 0 ? mapped : void 0;
|
|
516
|
+
}
|
|
358
517
|
function resolveSpaLessons(descriptor) {
|
|
359
518
|
const mapped = mapLessonkitIds(descriptor);
|
|
360
519
|
if (descriptor.layout === "single-spa") {
|
|
@@ -392,7 +551,7 @@ function descriptorToInterchange(descriptor) {
|
|
|
392
551
|
type: "spa",
|
|
393
552
|
path: l.path
|
|
394
553
|
})),
|
|
395
|
-
tracking: descriptor.tracking,
|
|
554
|
+
tracking: mapDescriptorTracking(descriptor.tracking),
|
|
396
555
|
runtime: runtime ? {
|
|
397
556
|
theme: runtime.theme,
|
|
398
557
|
cssVariables: runtime.cssVariables
|
|
@@ -415,13 +574,18 @@ async function resolveSpaDirs(options) {
|
|
|
415
574
|
const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? "dist";
|
|
416
575
|
const srcDist = projectRoot ? (0, import_node_path3.resolve)(projectRoot, spaDistRelative) : (0, import_node_path3.resolve)(spaDistRelative);
|
|
417
576
|
if (projectRoot) {
|
|
418
|
-
|
|
577
|
+
assertRealPathUnderRoot((0, import_node_path3.resolve)(projectRoot), srcDist);
|
|
419
578
|
}
|
|
420
579
|
try {
|
|
421
580
|
await (0, import_promises.access)(srcDist);
|
|
422
581
|
} catch {
|
|
423
582
|
throw new Error(`spaDistDir not found: ${srcDist}`);
|
|
424
583
|
}
|
|
584
|
+
try {
|
|
585
|
+
await (0, import_promises.access)((0, import_node_path3.join)(srcDist, "index.html"));
|
|
586
|
+
} catch {
|
|
587
|
+
throw new Error(`spaDistDir must contain index.html: ${(0, import_node_path3.join)(srcDist, "index.html")}`);
|
|
588
|
+
}
|
|
425
589
|
const lessonId = spaLessons[0]?.id ?? "main";
|
|
426
590
|
return { [lessonId]: srcDist };
|
|
427
591
|
}
|
|
@@ -434,7 +598,19 @@ async function resolveSpaDirs(options) {
|
|
|
434
598
|
}
|
|
435
599
|
const resolved = projectRoot ? (0, import_node_path3.resolve)(projectRoot, src) : (0, import_node_path3.resolve)(src);
|
|
436
600
|
if (projectRoot) {
|
|
437
|
-
|
|
601
|
+
assertRealPathUnderRoot((0, import_node_path3.resolve)(projectRoot), resolved);
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
await (0, import_promises.access)(resolved);
|
|
605
|
+
} catch {
|
|
606
|
+
throw new Error(`lessonSpaDirs path not found for lesson "${lesson.id}": ${resolved}`);
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
await (0, import_promises.access)((0, import_node_path3.join)(resolved, "index.html"));
|
|
610
|
+
} catch {
|
|
611
|
+
throw new Error(
|
|
612
|
+
`lessonSpaDirs must contain index.html for lesson "${lesson.id}": ${(0, import_node_path3.join)(resolved, "index.html")}`
|
|
613
|
+
);
|
|
438
614
|
}
|
|
439
615
|
dirs[lesson.id] = resolved;
|
|
440
616
|
}
|
|
@@ -452,7 +628,7 @@ async function writeLxpackProject(options) {
|
|
|
452
628
|
const descriptor = validation.descriptor;
|
|
453
629
|
const outDir = (0, import_node_path4.resolve)(options.outDir);
|
|
454
630
|
if (options.projectRoot) {
|
|
455
|
-
|
|
631
|
+
assertRealPathUnderRoot((0, import_node_path4.resolve)(options.projectRoot), outDir);
|
|
456
632
|
}
|
|
457
633
|
const spaDirs = await resolveSpaDirs({ ...options, descriptor });
|
|
458
634
|
const interchange = descriptorToInterchange(descriptor);
|
|
@@ -476,68 +652,27 @@ async function writeLxpackProject(options) {
|
|
|
476
652
|
}
|
|
477
653
|
|
|
478
654
|
// src/packageCourse.ts
|
|
479
|
-
var
|
|
655
|
+
var import_node_path8 = require("path");
|
|
656
|
+
var fsp3 = __toESM(require("fs/promises"), 1);
|
|
657
|
+
var import_api2 = require("@lxpack/api");
|
|
658
|
+
|
|
659
|
+
// src/packaging/validateInputs.ts
|
|
480
660
|
var import_node_path5 = require("path");
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
courseDir: (0, import_node_path5.resolve)(options.courseDir),
|
|
486
|
-
target: options.target
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
async function buildLessonkitProject(options) {
|
|
490
|
-
return (0, import_api.buildCourse)({
|
|
491
|
-
courseDir: (0, import_node_path5.resolve)(options.courseDir),
|
|
492
|
-
target: options.target,
|
|
493
|
-
output: options.output,
|
|
494
|
-
dir: options.dir,
|
|
495
|
-
outputBaseDir: options.outputBaseDir,
|
|
496
|
-
assessments: options.assessments
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
async function pathExists(path) {
|
|
500
|
-
try {
|
|
501
|
-
await fsp.access(path);
|
|
502
|
-
return true;
|
|
503
|
-
} catch {
|
|
504
|
-
return false;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
508
|
-
const tmpPromote = `${outDir}.tmp-promote`;
|
|
509
|
-
const backup = `${outDir}.bak`;
|
|
510
|
-
await fsp.rename(stagingDir, tmpPromote);
|
|
511
|
-
const hadOutDir = await pathExists(outDir);
|
|
512
|
-
if (hadOutDir) {
|
|
513
|
-
await fsp.rename(outDir, backup);
|
|
514
|
-
}
|
|
515
|
-
try {
|
|
516
|
-
await fsp.rename(tmpPromote, outDir);
|
|
517
|
-
} catch (promoteError) {
|
|
518
|
-
if (hadOutDir) {
|
|
519
|
-
try {
|
|
520
|
-
await fsp.rename(backup, outDir);
|
|
521
|
-
} catch (restoreError) {
|
|
522
|
-
console.warn(
|
|
523
|
-
`[lessonkit/lxpack] failed to restore ${outDir} after promote error:`,
|
|
524
|
-
restoreError instanceof Error ? restoreError.message : restoreError
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
529
|
-
throw promoteError;
|
|
530
|
-
}
|
|
531
|
-
if (hadOutDir) {
|
|
532
|
-
await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
async function packageLessonkitCourse(options) {
|
|
536
|
-
const { target, output, dir, outputBaseDir, ...writeOpts } = options;
|
|
537
|
-
const outDir = (0, import_node_path5.resolve)(writeOpts.outDir);
|
|
538
|
-
const projectRoot = writeOpts.projectRoot ? (0, import_node_path5.resolve)(writeOpts.projectRoot) : void 0;
|
|
661
|
+
function validatePackageInputs(options) {
|
|
662
|
+
const { target, output, outputBaseDir } = options;
|
|
663
|
+
const outDir = (0, import_node_path5.resolve)(options.outDir);
|
|
664
|
+
const projectRoot = options.projectRoot ? (0, import_node_path5.resolve)(options.projectRoot) : void 0;
|
|
539
665
|
if (projectRoot) {
|
|
540
|
-
|
|
666
|
+
try {
|
|
667
|
+
assertRealPathUnderRoot(projectRoot, outDir);
|
|
668
|
+
} catch (err) {
|
|
669
|
+
return {
|
|
670
|
+
ok: false,
|
|
671
|
+
courseDir: outDir,
|
|
672
|
+
target,
|
|
673
|
+
issues: [{ path: "outDir", message: err instanceof Error ? err.message : String(err) }]
|
|
674
|
+
};
|
|
675
|
+
}
|
|
541
676
|
}
|
|
542
677
|
if (outputBaseDir && !isSafeRelativeSpaPath(outputBaseDir)) {
|
|
543
678
|
return {
|
|
@@ -547,10 +682,18 @@ async function packageLessonkitCourse(options) {
|
|
|
547
682
|
issues: [{ path: "outputBaseDir", message: `unsafe outputBaseDir: ${outputBaseDir}` }]
|
|
548
683
|
};
|
|
549
684
|
}
|
|
550
|
-
if (projectRoot && output) {
|
|
551
|
-
|
|
685
|
+
if (output && !projectRoot && !isSafeRelativeSpaPath(output)) {
|
|
686
|
+
return {
|
|
687
|
+
ok: false,
|
|
688
|
+
courseDir: outDir,
|
|
689
|
+
target,
|
|
690
|
+
issues: [{ path: "output", message: `unsafe output: ${output}` }]
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
if (projectRoot && outputBaseDir) {
|
|
694
|
+
const resolvedOutputBase = (0, import_node_path5.resolve)(projectRoot, outputBaseDir);
|
|
552
695
|
try {
|
|
553
|
-
|
|
696
|
+
assertRealPathUnderRoot(projectRoot, resolvedOutputBase);
|
|
554
697
|
} catch (err) {
|
|
555
698
|
return {
|
|
556
699
|
ok: false,
|
|
@@ -558,28 +701,152 @@ async function packageLessonkitCourse(options) {
|
|
|
558
701
|
target,
|
|
559
702
|
issues: [
|
|
560
703
|
{
|
|
561
|
-
path: "
|
|
704
|
+
path: "outputBaseDir",
|
|
562
705
|
message: err instanceof Error ? err.message : String(err)
|
|
563
706
|
}
|
|
564
707
|
]
|
|
565
708
|
};
|
|
566
709
|
}
|
|
567
710
|
}
|
|
568
|
-
|
|
569
|
-
|
|
711
|
+
if (projectRoot && output) {
|
|
712
|
+
const resolvedOutput = (0, import_node_path5.resolve)(projectRoot, output);
|
|
713
|
+
try {
|
|
714
|
+
assertRealPathUnderRoot(projectRoot, resolvedOutput);
|
|
715
|
+
} catch (err) {
|
|
716
|
+
return {
|
|
717
|
+
ok: false,
|
|
718
|
+
courseDir: outDir,
|
|
719
|
+
target,
|
|
720
|
+
issues: [{ path: "output", message: err instanceof Error ? err.message : String(err) }]
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return { ok: true, outDir, projectRoot };
|
|
725
|
+
}
|
|
726
|
+
function validateArtifactInStaging(stagingRoot, artifactPath, field) {
|
|
727
|
+
if (!artifactPath) return null;
|
|
728
|
+
const resolved = resolveComparablePath(artifactPath);
|
|
729
|
+
if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
|
|
570
730
|
return {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
target,
|
|
574
|
-
issues: descriptorValidation.issues.map((i) => ({
|
|
575
|
-
path: i.path,
|
|
576
|
-
message: i.message
|
|
577
|
-
}))
|
|
731
|
+
path: field,
|
|
732
|
+
message: `${field} is outside the staging directory: ${artifactPath}`
|
|
578
733
|
};
|
|
579
734
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
|
|
738
|
+
if (!artifactPath) return void 0;
|
|
739
|
+
const resolved = resolveComparablePath(artifactPath);
|
|
740
|
+
if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
|
|
741
|
+
return artifactPath;
|
|
742
|
+
}
|
|
743
|
+
const rel = relativePathUnderRoot(stagingRoot, resolved);
|
|
744
|
+
if (rel.startsWith("..") || (0, import_node_path5.isAbsolute)(rel)) {
|
|
745
|
+
return artifactPath;
|
|
746
|
+
}
|
|
747
|
+
if (!rel) return outDir;
|
|
748
|
+
if (/^[a-zA-Z]:[/\\]/.test(outDir)) {
|
|
749
|
+
return import_node_path5.win32.join(outDir, rel.replace(/\//g, import_node_path5.win32.sep));
|
|
750
|
+
}
|
|
751
|
+
return (0, import_node_path5.join)(outDir, rel);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/packaging/promote.ts
|
|
755
|
+
var fsp = __toESM(require("fs/promises"), 1);
|
|
756
|
+
var import_node_crypto = require("crypto");
|
|
757
|
+
var import_node_path6 = require("path");
|
|
758
|
+
async function pathExists(path) {
|
|
759
|
+
try {
|
|
760
|
+
await fsp.access(path);
|
|
761
|
+
return true;
|
|
762
|
+
} catch {
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
async function renameOrCopy(from, to) {
|
|
767
|
+
try {
|
|
768
|
+
await fsp.rename(from, to);
|
|
769
|
+
} catch (err) {
|
|
770
|
+
const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
|
|
771
|
+
if (code !== "EXDEV") throw err;
|
|
772
|
+
await fsp.cp(from, to, { recursive: true });
|
|
773
|
+
await fsp.rm(from, { recursive: true, force: true });
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async function assertNoLegacyPromoteArtifacts(outDir) {
|
|
777
|
+
const legacyTmp = `${outDir}.tmp-promote`;
|
|
778
|
+
const legacyBak = `${outDir}.bak`;
|
|
779
|
+
const stale = [];
|
|
780
|
+
if (await pathExists(legacyTmp)) stale.push(legacyTmp);
|
|
781
|
+
if (await pathExists(legacyBak)) stale.push(legacyBak);
|
|
782
|
+
if (stale.length) {
|
|
783
|
+
throw new Error(
|
|
784
|
+
`[lessonkit/lxpack] cannot promote: remove stale packaging artifacts from a previous failed run: ${stale.join(", ")}`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
789
|
+
await assertNoLegacyPromoteArtifacts(outDir);
|
|
790
|
+
const parent = (0, import_node_path6.dirname)(outDir);
|
|
791
|
+
const tmpPromote = await fsp.mkdtemp((0, import_node_path6.join)(parent, ".lk-promote-"));
|
|
792
|
+
await renameOrCopy(stagingDir, tmpPromote);
|
|
793
|
+
const hadOutDir = await pathExists(outDir);
|
|
794
|
+
const backup = hadOutDir ? await fsp.mkdtemp((0, import_node_path6.join)(parent, ".lk-backup-")) : void 0;
|
|
795
|
+
if (hadOutDir && backup) {
|
|
796
|
+
await renameOrCopy(outDir, backup);
|
|
797
|
+
}
|
|
798
|
+
try {
|
|
799
|
+
await renameOrCopy(tmpPromote, outDir);
|
|
800
|
+
} catch (promoteError) {
|
|
801
|
+
if (hadOutDir && backup) {
|
|
802
|
+
try {
|
|
803
|
+
await renameOrCopy(backup, outDir);
|
|
804
|
+
} catch (restoreError) {
|
|
805
|
+
const failedPromote2 = (0, import_node_path6.join)(parent, `.lk-failed-promote-${(0, import_node_crypto.randomUUID)()}`);
|
|
806
|
+
try {
|
|
807
|
+
await renameOrCopy(tmpPromote, failedPromote2);
|
|
808
|
+
} catch {
|
|
809
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
810
|
+
}
|
|
811
|
+
const promoteMsg = promoteError instanceof Error ? promoteError.message : String(promoteError);
|
|
812
|
+
const restoreMsg = restoreError instanceof Error ? restoreError.message : String(restoreError);
|
|
813
|
+
throw new Error(
|
|
814
|
+
`[lessonkit/lxpack] promote failed (${promoteMsg}) and could not restore ${outDir} (${restoreMsg}). Recovery: previous output may be in ${backup}; staged package may be in ${failedPromote2}.`
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
} else {
|
|
818
|
+
try {
|
|
819
|
+
await renameOrCopy(tmpPromote, stagingDir);
|
|
820
|
+
} catch (restoreError) {
|
|
821
|
+
console.warn(
|
|
822
|
+
`[lessonkit/lxpack] failed to restore ${stagingDir} after promote error:`,
|
|
823
|
+
restoreError instanceof Error ? restoreError.message : restoreError
|
|
824
|
+
);
|
|
825
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
826
|
+
}
|
|
827
|
+
throw promoteError;
|
|
828
|
+
}
|
|
829
|
+
const failedPromote = (0, import_node_path6.join)(parent, `.lk-failed-promote-${(0, import_node_crypto.randomUUID)()}`);
|
|
830
|
+
try {
|
|
831
|
+
await renameOrCopy(tmpPromote, failedPromote);
|
|
832
|
+
} catch {
|
|
833
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
834
|
+
}
|
|
835
|
+
throw promoteError;
|
|
836
|
+
}
|
|
837
|
+
if (backup) {
|
|
838
|
+
await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/packaging/staging.ts
|
|
843
|
+
var fsp2 = __toESM(require("fs/promises"), 1);
|
|
844
|
+
var import_node_path7 = require("path");
|
|
845
|
+
var import_node_os = require("os");
|
|
846
|
+
var import_api = require("@lxpack/api");
|
|
847
|
+
async function buildStagingPackage(options) {
|
|
848
|
+
const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
|
|
849
|
+
const stagingDir = await fsp2.mkdtemp((0, import_node_path7.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
|
|
583
850
|
try {
|
|
584
851
|
let spaDirs;
|
|
585
852
|
try {
|
|
@@ -587,8 +854,7 @@ async function packageLessonkitCourse(options) {
|
|
|
587
854
|
} catch (err) {
|
|
588
855
|
return {
|
|
589
856
|
ok: false,
|
|
590
|
-
|
|
591
|
-
target,
|
|
857
|
+
stagingDir,
|
|
592
858
|
issues: [
|
|
593
859
|
{
|
|
594
860
|
path: "spaDirs",
|
|
@@ -599,8 +865,8 @@ async function packageLessonkitCourse(options) {
|
|
|
599
865
|
}
|
|
600
866
|
const interchange = descriptorToInterchange(descriptor);
|
|
601
867
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
602
|
-
await
|
|
603
|
-
const defaultOutput = output ?? (dir ? (0,
|
|
868
|
+
await fsp2.mkdir((0, import_node_path7.join)(stagingDir, outputBase), { recursive: true });
|
|
869
|
+
const defaultOutput = output ?? (dir ? (0, import_node_path7.join)(outputBase, target) : (0, import_node_path7.join)(outputBase, `course-${target}.zip`));
|
|
604
870
|
const build = await (0, import_api.packageLessonkit)({
|
|
605
871
|
interchange,
|
|
606
872
|
spaDirs,
|
|
@@ -613,15 +879,9 @@ async function packageLessonkitCourse(options) {
|
|
|
613
879
|
writeAuthoringFiles: true
|
|
614
880
|
});
|
|
615
881
|
if (!build.ok) {
|
|
616
|
-
const validation2 = {
|
|
617
|
-
ok: false,
|
|
618
|
-
issues: build.issues
|
|
619
|
-
};
|
|
620
882
|
return {
|
|
621
883
|
ok: false,
|
|
622
|
-
|
|
623
|
-
target,
|
|
624
|
-
validation: validation2,
|
|
884
|
+
stagingDir,
|
|
625
885
|
build,
|
|
626
886
|
issues: build.issues.map((i) => ({
|
|
627
887
|
path: i.path,
|
|
@@ -630,48 +890,256 @@ async function packageLessonkitCourse(options) {
|
|
|
630
890
|
}))
|
|
631
891
|
};
|
|
632
892
|
}
|
|
633
|
-
|
|
893
|
+
return {
|
|
634
894
|
ok: true,
|
|
635
|
-
|
|
636
|
-
|
|
895
|
+
stagingDir,
|
|
896
|
+
build,
|
|
897
|
+
outputPath: "outputPath" in build ? build.outputPath : void 0,
|
|
898
|
+
outputDir: "outputDir" in build ? build.outputDir : void 0
|
|
637
899
|
};
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
900
|
+
} catch (err) {
|
|
901
|
+
await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
902
|
+
throw err;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
async function ensureOutDirParent(outDir) {
|
|
906
|
+
await fsp2.mkdir((0, import_node_path7.dirname)(outDir), { recursive: true });
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/packageCourse.ts
|
|
910
|
+
async function validateLessonkitProject(options) {
|
|
911
|
+
return (0, import_api2.validateCourse)({
|
|
912
|
+
courseDir: (0, import_node_path8.resolve)(options.courseDir),
|
|
913
|
+
target: options.target
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
async function buildLessonkitProject(options) {
|
|
917
|
+
const buildOptions = {
|
|
918
|
+
courseDir: (0, import_node_path8.resolve)(options.courseDir),
|
|
919
|
+
target: options.target,
|
|
920
|
+
output: options.output,
|
|
921
|
+
dir: options.dir,
|
|
922
|
+
outputBaseDir: options.outputBaseDir,
|
|
923
|
+
assessments: options.assessments
|
|
924
|
+
};
|
|
925
|
+
return (0, import_api2.buildCourse)(buildOptions);
|
|
926
|
+
}
|
|
927
|
+
async function packageLessonkitCourse(options) {
|
|
928
|
+
const { target, output, dir, outputBaseDir, ...writeOpts } = options;
|
|
929
|
+
const inputValidation = validatePackageInputs({
|
|
930
|
+
target,
|
|
931
|
+
output,
|
|
932
|
+
outputBaseDir,
|
|
933
|
+
outDir: writeOpts.outDir,
|
|
934
|
+
projectRoot: writeOpts.projectRoot
|
|
935
|
+
});
|
|
936
|
+
if (!inputValidation.ok) {
|
|
937
|
+
return {
|
|
938
|
+
ok: false,
|
|
939
|
+
courseDir: inputValidation.courseDir,
|
|
940
|
+
target: inputValidation.target,
|
|
941
|
+
issues: inputValidation.issues
|
|
646
942
|
};
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
943
|
+
}
|
|
944
|
+
const outDir = inputValidation.outDir;
|
|
945
|
+
const descriptorValidation = validateDescriptorForTarget(writeOpts.descriptor, target);
|
|
946
|
+
if (!descriptorValidation.ok) {
|
|
947
|
+
return {
|
|
948
|
+
ok: false,
|
|
949
|
+
courseDir: outDir,
|
|
950
|
+
target,
|
|
951
|
+
issues: descriptorValidation.issues.map((i) => ({
|
|
952
|
+
path: i.path,
|
|
953
|
+
message: i.message
|
|
954
|
+
}))
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
const descriptor = descriptorValidation.descriptor;
|
|
958
|
+
const staged = await buildStagingPackage({
|
|
959
|
+
...writeOpts,
|
|
960
|
+
descriptor,
|
|
961
|
+
target,
|
|
962
|
+
output,
|
|
963
|
+
dir,
|
|
964
|
+
outputBaseDir
|
|
965
|
+
});
|
|
966
|
+
if (!staged.ok) {
|
|
967
|
+
await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
968
|
+
const validation2 = staged.build ? { ok: false, issues: staged.build.issues } : void 0;
|
|
969
|
+
return {
|
|
970
|
+
ok: false,
|
|
971
|
+
courseDir: outDir,
|
|
972
|
+
target,
|
|
973
|
+
validation: validation2,
|
|
974
|
+
build: staged.build,
|
|
975
|
+
issues: staged.issues
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
const { stagingDir, build } = staged;
|
|
979
|
+
const stagingRoot = await fsp3.realpath(stagingDir);
|
|
980
|
+
const artifactIssues = [
|
|
981
|
+
validateArtifactInStaging(stagingRoot, staged.outputPath, "outputPath"),
|
|
982
|
+
validateArtifactInStaging(stagingRoot, staged.outputDir, "outputDir")
|
|
983
|
+
].filter((issue) => issue != null);
|
|
984
|
+
if (artifactIssues.length > 0) {
|
|
985
|
+
await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
986
|
+
return {
|
|
987
|
+
ok: false,
|
|
988
|
+
courseDir: outDir,
|
|
989
|
+
target,
|
|
990
|
+
validation: { ok: true, manifest: build.manifest, issues: build.issues },
|
|
991
|
+
build,
|
|
992
|
+
issues: artifactIssues
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
const remappedOutputPath = remapArtifactPaths(stagingRoot, outDir, staged.outputPath);
|
|
996
|
+
const remappedOutputDir = remapArtifactPaths(stagingRoot, outDir, staged.outputDir);
|
|
997
|
+
const validation = {
|
|
998
|
+
ok: true,
|
|
999
|
+
manifest: build.manifest,
|
|
1000
|
+
issues: build.issues
|
|
1001
|
+
};
|
|
1002
|
+
try {
|
|
1003
|
+
await ensureOutDirParent(outDir);
|
|
652
1004
|
await promoteStagingToOutDir(stagingDir, outDir);
|
|
653
|
-
|
|
654
|
-
const remappedBuild = { ...build };
|
|
655
|
-
if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
|
|
656
|
-
remappedBuild.outputPath = remappedOutputPath;
|
|
657
|
-
}
|
|
658
|
-
if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
|
|
659
|
-
remappedBuild.outputDir = remappedOutputDir;
|
|
660
|
-
}
|
|
1005
|
+
} catch (err) {
|
|
661
1006
|
return {
|
|
662
|
-
ok:
|
|
1007
|
+
ok: false,
|
|
663
1008
|
courseDir: outDir,
|
|
664
1009
|
target,
|
|
665
|
-
outputPath: remappedOutputPath,
|
|
666
|
-
outputDir: remappedOutputDir,
|
|
667
|
-
fileCount: build.fileCount,
|
|
668
1010
|
validation,
|
|
669
|
-
build
|
|
1011
|
+
build,
|
|
1012
|
+
issues: [
|
|
1013
|
+
{
|
|
1014
|
+
path: "promote",
|
|
1015
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1016
|
+
}
|
|
1017
|
+
]
|
|
670
1018
|
};
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
|
|
1019
|
+
}
|
|
1020
|
+
const remappedBuild = { ...build };
|
|
1021
|
+
if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
|
|
1022
|
+
remappedBuild.outputPath = remappedOutputPath;
|
|
1023
|
+
}
|
|
1024
|
+
if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
|
|
1025
|
+
remappedBuild.outputDir = remappedOutputDir;
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
ok: true,
|
|
1029
|
+
courseDir: outDir,
|
|
1030
|
+
target,
|
|
1031
|
+
outputPath: remappedOutputPath,
|
|
1032
|
+
outputDir: remappedOutputDir,
|
|
1033
|
+
fileCount: build.fileCount,
|
|
1034
|
+
validation,
|
|
1035
|
+
build: remappedBuild
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/manifest.ts
|
|
1040
|
+
var DEFAULT_PATHS = {
|
|
1041
|
+
spaDistDir: "dist",
|
|
1042
|
+
lxpackOutDir: ".lxpack/course",
|
|
1043
|
+
outputBaseDir: ".lxpack/out"
|
|
1044
|
+
};
|
|
1045
|
+
function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
|
|
1046
|
+
if (!raw || typeof raw !== "object") {
|
|
1047
|
+
return { ok: false, issues: [{ path: label, message: "must be a JSON object" }] };
|
|
1048
|
+
}
|
|
1049
|
+
const config = raw;
|
|
1050
|
+
const issues = [];
|
|
1051
|
+
let schemaVersion = config.schemaVersion;
|
|
1052
|
+
if (schemaVersion === "1") {
|
|
1053
|
+
schemaVersion = 1;
|
|
1054
|
+
}
|
|
1055
|
+
if (schemaVersion !== 1) {
|
|
1056
|
+
issues.push({
|
|
1057
|
+
path: "schemaVersion",
|
|
1058
|
+
message: `must be 1 (got ${String(config.schemaVersion)})`
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
const nameRaw = config.name;
|
|
1062
|
+
const name = typeof nameRaw === "string" ? nameRaw.trim() : "";
|
|
1063
|
+
if (!name) {
|
|
1064
|
+
issues.push({ path: "name", message: "must be a non-empty string" });
|
|
1065
|
+
}
|
|
1066
|
+
const courseRaw = config.course;
|
|
1067
|
+
if (Array.isArray(courseRaw)) {
|
|
1068
|
+
issues.push({ path: "course", message: "must be an object, not an array" });
|
|
1069
|
+
return { ok: false, issues };
|
|
1070
|
+
}
|
|
1071
|
+
if (!courseRaw || typeof courseRaw !== "object") {
|
|
1072
|
+
issues.push({ path: "course", message: "must be an object" });
|
|
1073
|
+
return { ok: false, issues };
|
|
1074
|
+
}
|
|
1075
|
+
const courseObj = courseRaw;
|
|
1076
|
+
if (courseObj.lessons !== void 0 && !Array.isArray(courseObj.lessons)) {
|
|
1077
|
+
issues.push({ path: "course.lessons", message: "must be an array" });
|
|
1078
|
+
}
|
|
1079
|
+
if (courseObj.assessments !== void 0 && !Array.isArray(courseObj.assessments)) {
|
|
1080
|
+
issues.push({ path: "course.assessments", message: "must be an array" });
|
|
1081
|
+
}
|
|
1082
|
+
if (issues.length) return { ok: false, issues };
|
|
1083
|
+
const validation = validateDescriptor(courseRaw);
|
|
1084
|
+
if (!validation.ok) {
|
|
1085
|
+
for (const i of validation.issues) {
|
|
1086
|
+
issues.push({
|
|
1087
|
+
path: i.path.startsWith("course.") ? i.path : `course.${i.path}`,
|
|
1088
|
+
message: i.message
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
} else if (validation.descriptor.layout === "per-lesson-spa") {
|
|
1092
|
+
issues.push({
|
|
1093
|
+
path: "course.layout",
|
|
1094
|
+
message: "per-lesson-spa is not supported by lessonkit package yet. Use single-spa or package via @lessonkit/lxpack directly."
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
const paths = { ...DEFAULT_PATHS };
|
|
1098
|
+
const pathsRaw = config.paths;
|
|
1099
|
+
if (pathsRaw !== void 0 && (typeof pathsRaw !== "object" || pathsRaw === null)) {
|
|
1100
|
+
issues.push({ path: "paths", message: "must be an object" });
|
|
1101
|
+
} else if (pathsRaw && typeof pathsRaw === "object") {
|
|
1102
|
+
const p = pathsRaw;
|
|
1103
|
+
for (const key of ["spaDistDir", "lxpackOutDir", "outputBaseDir"]) {
|
|
1104
|
+
if (p[key] !== void 0) {
|
|
1105
|
+
if (typeof p[key] !== "string" || !p[key].trim()) {
|
|
1106
|
+
issues.push({ path: `paths.${key}`, message: "must be a non-empty string" });
|
|
1107
|
+
} else {
|
|
1108
|
+
paths[key] = p[key].trim();
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
const courseSpaDistDir = validation.ok ? validation.descriptor.spaDistDir?.trim() : void 0;
|
|
1114
|
+
if (courseSpaDistDir && courseSpaDistDir !== paths.spaDistDir) {
|
|
1115
|
+
issues.push({
|
|
1116
|
+
path: "course.spaDistDir",
|
|
1117
|
+
message: `"course.spaDistDir" (${courseSpaDistDir}) differs from "paths.spaDistDir" (${paths.spaDistDir}). Use paths.spaDistDir for CLI build and package.`
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
if (projectRoot) {
|
|
1121
|
+
const pathIssues = validateProjectPaths(projectRoot, paths);
|
|
1122
|
+
for (const pi of pathIssues) {
|
|
1123
|
+
issues.push({ path: pi.path, message: pi.message });
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (issues.length) return { ok: false, issues };
|
|
1127
|
+
if (!validation.ok) return { ok: false, issues };
|
|
1128
|
+
return {
|
|
1129
|
+
ok: true,
|
|
1130
|
+
manifest: {
|
|
1131
|
+
schemaVersion: 1,
|
|
1132
|
+
name,
|
|
1133
|
+
course: validation.descriptor,
|
|
1134
|
+
paths
|
|
674
1135
|
}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
async function loadLessonkitManifestFromFile(readJson, label = "lessonkit.json", projectRoot) {
|
|
1139
|
+
try {
|
|
1140
|
+
return parseLessonkitManifest(await readJson(), label, projectRoot);
|
|
1141
|
+
} catch {
|
|
1142
|
+
return { ok: false, issues: [{ path: label, message: "failed to read or parse JSON" }] };
|
|
675
1143
|
}
|
|
676
1144
|
}
|
|
677
1145
|
|
|
@@ -681,6 +1149,15 @@ var import_tracking_schema2 = require("@lxpack/tracking-schema");
|
|
|
681
1149
|
// src/telemetry.ts
|
|
682
1150
|
var import_tracking_schema = require("@lxpack/tracking-schema");
|
|
683
1151
|
var SUPPORTED = new Set(import_tracking_schema.LESSONKIT_TELEMETRY_EVENTS);
|
|
1152
|
+
function isQuizAnsweredData(data) {
|
|
1153
|
+
return typeof data === "object" && data !== null && typeof data.checkId === "string";
|
|
1154
|
+
}
|
|
1155
|
+
function isQuizCompletedData(data) {
|
|
1156
|
+
return typeof data === "object" && data !== null && typeof data.checkId === "string";
|
|
1157
|
+
}
|
|
1158
|
+
function isInteractionData(data) {
|
|
1159
|
+
return typeof data === "object" && data !== null;
|
|
1160
|
+
}
|
|
684
1161
|
function telemetryEventToLessonkit(event) {
|
|
685
1162
|
if (!SUPPORTED.has(event.name)) {
|
|
686
1163
|
return null;
|
|
@@ -692,16 +1169,16 @@ function telemetryEventToLessonkit(event) {
|
|
|
692
1169
|
};
|
|
693
1170
|
if (name === "quiz_completed" || name === "quiz_answered") {
|
|
694
1171
|
const data = event.data;
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
1172
|
+
if (isQuizAnsweredData(data) || isQuizCompletedData(data)) {
|
|
1173
|
+
mapped.assessmentId = data.checkId;
|
|
1174
|
+
if ("score" in data) {
|
|
1175
|
+
mapped.score = data.score;
|
|
1176
|
+
mapped.maxScore = data.maxScore;
|
|
1177
|
+
mapped.passingScore = data.passingScore;
|
|
1178
|
+
}
|
|
702
1179
|
mapped.data = data;
|
|
703
1180
|
}
|
|
704
|
-
} else if (name === "interaction" && event.data) {
|
|
1181
|
+
} else if (name === "interaction" && event.data && isInteractionData(event.data)) {
|
|
705
1182
|
mapped.data = event.data;
|
|
706
1183
|
}
|
|
707
1184
|
return mapped;
|
|
@@ -714,21 +1191,29 @@ var import_validators2 = require("@lxpack/validators");
|
|
|
714
1191
|
LESSONKIT_TELEMETRY_EVENTS,
|
|
715
1192
|
assessmentDescriptorToLxpack,
|
|
716
1193
|
buildLessonkitProject,
|
|
1194
|
+
buildStagingPackage,
|
|
717
1195
|
descriptorToInterchange,
|
|
1196
|
+
ensureOutDirParent,
|
|
718
1197
|
extractAssessments,
|
|
719
1198
|
lessonkitInterchangeSchema,
|
|
1199
|
+
loadLessonkitManifestFromFile,
|
|
720
1200
|
mapLessonkitIds,
|
|
721
1201
|
mapLessonkitTelemetryToBridgeAction,
|
|
722
1202
|
mapLessonkitTelemetryToLxpack,
|
|
723
1203
|
materializeLessonkitProject,
|
|
724
1204
|
packageLessonkitCourse,
|
|
725
1205
|
parseLessonkitInterchange,
|
|
1206
|
+
parseLessonkitManifest,
|
|
1207
|
+
promoteStagingToOutDir,
|
|
1208
|
+
remapArtifactPaths,
|
|
726
1209
|
resolveSafePackageOutputOverride,
|
|
727
1210
|
resolveSpaLessons,
|
|
728
1211
|
telemetryEventToLessonkit,
|
|
729
1212
|
themeToLxpackRuntime,
|
|
730
1213
|
validateDescriptor,
|
|
1214
|
+
validateDescriptorForTarget,
|
|
731
1215
|
validateLessonkitProject,
|
|
1216
|
+
validatePackageInputs,
|
|
732
1217
|
validateProjectPaths,
|
|
733
1218
|
writeLxpackProject
|
|
734
1219
|
});
|