@lessonkit/lxpack 0.9.2 → 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 +476 -120
- package/dist/index.d.cts +82 -2
- package/dist/index.d.ts +82 -2
- package/dist/index.js +474 -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
|
}
|
|
@@ -432,7 +481,23 @@ async function resolveSpaDirs(options) {
|
|
|
432
481
|
if (!src) {
|
|
433
482
|
throw new Error(`lessonSpaDirs missing build output for lesson "${lesson.id}"`);
|
|
434
483
|
}
|
|
435
|
-
|
|
484
|
+
const resolved = projectRoot ? (0, import_node_path3.resolve)(projectRoot, src) : (0, import_node_path3.resolve)(src);
|
|
485
|
+
if (projectRoot) {
|
|
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
|
+
);
|
|
499
|
+
}
|
|
500
|
+
dirs[lesson.id] = resolved;
|
|
436
501
|
}
|
|
437
502
|
return dirs;
|
|
438
503
|
}
|
|
@@ -472,26 +537,105 @@ async function writeLxpackProject(options) {
|
|
|
472
537
|
}
|
|
473
538
|
|
|
474
539
|
// src/packageCourse.ts
|
|
475
|
-
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
|
|
476
545
|
var import_node_path5 = require("path");
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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 };
|
|
484
610
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
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;
|
|
494
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);
|
|
495
639
|
async function pathExists(path) {
|
|
496
640
|
try {
|
|
497
641
|
await fsp.access(path);
|
|
@@ -500,82 +644,76 @@ async function pathExists(path) {
|
|
|
500
644
|
return false;
|
|
501
645
|
}
|
|
502
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
|
+
}
|
|
503
657
|
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
504
658
|
const tmpPromote = `${outDir}.tmp-promote`;
|
|
505
659
|
const backup = `${outDir}.bak`;
|
|
506
|
-
await
|
|
660
|
+
await renameOrCopy(stagingDir, tmpPromote);
|
|
507
661
|
const hadOutDir = await pathExists(outDir);
|
|
508
662
|
if (hadOutDir) {
|
|
509
|
-
await
|
|
663
|
+
await renameOrCopy(outDir, backup);
|
|
510
664
|
}
|
|
511
665
|
try {
|
|
512
|
-
await
|
|
666
|
+
await renameOrCopy(tmpPromote, outDir);
|
|
513
667
|
} catch (promoteError) {
|
|
514
668
|
if (hadOutDir) {
|
|
515
669
|
try {
|
|
516
|
-
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);
|
|
517
687
|
} catch (restoreError) {
|
|
518
688
|
console.warn(
|
|
519
|
-
`[lessonkit/lxpack] failed to restore ${
|
|
689
|
+
`[lessonkit/lxpack] failed to restore ${stagingDir} after promote error:`,
|
|
520
690
|
restoreError instanceof Error ? restoreError.message : restoreError
|
|
521
691
|
);
|
|
692
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
522
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);
|
|
523
701
|
}
|
|
524
|
-
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
525
702
|
throw promoteError;
|
|
526
703
|
}
|
|
527
704
|
if (hadOutDir) {
|
|
528
705
|
await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
|
|
529
706
|
}
|
|
530
707
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
ok: false,
|
|
541
|
-
courseDir: outDir,
|
|
542
|
-
target,
|
|
543
|
-
issues: [{ path: "outputBaseDir", message: `unsafe outputBaseDir: ${outputBaseDir}` }]
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
if (projectRoot && output) {
|
|
547
|
-
const resolvedOutput = (0, import_node_path5.resolve)(projectRoot, output);
|
|
548
|
-
try {
|
|
549
|
-
assertResolvedPathUnderRoot(projectRoot, resolvedOutput);
|
|
550
|
-
} catch (err) {
|
|
551
|
-
return {
|
|
552
|
-
ok: false,
|
|
553
|
-
courseDir: outDir,
|
|
554
|
-
target,
|
|
555
|
-
issues: [
|
|
556
|
-
{
|
|
557
|
-
path: "output",
|
|
558
|
-
message: err instanceof Error ? err.message : String(err)
|
|
559
|
-
}
|
|
560
|
-
]
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
const descriptorValidation = validateDescriptor(writeOpts.descriptor);
|
|
565
|
-
if (!descriptorValidation.ok) {
|
|
566
|
-
return {
|
|
567
|
-
ok: false,
|
|
568
|
-
courseDir: outDir,
|
|
569
|
-
target,
|
|
570
|
-
issues: descriptorValidation.issues.map((i) => ({
|
|
571
|
-
path: i.path,
|
|
572
|
-
message: i.message
|
|
573
|
-
}))
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
const descriptor = descriptorValidation.descriptor;
|
|
577
|
-
const stagingDir = await fsp.mkdtemp((0, import_node_path5.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
|
|
578
|
-
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-"));
|
|
579
717
|
try {
|
|
580
718
|
let spaDirs;
|
|
581
719
|
try {
|
|
@@ -583,8 +721,7 @@ async function packageLessonkitCourse(options) {
|
|
|
583
721
|
} catch (err) {
|
|
584
722
|
return {
|
|
585
723
|
ok: false,
|
|
586
|
-
|
|
587
|
-
target,
|
|
724
|
+
stagingDir,
|
|
588
725
|
issues: [
|
|
589
726
|
{
|
|
590
727
|
path: "spaDirs",
|
|
@@ -595,8 +732,8 @@ async function packageLessonkitCourse(options) {
|
|
|
595
732
|
}
|
|
596
733
|
const interchange = descriptorToInterchange(descriptor);
|
|
597
734
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
598
|
-
await
|
|
599
|
-
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`));
|
|
600
737
|
const build = await (0, import_api.packageLessonkit)({
|
|
601
738
|
interchange,
|
|
602
739
|
spaDirs,
|
|
@@ -609,15 +746,9 @@ async function packageLessonkitCourse(options) {
|
|
|
609
746
|
writeAuthoringFiles: true
|
|
610
747
|
});
|
|
611
748
|
if (!build.ok) {
|
|
612
|
-
const validation2 = {
|
|
613
|
-
ok: false,
|
|
614
|
-
issues: build.issues
|
|
615
|
-
};
|
|
616
749
|
return {
|
|
617
750
|
ok: false,
|
|
618
|
-
|
|
619
|
-
target,
|
|
620
|
-
validation: validation2,
|
|
751
|
+
stagingDir,
|
|
621
752
|
build,
|
|
622
753
|
issues: build.issues.map((i) => ({
|
|
623
754
|
path: i.path,
|
|
@@ -626,48 +757,266 @@ async function packageLessonkitCourse(options) {
|
|
|
626
757
|
}))
|
|
627
758
|
};
|
|
628
759
|
}
|
|
629
|
-
|
|
760
|
+
return {
|
|
630
761
|
ok: true,
|
|
631
|
-
|
|
632
|
-
|
|
762
|
+
stagingDir,
|
|
763
|
+
build,
|
|
764
|
+
outputPath: "outputPath" in build ? build.outputPath : void 0,
|
|
765
|
+
outputDir: "outputDir" in build ? build.outputDir : void 0
|
|
633
766
|
};
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
|
642
808
|
};
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
+
};
|
|
656
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;
|
|
657
851
|
return {
|
|
658
|
-
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,
|
|
659
890
|
courseDir: outDir,
|
|
660
891
|
target,
|
|
661
|
-
outputPath: remappedOutputPath,
|
|
662
|
-
outputDir: remappedOutputDir,
|
|
663
|
-
fileCount: build.fileCount,
|
|
664
892
|
validation,
|
|
665
|
-
build
|
|
893
|
+
build,
|
|
894
|
+
issues: [
|
|
895
|
+
{
|
|
896
|
+
path: "promote",
|
|
897
|
+
message: err instanceof Error ? err.message : String(err)
|
|
898
|
+
}
|
|
899
|
+
]
|
|
666
900
|
};
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
|
|
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
|
+
});
|
|
670
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" }] };
|
|
671
1020
|
}
|
|
672
1021
|
}
|
|
673
1022
|
|
|
@@ -710,21 +1059,28 @@ var import_validators2 = require("@lxpack/validators");
|
|
|
710
1059
|
LESSONKIT_TELEMETRY_EVENTS,
|
|
711
1060
|
assessmentDescriptorToLxpack,
|
|
712
1061
|
buildLessonkitProject,
|
|
1062
|
+
buildStagingPackage,
|
|
713
1063
|
descriptorToInterchange,
|
|
1064
|
+
ensureOutDirParent,
|
|
714
1065
|
extractAssessments,
|
|
715
1066
|
lessonkitInterchangeSchema,
|
|
1067
|
+
loadLessonkitManifestFromFile,
|
|
716
1068
|
mapLessonkitIds,
|
|
717
1069
|
mapLessonkitTelemetryToBridgeAction,
|
|
718
1070
|
mapLessonkitTelemetryToLxpack,
|
|
719
1071
|
materializeLessonkitProject,
|
|
720
1072
|
packageLessonkitCourse,
|
|
721
1073
|
parseLessonkitInterchange,
|
|
1074
|
+
parseLessonkitManifest,
|
|
1075
|
+
promoteStagingToOutDir,
|
|
1076
|
+
remapArtifactPaths,
|
|
722
1077
|
resolveSafePackageOutputOverride,
|
|
723
1078
|
resolveSpaLessons,
|
|
724
1079
|
telemetryEventToLessonkit,
|
|
725
1080
|
themeToLxpackRuntime,
|
|
726
1081
|
validateDescriptor,
|
|
727
1082
|
validateLessonkitProject,
|
|
1083
|
+
validatePackageInputs,
|
|
728
1084
|
validateProjectPaths,
|
|
729
1085
|
writeLxpackProject
|
|
730
1086
|
});
|