@lessonkit/lxpack 0.9.3 → 1.0.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 +16 -15
- package/dist/bridge.cjs +64 -0
- package/dist/bridge.d.cts +8 -2
- package/dist/bridge.d.ts +8 -2
- package/dist/bridge.js +64 -2
- package/dist/index.cjs +472 -120
- package/dist/index.d.cts +82 -2
- package/dist/index.d.ts +82 -2
- package/dist/index.js +470 -126
- package/package.json +8 -8
package/dist/index.cjs
CHANGED
|
@@ -33,21 +33,28 @@ __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,
|
|
50
56
|
validateLessonkitProject: () => validateLessonkitProject,
|
|
57
|
+
validatePackageInputs: () => validatePackageInputs,
|
|
51
58
|
validateProjectPaths: () => validateProjectPaths,
|
|
52
59
|
writeLxpackProject: () => writeLxpackProject
|
|
53
60
|
});
|
|
@@ -57,7 +64,14 @@ module.exports = __toCommonJS(index_exports);
|
|
|
57
64
|
var import_core = require("@lessonkit/core");
|
|
58
65
|
|
|
59
66
|
// src/spaPath.ts
|
|
67
|
+
var import_node_fs = require("fs");
|
|
60
68
|
var import_node_path = require("path");
|
|
69
|
+
function resolveComparablePath(p) {
|
|
70
|
+
if (/^[a-zA-Z]:[/\\]/.test(p)) {
|
|
71
|
+
return import_node_path.win32.resolve(p);
|
|
72
|
+
}
|
|
73
|
+
return (0, import_node_path.resolve)(p);
|
|
74
|
+
}
|
|
61
75
|
function isSafeRelativeSpaPath(spaPath) {
|
|
62
76
|
if (!spaPath.length || spaPath.includes("\0")) return false;
|
|
63
77
|
if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
|
|
@@ -67,13 +81,43 @@ function isSafeRelativeSpaPath(spaPath) {
|
|
|
67
81
|
return true;
|
|
68
82
|
}
|
|
69
83
|
function assertResolvedPathUnderRoot(root, target) {
|
|
70
|
-
const rootResolved = (
|
|
71
|
-
const targetResolved = (
|
|
84
|
+
const rootResolved = resolveComparablePath(root);
|
|
85
|
+
const targetResolved = resolveComparablePath(target);
|
|
72
86
|
const prefix = rootResolved.endsWith(import_node_path.sep) ? rootResolved : rootResolved + import_node_path.sep;
|
|
73
|
-
|
|
87
|
+
const win32Prefix = rootResolved.endsWith(import_node_path.win32.sep) ? rootResolved : rootResolved + import_node_path.win32.sep;
|
|
88
|
+
if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix) && !targetResolved.startsWith(win32Prefix)) {
|
|
74
89
|
throw new Error(`unsafe path escapes project root: ${target}`);
|
|
75
90
|
}
|
|
76
91
|
}
|
|
92
|
+
function assertRealPathUnderRoot(root, target) {
|
|
93
|
+
const rootResolved = resolveComparablePath(root);
|
|
94
|
+
const targetResolved = resolveComparablePath(target);
|
|
95
|
+
let rootReal;
|
|
96
|
+
try {
|
|
97
|
+
rootReal = (0, import_node_fs.realpathSync)(rootResolved);
|
|
98
|
+
} catch {
|
|
99
|
+
rootReal = rootResolved;
|
|
100
|
+
}
|
|
101
|
+
let targetCheck;
|
|
102
|
+
try {
|
|
103
|
+
targetCheck = (0, import_node_fs.realpathSync)(targetResolved);
|
|
104
|
+
} catch {
|
|
105
|
+
const rel = (0, import_node_path.relative)(rootResolved, targetResolved);
|
|
106
|
+
if (rel.startsWith("..") || rel.includes(`..${import_node_path.sep}`)) {
|
|
107
|
+
throw new Error(`unsafe path escapes project root: ${target}`);
|
|
108
|
+
}
|
|
109
|
+
targetCheck = (0, import_node_path.resolve)(rootReal, rel);
|
|
110
|
+
}
|
|
111
|
+
assertResolvedPathUnderRoot(rootReal, targetCheck);
|
|
112
|
+
}
|
|
113
|
+
function isResolvedPathUnderRoot(root, target) {
|
|
114
|
+
const rootResolved = resolveComparablePath(root);
|
|
115
|
+
const targetResolved = resolveComparablePath(target);
|
|
116
|
+
if (targetResolved === rootResolved) return true;
|
|
117
|
+
const prefix = rootResolved.endsWith(import_node_path.sep) ? rootResolved : rootResolved + import_node_path.sep;
|
|
118
|
+
const win32Prefix = rootResolved.endsWith(import_node_path.win32.sep) ? rootResolved : rootResolved + import_node_path.win32.sep;
|
|
119
|
+
return targetResolved.startsWith(prefix) || targetResolved.startsWith(win32Prefix);
|
|
120
|
+
}
|
|
77
121
|
|
|
78
122
|
// src/theme.ts
|
|
79
123
|
var import_themes = require("@lessonkit/themes");
|
|
@@ -249,7 +293,7 @@ function validateDescriptor(input) {
|
|
|
249
293
|
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
250
294
|
}
|
|
251
295
|
const passingScore = assessment.passingScore;
|
|
252
|
-
if (passingScore !== void 0 && !(passingScore > 0)) {
|
|
296
|
+
if (passingScore !== void 0 && !(Number.isFinite(passingScore) && passingScore > 0)) {
|
|
253
297
|
issues.push({
|
|
254
298
|
path: `${path}.passingScore`,
|
|
255
299
|
message: "passingScore must be greater than 0 (absolute point threshold)"
|
|
@@ -415,13 +459,18 @@ async function resolveSpaDirs(options) {
|
|
|
415
459
|
const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? "dist";
|
|
416
460
|
const srcDist = projectRoot ? (0, import_node_path3.resolve)(projectRoot, spaDistRelative) : (0, import_node_path3.resolve)(spaDistRelative);
|
|
417
461
|
if (projectRoot) {
|
|
418
|
-
|
|
462
|
+
assertRealPathUnderRoot((0, import_node_path3.resolve)(projectRoot), srcDist);
|
|
419
463
|
}
|
|
420
464
|
try {
|
|
421
465
|
await (0, import_promises.access)(srcDist);
|
|
422
466
|
} catch {
|
|
423
467
|
throw new Error(`spaDistDir not found: ${srcDist}`);
|
|
424
468
|
}
|
|
469
|
+
try {
|
|
470
|
+
await (0, import_promises.access)((0, import_node_path3.join)(srcDist, "index.html"));
|
|
471
|
+
} catch {
|
|
472
|
+
throw new Error(`spaDistDir must contain index.html: ${(0, import_node_path3.join)(srcDist, "index.html")}`);
|
|
473
|
+
}
|
|
425
474
|
const lessonId = spaLessons[0]?.id ?? "main";
|
|
426
475
|
return { [lessonId]: srcDist };
|
|
427
476
|
}
|
|
@@ -434,7 +483,19 @@ async function resolveSpaDirs(options) {
|
|
|
434
483
|
}
|
|
435
484
|
const resolved = projectRoot ? (0, import_node_path3.resolve)(projectRoot, src) : (0, import_node_path3.resolve)(src);
|
|
436
485
|
if (projectRoot) {
|
|
437
|
-
|
|
486
|
+
assertRealPathUnderRoot((0, import_node_path3.resolve)(projectRoot), resolved);
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
await (0, import_promises.access)(resolved);
|
|
490
|
+
} catch {
|
|
491
|
+
throw new Error(`lessonSpaDirs path not found for lesson "${lesson.id}": ${resolved}`);
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
await (0, import_promises.access)((0, import_node_path3.join)(resolved, "index.html"));
|
|
495
|
+
} catch {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`lessonSpaDirs must contain index.html for lesson "${lesson.id}": ${(0, import_node_path3.join)(resolved, "index.html")}`
|
|
498
|
+
);
|
|
438
499
|
}
|
|
439
500
|
dirs[lesson.id] = resolved;
|
|
440
501
|
}
|
|
@@ -476,26 +537,105 @@ async function writeLxpackProject(options) {
|
|
|
476
537
|
}
|
|
477
538
|
|
|
478
539
|
// src/packageCourse.ts
|
|
479
|
-
var
|
|
540
|
+
var import_node_path7 = require("path");
|
|
541
|
+
var fsp3 = __toESM(require("fs/promises"), 1);
|
|
542
|
+
var import_api2 = require("@lxpack/api");
|
|
543
|
+
|
|
544
|
+
// src/packaging/validateInputs.ts
|
|
480
545
|
var import_node_path5 = require("path");
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
546
|
+
function validatePackageInputs(options) {
|
|
547
|
+
const { target, output, outputBaseDir } = options;
|
|
548
|
+
const outDir = (0, import_node_path5.resolve)(options.outDir);
|
|
549
|
+
const projectRoot = options.projectRoot ? (0, import_node_path5.resolve)(options.projectRoot) : void 0;
|
|
550
|
+
if (projectRoot) {
|
|
551
|
+
try {
|
|
552
|
+
assertResolvedPathUnderRoot(projectRoot, outDir);
|
|
553
|
+
} catch (err) {
|
|
554
|
+
return {
|
|
555
|
+
ok: false,
|
|
556
|
+
courseDir: outDir,
|
|
557
|
+
target,
|
|
558
|
+
issues: [{ path: "outDir", message: err instanceof Error ? err.message : String(err) }]
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (outputBaseDir && !isSafeRelativeSpaPath(outputBaseDir)) {
|
|
563
|
+
return {
|
|
564
|
+
ok: false,
|
|
565
|
+
courseDir: outDir,
|
|
566
|
+
target,
|
|
567
|
+
issues: [{ path: "outputBaseDir", message: `unsafe outputBaseDir: ${outputBaseDir}` }]
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (output && !projectRoot && !isSafeRelativeSpaPath(output)) {
|
|
571
|
+
return {
|
|
572
|
+
ok: false,
|
|
573
|
+
courseDir: outDir,
|
|
574
|
+
target,
|
|
575
|
+
issues: [{ path: "output", message: `unsafe output: ${output}` }]
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
if (projectRoot && outputBaseDir) {
|
|
579
|
+
const resolvedOutputBase = (0, import_node_path5.resolve)(projectRoot, outputBaseDir);
|
|
580
|
+
try {
|
|
581
|
+
assertRealPathUnderRoot(projectRoot, resolvedOutputBase);
|
|
582
|
+
} catch (err) {
|
|
583
|
+
return {
|
|
584
|
+
ok: false,
|
|
585
|
+
courseDir: outDir,
|
|
586
|
+
target,
|
|
587
|
+
issues: [
|
|
588
|
+
{
|
|
589
|
+
path: "outputBaseDir",
|
|
590
|
+
message: err instanceof Error ? err.message : String(err)
|
|
591
|
+
}
|
|
592
|
+
]
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (projectRoot && output) {
|
|
597
|
+
const resolvedOutput = (0, import_node_path5.resolve)(projectRoot, output);
|
|
598
|
+
try {
|
|
599
|
+
assertResolvedPathUnderRoot(projectRoot, resolvedOutput);
|
|
600
|
+
} catch (err) {
|
|
601
|
+
return {
|
|
602
|
+
ok: false,
|
|
603
|
+
courseDir: outDir,
|
|
604
|
+
target,
|
|
605
|
+
issues: [{ path: "output", message: err instanceof Error ? err.message : String(err) }]
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return { ok: true, outDir, projectRoot };
|
|
488
610
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
}
|
|
611
|
+
function validateArtifactInStaging(stagingRoot, artifactPath, field) {
|
|
612
|
+
if (!artifactPath) return null;
|
|
613
|
+
const resolved = resolveComparablePath(artifactPath);
|
|
614
|
+
if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
|
|
615
|
+
return {
|
|
616
|
+
path: field,
|
|
617
|
+
message: `${field} is outside the staging directory: ${artifactPath}`
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
498
621
|
}
|
|
622
|
+
function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
|
|
623
|
+
if (!artifactPath) return void 0;
|
|
624
|
+
const resolved = resolveComparablePath(artifactPath);
|
|
625
|
+
if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
|
|
626
|
+
return artifactPath;
|
|
627
|
+
}
|
|
628
|
+
const stagingResolved = resolveComparablePath(stagingRoot);
|
|
629
|
+
const relative2 = resolved === stagingResolved ? "" : resolved.slice(stagingResolved.length).replace(/^[/\\]/, "");
|
|
630
|
+
if (!relative2) return outDir;
|
|
631
|
+
if (/^[a-zA-Z]:[/\\]/.test(outDir)) {
|
|
632
|
+
return import_node_path5.win32.join(outDir, relative2.replace(/\//g, import_node_path5.win32.sep));
|
|
633
|
+
}
|
|
634
|
+
return (0, import_node_path5.join)(outDir, relative2);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/packaging/promote.ts
|
|
638
|
+
var fsp = __toESM(require("fs/promises"), 1);
|
|
499
639
|
async function pathExists(path) {
|
|
500
640
|
try {
|
|
501
641
|
await fsp.access(path);
|
|
@@ -504,82 +644,76 @@ async function pathExists(path) {
|
|
|
504
644
|
return false;
|
|
505
645
|
}
|
|
506
646
|
}
|
|
647
|
+
async function renameOrCopy(from, to) {
|
|
648
|
+
try {
|
|
649
|
+
await fsp.rename(from, to);
|
|
650
|
+
} catch (err) {
|
|
651
|
+
const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
|
|
652
|
+
if (code !== "EXDEV") throw err;
|
|
653
|
+
await fsp.cp(from, to, { recursive: true });
|
|
654
|
+
await fsp.rm(from, { recursive: true, force: true });
|
|
655
|
+
}
|
|
656
|
+
}
|
|
507
657
|
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
508
658
|
const tmpPromote = `${outDir}.tmp-promote`;
|
|
509
659
|
const backup = `${outDir}.bak`;
|
|
510
|
-
await
|
|
660
|
+
await renameOrCopy(stagingDir, tmpPromote);
|
|
511
661
|
const hadOutDir = await pathExists(outDir);
|
|
512
662
|
if (hadOutDir) {
|
|
513
|
-
await
|
|
663
|
+
await renameOrCopy(outDir, backup);
|
|
514
664
|
}
|
|
515
665
|
try {
|
|
516
|
-
await
|
|
666
|
+
await renameOrCopy(tmpPromote, outDir);
|
|
517
667
|
} catch (promoteError) {
|
|
518
668
|
if (hadOutDir) {
|
|
519
669
|
try {
|
|
520
|
-
await
|
|
670
|
+
await renameOrCopy(backup, outDir);
|
|
671
|
+
} catch (restoreError) {
|
|
672
|
+
const failedPromote2 = `${outDir}.failed-promote-${Date.now()}`;
|
|
673
|
+
try {
|
|
674
|
+
await renameOrCopy(tmpPromote, failedPromote2);
|
|
675
|
+
} catch {
|
|
676
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
677
|
+
}
|
|
678
|
+
const promoteMsg = promoteError instanceof Error ? promoteError.message : String(promoteError);
|
|
679
|
+
const restoreMsg = restoreError instanceof Error ? restoreError.message : String(restoreError);
|
|
680
|
+
throw new Error(
|
|
681
|
+
`[lessonkit/lxpack] promote failed (${promoteMsg}) and could not restore ${outDir} (${restoreMsg}). Recovery: previous output may be in ${backup}; staged package may be in ${failedPromote2}.`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
} else {
|
|
685
|
+
try {
|
|
686
|
+
await renameOrCopy(tmpPromote, stagingDir);
|
|
521
687
|
} catch (restoreError) {
|
|
522
688
|
console.warn(
|
|
523
|
-
`[lessonkit/lxpack] failed to restore ${
|
|
689
|
+
`[lessonkit/lxpack] failed to restore ${stagingDir} after promote error:`,
|
|
524
690
|
restoreError instanceof Error ? restoreError.message : restoreError
|
|
525
691
|
);
|
|
692
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
526
693
|
}
|
|
694
|
+
throw promoteError;
|
|
695
|
+
}
|
|
696
|
+
const failedPromote = `${outDir}.failed-promote-${Date.now()}`;
|
|
697
|
+
try {
|
|
698
|
+
await renameOrCopy(tmpPromote, failedPromote);
|
|
699
|
+
} catch {
|
|
700
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
527
701
|
}
|
|
528
|
-
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
529
702
|
throw promoteError;
|
|
530
703
|
}
|
|
531
704
|
if (hadOutDir) {
|
|
532
705
|
await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
|
|
533
706
|
}
|
|
534
707
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
ok: false,
|
|
545
|
-
courseDir: outDir,
|
|
546
|
-
target,
|
|
547
|
-
issues: [{ path: "outputBaseDir", message: `unsafe outputBaseDir: ${outputBaseDir}` }]
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
if (projectRoot && output) {
|
|
551
|
-
const resolvedOutput = (0, import_node_path5.resolve)(projectRoot, output);
|
|
552
|
-
try {
|
|
553
|
-
assertResolvedPathUnderRoot(projectRoot, resolvedOutput);
|
|
554
|
-
} catch (err) {
|
|
555
|
-
return {
|
|
556
|
-
ok: false,
|
|
557
|
-
courseDir: outDir,
|
|
558
|
-
target,
|
|
559
|
-
issues: [
|
|
560
|
-
{
|
|
561
|
-
path: "output",
|
|
562
|
-
message: err instanceof Error ? err.message : String(err)
|
|
563
|
-
}
|
|
564
|
-
]
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
const descriptorValidation = validateDescriptor(writeOpts.descriptor);
|
|
569
|
-
if (!descriptorValidation.ok) {
|
|
570
|
-
return {
|
|
571
|
-
ok: false,
|
|
572
|
-
courseDir: outDir,
|
|
573
|
-
target,
|
|
574
|
-
issues: descriptorValidation.issues.map((i) => ({
|
|
575
|
-
path: i.path,
|
|
576
|
-
message: i.message
|
|
577
|
-
}))
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
const descriptor = descriptorValidation.descriptor;
|
|
581
|
-
const stagingDir = await fsp.mkdtemp((0, import_node_path5.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
|
|
582
|
-
let promoted = false;
|
|
708
|
+
|
|
709
|
+
// src/packaging/staging.ts
|
|
710
|
+
var fsp2 = __toESM(require("fs/promises"), 1);
|
|
711
|
+
var import_node_path6 = require("path");
|
|
712
|
+
var import_node_os = require("os");
|
|
713
|
+
var import_api = require("@lxpack/api");
|
|
714
|
+
async function buildStagingPackage(options) {
|
|
715
|
+
const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
|
|
716
|
+
const stagingDir = await fsp2.mkdtemp((0, import_node_path6.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
|
|
583
717
|
try {
|
|
584
718
|
let spaDirs;
|
|
585
719
|
try {
|
|
@@ -587,8 +721,7 @@ async function packageLessonkitCourse(options) {
|
|
|
587
721
|
} catch (err) {
|
|
588
722
|
return {
|
|
589
723
|
ok: false,
|
|
590
|
-
|
|
591
|
-
target,
|
|
724
|
+
stagingDir,
|
|
592
725
|
issues: [
|
|
593
726
|
{
|
|
594
727
|
path: "spaDirs",
|
|
@@ -599,8 +732,8 @@ async function packageLessonkitCourse(options) {
|
|
|
599
732
|
}
|
|
600
733
|
const interchange = descriptorToInterchange(descriptor);
|
|
601
734
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
602
|
-
await
|
|
603
|
-
const defaultOutput = output ?? (dir ? (0,
|
|
735
|
+
await fsp2.mkdir((0, import_node_path6.join)(stagingDir, outputBase), { recursive: true });
|
|
736
|
+
const defaultOutput = output ?? (dir ? (0, import_node_path6.join)(outputBase, target) : (0, import_node_path6.join)(outputBase, `course-${target}.zip`));
|
|
604
737
|
const build = await (0, import_api.packageLessonkit)({
|
|
605
738
|
interchange,
|
|
606
739
|
spaDirs,
|
|
@@ -613,15 +746,9 @@ async function packageLessonkitCourse(options) {
|
|
|
613
746
|
writeAuthoringFiles: true
|
|
614
747
|
});
|
|
615
748
|
if (!build.ok) {
|
|
616
|
-
const validation2 = {
|
|
617
|
-
ok: false,
|
|
618
|
-
issues: build.issues
|
|
619
|
-
};
|
|
620
749
|
return {
|
|
621
750
|
ok: false,
|
|
622
|
-
|
|
623
|
-
target,
|
|
624
|
-
validation: validation2,
|
|
751
|
+
stagingDir,
|
|
625
752
|
build,
|
|
626
753
|
issues: build.issues.map((i) => ({
|
|
627
754
|
path: i.path,
|
|
@@ -630,48 +757,266 @@ async function packageLessonkitCourse(options) {
|
|
|
630
757
|
}))
|
|
631
758
|
};
|
|
632
759
|
}
|
|
633
|
-
|
|
760
|
+
return {
|
|
634
761
|
ok: true,
|
|
635
|
-
|
|
636
|
-
|
|
762
|
+
stagingDir,
|
|
763
|
+
build,
|
|
764
|
+
outputPath: "outputPath" in build ? build.outputPath : void 0,
|
|
765
|
+
outputDir: "outputDir" in build ? build.outputDir : void 0
|
|
637
766
|
};
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
767
|
+
} catch (err) {
|
|
768
|
+
await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
769
|
+
throw err;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
async function ensureOutDirParent(outDir) {
|
|
773
|
+
await fsp2.mkdir((0, import_node_path6.dirname)(outDir), { recursive: true });
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/packageCourse.ts
|
|
777
|
+
async function validateLessonkitProject(options) {
|
|
778
|
+
return (0, import_api2.validateCourse)({
|
|
779
|
+
courseDir: (0, import_node_path7.resolve)(options.courseDir),
|
|
780
|
+
target: options.target
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
async function buildLessonkitProject(options) {
|
|
784
|
+
return (0, import_api2.buildCourse)({
|
|
785
|
+
courseDir: (0, import_node_path7.resolve)(options.courseDir),
|
|
786
|
+
target: options.target,
|
|
787
|
+
output: options.output,
|
|
788
|
+
dir: options.dir,
|
|
789
|
+
outputBaseDir: options.outputBaseDir,
|
|
790
|
+
assessments: options.assessments
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
async function packageLessonkitCourse(options) {
|
|
794
|
+
const { target, output, dir, outputBaseDir, ...writeOpts } = options;
|
|
795
|
+
const inputValidation = validatePackageInputs({
|
|
796
|
+
target,
|
|
797
|
+
output,
|
|
798
|
+
outputBaseDir,
|
|
799
|
+
outDir: writeOpts.outDir,
|
|
800
|
+
projectRoot: writeOpts.projectRoot
|
|
801
|
+
});
|
|
802
|
+
if (!inputValidation.ok) {
|
|
803
|
+
return {
|
|
804
|
+
ok: false,
|
|
805
|
+
courseDir: inputValidation.courseDir,
|
|
806
|
+
target: inputValidation.target,
|
|
807
|
+
issues: inputValidation.issues
|
|
646
808
|
};
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
809
|
+
}
|
|
810
|
+
const outDir = inputValidation.outDir;
|
|
811
|
+
const descriptorValidation = validateDescriptor(writeOpts.descriptor);
|
|
812
|
+
if (!descriptorValidation.ok) {
|
|
813
|
+
return {
|
|
814
|
+
ok: false,
|
|
815
|
+
courseDir: outDir,
|
|
816
|
+
target,
|
|
817
|
+
issues: descriptorValidation.issues.map((i) => ({
|
|
818
|
+
path: i.path,
|
|
819
|
+
message: i.message
|
|
820
|
+
}))
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
const descriptor = descriptorValidation.descriptor;
|
|
824
|
+
if (target === "xapi" || target === "cmi5") {
|
|
825
|
+
const activityIri = descriptor.tracking?.xapi?.activityIri?.trim();
|
|
826
|
+
if (!activityIri) {
|
|
827
|
+
return {
|
|
828
|
+
ok: false,
|
|
829
|
+
courseDir: outDir,
|
|
830
|
+
target,
|
|
831
|
+
issues: [
|
|
832
|
+
{
|
|
833
|
+
path: "course.tracking.xapi.activityIri",
|
|
834
|
+
message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
|
|
835
|
+
}
|
|
836
|
+
]
|
|
837
|
+
};
|
|
660
838
|
}
|
|
839
|
+
}
|
|
840
|
+
const staged = await buildStagingPackage({
|
|
841
|
+
...writeOpts,
|
|
842
|
+
descriptor,
|
|
843
|
+
target,
|
|
844
|
+
output,
|
|
845
|
+
dir,
|
|
846
|
+
outputBaseDir
|
|
847
|
+
});
|
|
848
|
+
if (!staged.ok) {
|
|
849
|
+
await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
850
|
+
const validation2 = staged.build ? { ok: false, issues: staged.build.issues } : void 0;
|
|
661
851
|
return {
|
|
662
|
-
ok:
|
|
852
|
+
ok: false,
|
|
853
|
+
courseDir: outDir,
|
|
854
|
+
target,
|
|
855
|
+
validation: validation2,
|
|
856
|
+
build: staged.build,
|
|
857
|
+
issues: staged.issues
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
const { stagingDir, build } = staged;
|
|
861
|
+
const stagingRoot = await fsp3.realpath(stagingDir);
|
|
862
|
+
const artifactIssues = [
|
|
863
|
+
validateArtifactInStaging(stagingRoot, staged.outputPath, "outputPath"),
|
|
864
|
+
validateArtifactInStaging(stagingRoot, staged.outputDir, "outputDir")
|
|
865
|
+
].filter((issue) => issue != null);
|
|
866
|
+
if (artifactIssues.length > 0) {
|
|
867
|
+
await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
868
|
+
return {
|
|
869
|
+
ok: false,
|
|
870
|
+
courseDir: outDir,
|
|
871
|
+
target,
|
|
872
|
+
validation: { ok: true, manifest: build.manifest, issues: build.issues },
|
|
873
|
+
build,
|
|
874
|
+
issues: artifactIssues
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
const remappedOutputPath = remapArtifactPaths(stagingRoot, outDir, staged.outputPath);
|
|
878
|
+
const remappedOutputDir = remapArtifactPaths(stagingRoot, outDir, staged.outputDir);
|
|
879
|
+
const validation = {
|
|
880
|
+
ok: true,
|
|
881
|
+
manifest: build.manifest,
|
|
882
|
+
issues: build.issues
|
|
883
|
+
};
|
|
884
|
+
try {
|
|
885
|
+
await ensureOutDirParent(outDir);
|
|
886
|
+
await promoteStagingToOutDir(stagingDir, outDir);
|
|
887
|
+
} catch (err) {
|
|
888
|
+
return {
|
|
889
|
+
ok: false,
|
|
663
890
|
courseDir: outDir,
|
|
664
891
|
target,
|
|
665
|
-
outputPath: remappedOutputPath,
|
|
666
|
-
outputDir: remappedOutputDir,
|
|
667
|
-
fileCount: build.fileCount,
|
|
668
892
|
validation,
|
|
669
|
-
build
|
|
893
|
+
build,
|
|
894
|
+
issues: [
|
|
895
|
+
{
|
|
896
|
+
path: "promote",
|
|
897
|
+
message: err instanceof Error ? err.message : String(err)
|
|
898
|
+
}
|
|
899
|
+
]
|
|
670
900
|
};
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
|
|
901
|
+
}
|
|
902
|
+
const remappedBuild = { ...build };
|
|
903
|
+
if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
|
|
904
|
+
remappedBuild.outputPath = remappedOutputPath;
|
|
905
|
+
}
|
|
906
|
+
if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
|
|
907
|
+
remappedBuild.outputDir = remappedOutputDir;
|
|
908
|
+
}
|
|
909
|
+
return {
|
|
910
|
+
ok: true,
|
|
911
|
+
courseDir: outDir,
|
|
912
|
+
target,
|
|
913
|
+
outputPath: remappedOutputPath,
|
|
914
|
+
outputDir: remappedOutputDir,
|
|
915
|
+
fileCount: build.fileCount,
|
|
916
|
+
validation,
|
|
917
|
+
build: remappedBuild
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/manifest.ts
|
|
922
|
+
var DEFAULT_PATHS = {
|
|
923
|
+
spaDistDir: "dist",
|
|
924
|
+
lxpackOutDir: ".lxpack/course",
|
|
925
|
+
outputBaseDir: ".lxpack/out"
|
|
926
|
+
};
|
|
927
|
+
function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
|
|
928
|
+
if (!raw || typeof raw !== "object") {
|
|
929
|
+
return { ok: false, issues: [{ path: label, message: "must be a JSON object" }] };
|
|
930
|
+
}
|
|
931
|
+
const config = raw;
|
|
932
|
+
const issues = [];
|
|
933
|
+
if (config.schemaVersion !== 1) {
|
|
934
|
+
issues.push({
|
|
935
|
+
path: "schemaVersion",
|
|
936
|
+
message: `must be 1 (got ${String(config.schemaVersion)})`
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
const name = config.name;
|
|
940
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
941
|
+
issues.push({ path: "name", message: "must be a non-empty string" });
|
|
942
|
+
}
|
|
943
|
+
const courseRaw = config.course;
|
|
944
|
+
if (Array.isArray(courseRaw)) {
|
|
945
|
+
issues.push({ path: "course", message: "must be an object, not an array" });
|
|
946
|
+
return { ok: false, issues };
|
|
947
|
+
}
|
|
948
|
+
if (!courseRaw || typeof courseRaw !== "object") {
|
|
949
|
+
issues.push({ path: "course", message: "must be an object" });
|
|
950
|
+
return { ok: false, issues };
|
|
951
|
+
}
|
|
952
|
+
const courseObj = courseRaw;
|
|
953
|
+
if (courseObj.lessons !== void 0 && !Array.isArray(courseObj.lessons)) {
|
|
954
|
+
issues.push({ path: "course.lessons", message: "must be an array" });
|
|
955
|
+
}
|
|
956
|
+
if (courseObj.assessments !== void 0 && !Array.isArray(courseObj.assessments)) {
|
|
957
|
+
issues.push({ path: "course.assessments", message: "must be an array" });
|
|
958
|
+
}
|
|
959
|
+
if (issues.length) return { ok: false, issues };
|
|
960
|
+
const validation = validateDescriptor(courseRaw);
|
|
961
|
+
if (!validation.ok) {
|
|
962
|
+
for (const i of validation.issues) {
|
|
963
|
+
issues.push({
|
|
964
|
+
path: i.path.startsWith("course.") ? i.path : `course.${i.path}`,
|
|
965
|
+
message: i.message
|
|
966
|
+
});
|
|
674
967
|
}
|
|
968
|
+
} else if (validation.descriptor.layout === "per-lesson-spa") {
|
|
969
|
+
issues.push({
|
|
970
|
+
path: "course.layout",
|
|
971
|
+
message: "per-lesson-spa is not supported by lessonkit package yet. Use single-spa or package via @lessonkit/lxpack directly."
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
const paths = { ...DEFAULT_PATHS };
|
|
975
|
+
const pathsRaw = config.paths;
|
|
976
|
+
if (pathsRaw !== void 0 && (typeof pathsRaw !== "object" || pathsRaw === null)) {
|
|
977
|
+
issues.push({ path: "paths", message: "must be an object" });
|
|
978
|
+
} else if (pathsRaw && typeof pathsRaw === "object") {
|
|
979
|
+
const p = pathsRaw;
|
|
980
|
+
for (const key of ["spaDistDir", "lxpackOutDir", "outputBaseDir"]) {
|
|
981
|
+
if (p[key] !== void 0) {
|
|
982
|
+
if (typeof p[key] !== "string" || !p[key].trim()) {
|
|
983
|
+
issues.push({ path: `paths.${key}`, message: "must be a non-empty string" });
|
|
984
|
+
} else {
|
|
985
|
+
paths[key] = p[key].trim();
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
const courseSpaDistDir = validation.ok ? validation.descriptor.spaDistDir?.trim() : void 0;
|
|
991
|
+
if (courseSpaDistDir && courseSpaDistDir !== paths.spaDistDir) {
|
|
992
|
+
issues.push({
|
|
993
|
+
path: "course.spaDistDir",
|
|
994
|
+
message: `"course.spaDistDir" (${courseSpaDistDir}) differs from "paths.spaDistDir" (${paths.spaDistDir}). Use paths.spaDistDir for CLI build and package.`
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
if (projectRoot) {
|
|
998
|
+
const pathIssues = validateProjectPaths(projectRoot, paths);
|
|
999
|
+
for (const pi of pathIssues) {
|
|
1000
|
+
issues.push({ path: pi.path, message: pi.message });
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (issues.length) return { ok: false, issues };
|
|
1004
|
+
if (!validation.ok) return { ok: false, issues };
|
|
1005
|
+
return {
|
|
1006
|
+
ok: true,
|
|
1007
|
+
manifest: {
|
|
1008
|
+
schemaVersion: 1,
|
|
1009
|
+
name,
|
|
1010
|
+
course: validation.descriptor,
|
|
1011
|
+
paths
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
async function loadLessonkitManifestFromFile(readJson, label = "lessonkit.json", projectRoot) {
|
|
1016
|
+
try {
|
|
1017
|
+
return parseLessonkitManifest(await readJson(), label, projectRoot);
|
|
1018
|
+
} catch {
|
|
1019
|
+
return { ok: false, issues: [{ path: label, message: "failed to read or parse JSON" }] };
|
|
675
1020
|
}
|
|
676
1021
|
}
|
|
677
1022
|
|
|
@@ -714,21 +1059,28 @@ var import_validators2 = require("@lxpack/validators");
|
|
|
714
1059
|
LESSONKIT_TELEMETRY_EVENTS,
|
|
715
1060
|
assessmentDescriptorToLxpack,
|
|
716
1061
|
buildLessonkitProject,
|
|
1062
|
+
buildStagingPackage,
|
|
717
1063
|
descriptorToInterchange,
|
|
1064
|
+
ensureOutDirParent,
|
|
718
1065
|
extractAssessments,
|
|
719
1066
|
lessonkitInterchangeSchema,
|
|
1067
|
+
loadLessonkitManifestFromFile,
|
|
720
1068
|
mapLessonkitIds,
|
|
721
1069
|
mapLessonkitTelemetryToBridgeAction,
|
|
722
1070
|
mapLessonkitTelemetryToLxpack,
|
|
723
1071
|
materializeLessonkitProject,
|
|
724
1072
|
packageLessonkitCourse,
|
|
725
1073
|
parseLessonkitInterchange,
|
|
1074
|
+
parseLessonkitManifest,
|
|
1075
|
+
promoteStagingToOutDir,
|
|
1076
|
+
remapArtifactPaths,
|
|
726
1077
|
resolveSafePackageOutputOverride,
|
|
727
1078
|
resolveSpaLessons,
|
|
728
1079
|
telemetryEventToLessonkit,
|
|
729
1080
|
themeToLxpackRuntime,
|
|
730
1081
|
validateDescriptor,
|
|
731
1082
|
validateLessonkitProject,
|
|
1083
|
+
validatePackageInputs,
|
|
732
1084
|
validateProjectPaths,
|
|
733
1085
|
writeLxpackProject
|
|
734
1086
|
});
|