@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/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 = (0, import_node_path.resolve)(root);
71
- const targetResolved = (0, import_node_path.resolve)(target);
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
- if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix)) {
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
- assertResolvedPathUnderRoot((0, import_node_path3.resolve)(projectRoot), srcDist);
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
- dirs[lesson.id] = (0, import_node_path3.resolve)(src);
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 fsp = __toESM(require("fs/promises"), 1);
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
- var import_node_os = require("os");
478
- var import_api = require("@lxpack/api");
479
- async function validateLessonkitProject(options) {
480
- return (0, import_api.validateCourse)({
481
- courseDir: (0, import_node_path5.resolve)(options.courseDir),
482
- target: options.target
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
- async function buildLessonkitProject(options) {
486
- return (0, import_api.buildCourse)({
487
- courseDir: (0, import_node_path5.resolve)(options.courseDir),
488
- target: options.target,
489
- output: options.output,
490
- dir: options.dir,
491
- outputBaseDir: options.outputBaseDir,
492
- assessments: options.assessments
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 fsp.rename(stagingDir, tmpPromote);
660
+ await renameOrCopy(stagingDir, tmpPromote);
507
661
  const hadOutDir = await pathExists(outDir);
508
662
  if (hadOutDir) {
509
- await fsp.rename(outDir, backup);
663
+ await renameOrCopy(outDir, backup);
510
664
  }
511
665
  try {
512
- await fsp.rename(tmpPromote, outDir);
666
+ await renameOrCopy(tmpPromote, outDir);
513
667
  } catch (promoteError) {
514
668
  if (hadOutDir) {
515
669
  try {
516
- await fsp.rename(backup, outDir);
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 ${outDir} after promote error:`,
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
- async function packageLessonkitCourse(options) {
532
- const { target, output, dir, outputBaseDir, ...writeOpts } = options;
533
- const outDir = (0, import_node_path5.resolve)(writeOpts.outDir);
534
- const projectRoot = writeOpts.projectRoot ? (0, import_node_path5.resolve)(writeOpts.projectRoot) : void 0;
535
- if (projectRoot) {
536
- assertResolvedPathUnderRoot(projectRoot, outDir);
537
- }
538
- if (outputBaseDir && !isSafeRelativeSpaPath(outputBaseDir)) {
539
- return {
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
- courseDir: outDir,
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 fsp.mkdir((0, import_node_path5.join)(stagingDir, outputBase), { recursive: true });
599
- const defaultOutput = output ?? (dir ? (0, import_node_path5.join)(outputBase, target) : (0, import_node_path5.join)(outputBase, `course-${target}.zip`));
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
- courseDir: outDir,
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
- const validation = {
760
+ return {
630
761
  ok: true,
631
- manifest: build.manifest,
632
- issues: build.issues
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
- const stagingRoot = await fsp.realpath(stagingDir);
635
- const remapArtifactPath = (artifactPath) => {
636
- if (!artifactPath) return void 0;
637
- const resolved = (0, import_node_path5.resolve)(artifactPath);
638
- if (resolved === stagingRoot || resolved.startsWith(`${stagingRoot}/`)) {
639
- return (0, import_node_path5.join)(outDir, resolved.slice(stagingRoot.length + 1));
640
- }
641
- return artifactPath;
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
- const remappedOutputPath = remapArtifactPath(
644
- "outputPath" in build ? build.outputPath : void 0
645
- );
646
- const remappedOutputDir = remapArtifactPath("outputDir" in build ? build.outputDir : void 0);
647
- await fsp.mkdir((0, import_node_path5.dirname)(outDir), { recursive: true });
648
- await promoteStagingToOutDir(stagingDir, outDir);
649
- promoted = true;
650
- const remappedBuild = { ...build };
651
- if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
652
- remappedBuild.outputPath = remappedOutputPath;
653
- }
654
- if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
655
- remappedBuild.outputDir = remappedOutputDir;
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: true,
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: remappedBuild
893
+ build,
894
+ issues: [
895
+ {
896
+ path: "promote",
897
+ message: err instanceof Error ? err.message : String(err)
898
+ }
899
+ ]
666
900
  };
667
- } finally {
668
- if (!promoted) {
669
- await fsp.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
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
  });