@lessonkit/lxpack 1.0.0 → 1.0.2

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.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { CheckId, CourseId, LessonId } from '@lessonkit/core';
2
2
  import { ThemePresetName, LessonkitThemeV1 } from '@lessonkit/themes';
3
- import { LessonkitInterchangeV1 } from '@lxpack/validators';
4
- export { LessonkitInterchangeV1, MaterializeLessonkitOptions, MaterializeLessonkitResult, lessonkitInterchangeSchema, materializeLessonkitProject, parseLessonkitInterchange } from '@lxpack/validators';
5
3
  import { ExportTarget, BuildCourseResult, ValidateCourseResult } from '@lxpack/api';
6
4
  export { ExportTarget } from '@lxpack/api';
5
+ import { LessonkitInterchangeV1 } from '@lxpack/validators';
6
+ export { LessonkitInterchangeV1, MaterializeLessonkitOptions, MaterializeLessonkitResult, lessonkitInterchangeSchema, materializeLessonkitProject, parseLessonkitInterchange } from '@lxpack/validators';
7
7
  export { LESSONKIT_TELEMETRY_EVENTS, LessonkitBridgeAction, LessonkitTelemetryEvent, LessonkitTelemetryEventName, TrackingSchemaEvent, mapLessonkitTelemetryToBridgeAction, mapLessonkitTelemetryToLxpack } from '@lxpack/tracking-schema';
8
8
  export { t as telemetryEventToLessonkit } from './telemetry-gCxlwc7I.js';
9
9
 
@@ -53,10 +53,14 @@ type MappedLessonkitIds = {
53
53
  checkIds: CheckId[];
54
54
  };
55
55
 
56
- type DescriptorValidationIssue = {
56
+ /** Shared validation issue shape across lxpack parsers. */
57
+ type ValidationIssue = {
57
58
  path: string;
58
59
  message: string;
60
+ severity?: string;
59
61
  };
62
+
63
+ type DescriptorValidationIssue = ValidationIssue;
60
64
  type DescriptorValidationResult = {
61
65
  ok: true;
62
66
  descriptor: LessonkitCourseDescriptor;
@@ -64,7 +68,8 @@ type DescriptorValidationResult = {
64
68
  ok: false;
65
69
  issues: DescriptorValidationIssue[];
66
70
  };
67
- declare function validateDescriptor(input: LessonkitCourseDescriptor): DescriptorValidationResult;
71
+ declare function validateDescriptor(input: unknown): DescriptorValidationResult;
72
+ declare function validateDescriptorForTarget(input: unknown, target?: ExportTarget): DescriptorValidationResult;
68
73
 
69
74
  type ProjectPathsInput = {
70
75
  spaDistDir?: string;
@@ -94,7 +99,7 @@ declare function themeToLxpackRuntime(input: {
94
99
  }): LxpackRuntimeTheme;
95
100
 
96
101
  type SpaLessonEntry = {
97
- id: string;
102
+ id: LessonId;
98
103
  title: string;
99
104
  path: string;
100
105
  };
@@ -188,7 +193,7 @@ type BuildLessonkitProjectOptions = {
188
193
  output?: string;
189
194
  dir?: boolean;
190
195
  outputBaseDir?: string;
191
- assessments?: unknown[];
196
+ assessments?: LxpackInjectedAssessment[];
192
197
  };
193
198
  type PackageLessonkitCourseOptions = WriteLxpackProjectOptions & {
194
199
  target: ExportTarget;
@@ -268,4 +273,4 @@ type ParseManifestResult = {
268
273
  declare function parseLessonkitManifest(raw: unknown, label?: string, projectRoot?: string): ParseManifestResult;
269
274
  declare function loadLessonkitManifestFromFile(readJson: () => Promise<unknown>, label?: string, projectRoot?: string): Promise<ParseManifestResult>;
270
275
 
271
- export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitManifest, type LessonkitManifestPaths, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type ManifestParseIssue, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type PackageValidationIssue, type ParseManifestResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type ValidatePackageInputsResult, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, buildStagingPackage, descriptorToInterchange, ensureOutDirParent, extractAssessments, loadLessonkitManifestFromFile, mapLessonkitIds, packageLessonkitCourse, parseLessonkitManifest, promoteStagingToOutDir, remapArtifactPaths, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateLessonkitProject, validatePackageInputs, validateProjectPaths, writeLxpackProject };
276
+ export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitManifest, type LessonkitManifestPaths, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type ManifestParseIssue, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type PackageValidationIssue, type ParseManifestResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type ValidatePackageInputsResult, type ValidationIssue, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, buildStagingPackage, descriptorToInterchange, ensureOutDirParent, extractAssessments, loadLessonkitManifestFromFile, mapLessonkitIds, packageLessonkitCourse, parseLessonkitManifest, promoteStagingToOutDir, remapArtifactPaths, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateDescriptorForTarget, validateLessonkitProject, validatePackageInputs, validateProjectPaths, writeLxpackProject };
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  telemetryEventToLessonkit
3
- } from "./chunk-PSUSESH3.js";
3
+ } from "./chunk-DYQI222N.js";
4
4
 
5
5
  // src/validateDescriptor.ts
6
6
  import { validateId } from "@lessonkit/core";
7
7
 
8
8
  // src/spaPath.ts
9
9
  import { realpathSync } from "fs";
10
- import { relative, resolve, sep, win32 } from "path";
10
+ import { isAbsolute, relative, resolve, sep, win32 } from "path";
11
11
  function resolveComparablePath(p) {
12
12
  if (/^[a-zA-Z]:[/\\]/.test(p)) {
13
13
  return win32.resolve(p);
@@ -17,10 +17,11 @@ function resolveComparablePath(p) {
17
17
  function isSafeRelativeSpaPath(spaPath) {
18
18
  if (!spaPath.length || spaPath.includes("\0")) return false;
19
19
  if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
20
- if (/^[a-zA-Z]:[/\\]/.test(spaPath)) return false;
21
- const segments = spaPath.split(/[/\\]/).filter((s) => s.length > 0);
20
+ if (/^[a-zA-Z]:/.test(spaPath)) return false;
21
+ if (spaPath === "." || spaPath === "./") return false;
22
+ const segments = spaPath.split(/[/\\]/).filter((s) => s.length > 0 && s !== ".");
22
23
  if (segments.some((s) => s === "..")) return false;
23
- return true;
24
+ return segments.length > 0;
24
25
  }
25
26
  function assertResolvedPathUnderRoot(root, target) {
26
27
  const rootResolved = resolveComparablePath(root);
@@ -52,13 +53,25 @@ function assertRealPathUnderRoot(root, target) {
52
53
  }
53
54
  assertResolvedPathUnderRoot(rootReal, targetCheck);
54
55
  }
56
+ function normalizePathForComparison(p) {
57
+ const resolved = resolveComparablePath(p);
58
+ return /^[a-zA-Z]:[/\\]/.test(resolved) ? resolved.toLowerCase() : resolved;
59
+ }
60
+ function relativePathUnderRoot(root, target) {
61
+ const rootResolved = normalizePathForComparison(root);
62
+ const targetResolved = normalizePathForComparison(target);
63
+ if (/^[a-zA-Z]:[/\\]/.test(rootResolved)) {
64
+ return win32.relative(rootResolved, targetResolved);
65
+ }
66
+ return relative(rootResolved, targetResolved);
67
+ }
55
68
  function isResolvedPathUnderRoot(root, target) {
56
- const rootResolved = resolveComparablePath(root);
57
- const targetResolved = resolveComparablePath(target);
69
+ const rootResolved = normalizePathForComparison(root);
70
+ const targetResolved = normalizePathForComparison(target);
58
71
  if (targetResolved === rootResolved) return true;
59
- const prefix = rootResolved.endsWith(sep) ? rootResolved : rootResolved + sep;
60
- const win32Prefix = rootResolved.endsWith(win32.sep) ? rootResolved : rootResolved + win32.sep;
61
- return targetResolved.startsWith(prefix) || targetResolved.startsWith(win32Prefix);
72
+ const rel = relativePathUnderRoot(root, target);
73
+ if (!rel) return true;
74
+ return !rel.startsWith("..") && !isAbsolute(rel);
62
75
  }
63
76
 
64
77
  // src/theme.ts
@@ -79,6 +92,70 @@ function themeToLxpackRuntime(input) {
79
92
  // src/validateDescriptor.ts
80
93
  var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
81
94
  var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
95
+ function isRecord(value) {
96
+ return typeof value === "object" && value !== null && !Array.isArray(value);
97
+ }
98
+ function parseLessonDescriptor(raw) {
99
+ if (!isRecord(raw)) {
100
+ return { id: "", title: "" };
101
+ }
102
+ return {
103
+ id: typeof raw.id === "string" ? raw.id : "",
104
+ title: typeof raw.title === "string" ? raw.title : "",
105
+ spaPath: typeof raw.spaPath === "string" ? raw.spaPath : void 0
106
+ };
107
+ }
108
+ function parseAssessmentDescriptor(raw) {
109
+ if (!isRecord(raw)) {
110
+ return { checkId: "", question: "", choices: [], answer: "" };
111
+ }
112
+ return {
113
+ checkId: typeof raw.checkId === "string" ? raw.checkId : "",
114
+ question: typeof raw.question === "string" ? raw.question : "",
115
+ choices: Array.isArray(raw.choices) ? raw.choices.filter((c) => typeof c === "string") : [],
116
+ answer: typeof raw.answer === "string" ? raw.answer : "",
117
+ passingScore: typeof raw.passingScore === "number" ? raw.passingScore : void 0
118
+ };
119
+ }
120
+ function parseCourseDescriptorInput(input) {
121
+ if (!isRecord(input)) return null;
122
+ const trackingRaw = input.tracking;
123
+ let tracking;
124
+ if (isRecord(trackingRaw)) {
125
+ const completionRaw = trackingRaw.completion;
126
+ const xapiRaw = trackingRaw.xapi;
127
+ tracking = {
128
+ completion: isRecord(completionRaw) ? {
129
+ threshold: typeof completionRaw.threshold === "number" ? completionRaw.threshold : void 0
130
+ } : void 0,
131
+ xapi: isRecord(xapiRaw) ? {
132
+ activityIri: typeof xapiRaw.activityIri === "string" ? xapiRaw.activityIri : void 0
133
+ } : void 0
134
+ };
135
+ }
136
+ const themeRaw = input.theme;
137
+ let theme;
138
+ if (isRecord(themeRaw)) {
139
+ theme = {
140
+ preset: typeof themeRaw.preset === "string" ? themeRaw.preset : void 0
141
+ };
142
+ if (isRecord(themeRaw.theme)) {
143
+ theme.theme = themeRaw.theme;
144
+ }
145
+ }
146
+ return {
147
+ courseId: typeof input.courseId === "string" ? input.courseId : "",
148
+ title: typeof input.title === "string" ? input.title : "",
149
+ version: typeof input.version === "string" ? input.version : void 0,
150
+ layout: typeof input.layout === "string" ? input.layout : void 0,
151
+ lessons: Array.isArray(input.lessons) ? input.lessons.map(parseLessonDescriptor) : [],
152
+ assessments: Array.isArray(input.assessments) ? input.assessments.map(parseAssessmentDescriptor) : void 0,
153
+ theme,
154
+ tracking,
155
+ spaDistDir: typeof input.spaDistDir === "string" ? input.spaDistDir : void 0,
156
+ spaLessonId: typeof input.spaLessonId === "string" ? input.spaLessonId : void 0
157
+ };
158
+ }
82
159
  function normalizeDescriptor(input) {
83
160
  const course = validateId(input.courseId, "courseId");
84
161
  if (!course.ok) throw new Error("normalizeDescriptor called with invalid courseId");
@@ -112,6 +189,31 @@ function normalizeDescriptor(input) {
112
189
  };
113
190
  }
114
191
  function validateDescriptor(input) {
192
+ const parsed = parseCourseDescriptorInput(input);
193
+ if (parsed === null) {
194
+ return { ok: false, issues: [{ path: "course", message: "must be an object" }] };
195
+ }
196
+ return validateDescriptorParsed(parsed);
197
+ }
198
+ function validateDescriptorForTarget(input, target) {
199
+ const result = validateDescriptor(input);
200
+ if (!result.ok || !target) return result;
201
+ if (target !== "xapi" && target !== "cmi5") return result;
202
+ const activityIri = result.descriptor.tracking?.xapi?.activityIri?.trim();
203
+ if (!activityIri) {
204
+ return {
205
+ ok: false,
206
+ issues: [
207
+ {
208
+ path: "course.tracking.xapi.activityIri",
209
+ message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
210
+ }
211
+ ]
212
+ };
213
+ }
214
+ return result;
215
+ }
216
+ function validateDescriptorParsed(input) {
115
217
  const issues = [];
116
218
  const course = validateId(input.courseId, "courseId");
117
219
  if (!course.ok) issues.push(...course.issues.map((i) => ({ path: i.path, message: i.message })));
@@ -247,7 +349,7 @@ function validateDescriptor(input) {
247
349
  }
248
350
 
249
351
  // src/validateProjectPaths.ts
250
- import { isAbsolute, resolve as resolve2 } from "path";
352
+ import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
251
353
  function validatePathField(value, fieldPath, projectRoot, issues) {
252
354
  if (!isSafeRelativeSpaPath(value)) {
253
355
  issues.push({
@@ -257,7 +359,7 @@ function validatePathField(value, fieldPath, projectRoot, issues) {
257
359
  return;
258
360
  }
259
361
  try {
260
- assertResolvedPathUnderRoot(projectRoot, resolve2(projectRoot, value));
362
+ assertRealPathUnderRoot(projectRoot, resolve2(projectRoot, value));
261
363
  } catch {
262
364
  issues.push({
263
365
  path: fieldPath,
@@ -285,16 +387,16 @@ function resolveSafePackageOutputOverride(projectRoot, override) {
285
387
  if (!trimmed) {
286
388
  throw new Error("output override must be a non-empty path");
287
389
  }
288
- if (isAbsolute(trimmed)) {
390
+ if (isAbsolute2(trimmed)) {
289
391
  const resolved2 = resolve2(trimmed);
290
- assertResolvedPathUnderRoot(root, resolved2);
392
+ assertRealPathUnderRoot(root, resolved2);
291
393
  return resolved2;
292
394
  }
293
395
  if (!isSafeRelativeSpaPath(trimmed)) {
294
396
  throw new Error(`unsafe output path: ${override}`);
295
397
  }
296
398
  const resolved = resolve2(root, trimmed);
297
- assertResolvedPathUnderRoot(root, resolved);
399
+ assertRealPathUnderRoot(root, resolved);
298
400
  return resolved;
299
401
  }
300
402
 
@@ -341,6 +443,18 @@ function extractAssessments(descriptor) {
341
443
  }
342
444
 
343
445
  // src/interchange.ts
446
+ function mapDescriptorTracking(tracking) {
447
+ if (!tracking) return void 0;
448
+ const mapped = {};
449
+ if (tracking.completion?.threshold !== void 0) {
450
+ mapped.completion = { threshold: tracking.completion.threshold };
451
+ }
452
+ const activityIri = tracking.xapi?.activityIri?.trim();
453
+ if (activityIri) {
454
+ mapped.xapi = { activityIri };
455
+ }
456
+ return Object.keys(mapped).length > 0 ? mapped : void 0;
457
+ }
344
458
  function resolveSpaLessons(descriptor) {
345
459
  const mapped = mapLessonkitIds(descriptor);
346
460
  if (descriptor.layout === "single-spa") {
@@ -378,7 +492,7 @@ function descriptorToInterchange(descriptor) {
378
492
  type: "spa",
379
493
  path: l.path
380
494
  })),
381
- tracking: descriptor.tracking,
495
+ tracking: mapDescriptorTracking(descriptor.tracking),
382
496
  runtime: runtime ? {
383
497
  theme: runtime.theme,
384
498
  cssVariables: runtime.cssVariables
@@ -455,7 +569,7 @@ async function writeLxpackProject(options) {
455
569
  const descriptor = validation.descriptor;
456
570
  const outDir = resolve4(options.outDir);
457
571
  if (options.projectRoot) {
458
- assertResolvedPathUnderRoot(resolve4(options.projectRoot), outDir);
572
+ assertRealPathUnderRoot(resolve4(options.projectRoot), outDir);
459
573
  }
460
574
  const spaDirs = await resolveSpaDirs({ ...options, descriptor });
461
575
  const interchange = descriptorToInterchange(descriptor);
@@ -487,14 +601,14 @@ import {
487
601
  } from "@lxpack/api";
488
602
 
489
603
  // src/packaging/validateInputs.ts
490
- import { join as join3, resolve as resolve5, win32 as win322 } from "path";
604
+ import { isAbsolute as isAbsolute3, join as join3, resolve as resolve5, win32 as win322 } from "path";
491
605
  function validatePackageInputs(options) {
492
606
  const { target, output, outputBaseDir } = options;
493
607
  const outDir = resolve5(options.outDir);
494
608
  const projectRoot = options.projectRoot ? resolve5(options.projectRoot) : void 0;
495
609
  if (projectRoot) {
496
610
  try {
497
- assertResolvedPathUnderRoot(projectRoot, outDir);
611
+ assertRealPathUnderRoot(projectRoot, outDir);
498
612
  } catch (err) {
499
613
  return {
500
614
  ok: false,
@@ -541,7 +655,7 @@ function validatePackageInputs(options) {
541
655
  if (projectRoot && output) {
542
656
  const resolvedOutput = resolve5(projectRoot, output);
543
657
  try {
544
- assertResolvedPathUnderRoot(projectRoot, resolvedOutput);
658
+ assertRealPathUnderRoot(projectRoot, resolvedOutput);
545
659
  } catch (err) {
546
660
  return {
547
661
  ok: false,
@@ -570,17 +684,21 @@ function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
570
684
  if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
571
685
  return artifactPath;
572
686
  }
573
- const stagingResolved = resolveComparablePath(stagingRoot);
574
- const relative2 = resolved === stagingResolved ? "" : resolved.slice(stagingResolved.length).replace(/^[/\\]/, "");
575
- if (!relative2) return outDir;
687
+ const rel = relativePathUnderRoot(stagingRoot, resolved);
688
+ if (rel.startsWith("..") || isAbsolute3(rel)) {
689
+ return artifactPath;
690
+ }
691
+ if (!rel) return outDir;
576
692
  if (/^[a-zA-Z]:[/\\]/.test(outDir)) {
577
- return win322.join(outDir, relative2.replace(/\//g, win322.sep));
693
+ return win322.join(outDir, rel.replace(/\//g, win322.sep));
578
694
  }
579
- return join3(outDir, relative2);
695
+ return join3(outDir, rel);
580
696
  }
581
697
 
582
698
  // src/packaging/promote.ts
583
699
  import * as fsp from "fs/promises";
700
+ import { randomUUID } from "crypto";
701
+ import { dirname, join as join4 } from "path";
584
702
  async function pathExists(path) {
585
703
  try {
586
704
  await fsp.access(path);
@@ -599,22 +717,36 @@ async function renameOrCopy(from, to) {
599
717
  await fsp.rm(from, { recursive: true, force: true });
600
718
  }
601
719
  }
720
+ async function assertNoLegacyPromoteArtifacts(outDir) {
721
+ const legacyTmp = `${outDir}.tmp-promote`;
722
+ const legacyBak = `${outDir}.bak`;
723
+ const stale = [];
724
+ if (await pathExists(legacyTmp)) stale.push(legacyTmp);
725
+ if (await pathExists(legacyBak)) stale.push(legacyBak);
726
+ if (stale.length) {
727
+ throw new Error(
728
+ `[lessonkit/lxpack] cannot promote: remove stale packaging artifacts from a previous failed run: ${stale.join(", ")}`
729
+ );
730
+ }
731
+ }
602
732
  async function promoteStagingToOutDir(stagingDir, outDir) {
603
- const tmpPromote = `${outDir}.tmp-promote`;
604
- const backup = `${outDir}.bak`;
733
+ await assertNoLegacyPromoteArtifacts(outDir);
734
+ const parent = dirname(outDir);
735
+ const tmpPromote = await fsp.mkdtemp(join4(parent, ".lk-promote-"));
605
736
  await renameOrCopy(stagingDir, tmpPromote);
606
737
  const hadOutDir = await pathExists(outDir);
607
- if (hadOutDir) {
738
+ const backup = hadOutDir ? await fsp.mkdtemp(join4(parent, ".lk-backup-")) : void 0;
739
+ if (hadOutDir && backup) {
608
740
  await renameOrCopy(outDir, backup);
609
741
  }
610
742
  try {
611
743
  await renameOrCopy(tmpPromote, outDir);
612
744
  } catch (promoteError) {
613
- if (hadOutDir) {
745
+ if (hadOutDir && backup) {
614
746
  try {
615
747
  await renameOrCopy(backup, outDir);
616
748
  } catch (restoreError) {
617
- const failedPromote2 = `${outDir}.failed-promote-${Date.now()}`;
749
+ const failedPromote2 = join4(parent, `.lk-failed-promote-${randomUUID()}`);
618
750
  try {
619
751
  await renameOrCopy(tmpPromote, failedPromote2);
620
752
  } catch {
@@ -638,7 +770,7 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
638
770
  }
639
771
  throw promoteError;
640
772
  }
641
- const failedPromote = `${outDir}.failed-promote-${Date.now()}`;
773
+ const failedPromote = join4(parent, `.lk-failed-promote-${randomUUID()}`);
642
774
  try {
643
775
  await renameOrCopy(tmpPromote, failedPromote);
644
776
  } catch {
@@ -646,19 +778,19 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
646
778
  }
647
779
  throw promoteError;
648
780
  }
649
- if (hadOutDir) {
781
+ if (backup) {
650
782
  await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
651
783
  }
652
784
  }
653
785
 
654
786
  // src/packaging/staging.ts
655
787
  import * as fsp2 from "fs/promises";
656
- import { dirname, join as join4 } from "path";
788
+ import { dirname as dirname2, join as join5 } from "path";
657
789
  import { tmpdir } from "os";
658
790
  import { packageLessonkit } from "@lxpack/api";
659
791
  async function buildStagingPackage(options) {
660
792
  const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
661
- const stagingDir = await fsp2.mkdtemp(join4(tmpdir(), "lessonkit-lxpack-"));
793
+ const stagingDir = await fsp2.mkdtemp(join5(tmpdir(), "lessonkit-lxpack-"));
662
794
  try {
663
795
  let spaDirs;
664
796
  try {
@@ -677,8 +809,8 @@ async function buildStagingPackage(options) {
677
809
  }
678
810
  const interchange = descriptorToInterchange(descriptor);
679
811
  const outputBase = outputBaseDir ?? ".lxpack/out";
680
- await fsp2.mkdir(join4(stagingDir, outputBase), { recursive: true });
681
- const defaultOutput = output ?? (dir ? join4(outputBase, target) : join4(outputBase, `course-${target}.zip`));
812
+ await fsp2.mkdir(join5(stagingDir, outputBase), { recursive: true });
813
+ const defaultOutput = output ?? (dir ? join5(outputBase, target) : join5(outputBase, `course-${target}.zip`));
682
814
  const build = await packageLessonkit({
683
815
  interchange,
684
816
  spaDirs,
@@ -715,7 +847,7 @@ async function buildStagingPackage(options) {
715
847
  }
716
848
  }
717
849
  async function ensureOutDirParent(outDir) {
718
- await fsp2.mkdir(dirname(outDir), { recursive: true });
850
+ await fsp2.mkdir(dirname2(outDir), { recursive: true });
719
851
  }
720
852
 
721
853
  // src/packageCourse.ts
@@ -726,14 +858,15 @@ async function validateLessonkitProject(options) {
726
858
  });
727
859
  }
728
860
  async function buildLessonkitProject(options) {
729
- return buildCourse({
861
+ const buildOptions = {
730
862
  courseDir: resolve6(options.courseDir),
731
863
  target: options.target,
732
864
  output: options.output,
733
865
  dir: options.dir,
734
866
  outputBaseDir: options.outputBaseDir,
735
867
  assessments: options.assessments
736
- });
868
+ };
869
+ return buildCourse(buildOptions);
737
870
  }
738
871
  async function packageLessonkitCourse(options) {
739
872
  const { target, output, dir, outputBaseDir, ...writeOpts } = options;
@@ -753,11 +886,11 @@ async function packageLessonkitCourse(options) {
753
886
  };
754
887
  }
755
888
  const outDir = inputValidation.outDir;
756
- const descriptorValidation = validateDescriptor(writeOpts.descriptor);
889
+ const descriptorValidation = validateDescriptorForTarget(writeOpts.descriptor, target);
757
890
  if (!descriptorValidation.ok) {
758
891
  return {
759
892
  ok: false,
760
- courseDir: outDir,
893
+ courseDir: resolve6(writeOpts.outDir),
761
894
  target,
762
895
  issues: descriptorValidation.issues.map((i) => ({
763
896
  path: i.path,
@@ -766,22 +899,6 @@ async function packageLessonkitCourse(options) {
766
899
  };
767
900
  }
768
901
  const descriptor = descriptorValidation.descriptor;
769
- if (target === "xapi" || target === "cmi5") {
770
- const activityIri = descriptor.tracking?.xapi?.activityIri?.trim();
771
- if (!activityIri) {
772
- return {
773
- ok: false,
774
- courseDir: outDir,
775
- target,
776
- issues: [
777
- {
778
- path: "course.tracking.xapi.activityIri",
779
- message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
780
- }
781
- ]
782
- };
783
- }
784
- }
785
902
  const staged = await buildStagingPackage({
786
903
  ...writeOpts,
787
904
  descriptor,
@@ -875,14 +992,19 @@ function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
875
992
  }
876
993
  const config = raw;
877
994
  const issues = [];
878
- if (config.schemaVersion !== 1) {
995
+ let schemaVersion = config.schemaVersion;
996
+ if (schemaVersion === "1") {
997
+ schemaVersion = 1;
998
+ }
999
+ if (schemaVersion !== 1) {
879
1000
  issues.push({
880
1001
  path: "schemaVersion",
881
1002
  message: `must be 1 (got ${String(config.schemaVersion)})`
882
1003
  });
883
1004
  }
884
- const name = config.name;
885
- if (typeof name !== "string" || !name.trim()) {
1005
+ const nameRaw = config.name;
1006
+ const name = typeof nameRaw === "string" ? nameRaw.trim() : "";
1007
+ if (!name) {
886
1008
  issues.push({ path: "name", message: "must be a non-empty string" });
887
1009
  }
888
1010
  const courseRaw = config.course;
@@ -1000,6 +1122,7 @@ export {
1000
1122
  telemetryEventToLessonkit,
1001
1123
  themeToLxpackRuntime,
1002
1124
  validateDescriptor,
1125
+ validateDescriptorForTarget,
1003
1126
  validateLessonkitProject,
1004
1127
  validatePackageInputs,
1005
1128
  validateProjectPaths,
@@ -0,0 +1,100 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://lessonkit.dev/schemas/lessonkit-manifest.v1.json",
4
+ "title": "LessonkitManifestV1",
5
+ "description": "Root lessonkit.json project manifest (schemaVersion 1)",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schemaVersion", "name", "course", "paths"],
9
+ "properties": {
10
+ "schemaVersion": { "const": 1 },
11
+ "name": { "type": "string", "minLength": 1 },
12
+ "course": { "$ref": "#/$defs/LessonkitCourseDescriptor" },
13
+ "paths": {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "required": ["spaDistDir", "lxpackOutDir", "outputBaseDir"],
17
+ "properties": {
18
+ "spaDistDir": { "type": "string", "minLength": 1 },
19
+ "lxpackOutDir": { "type": "string", "minLength": 1 },
20
+ "outputBaseDir": { "type": "string", "minLength": 1 }
21
+ }
22
+ }
23
+ },
24
+ "$defs": {
25
+ "LessonkitId": {
26
+ "type": "string",
27
+ "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
28
+ "maxLength": 64
29
+ },
30
+ "LessonkitCourseDescriptor": {
31
+ "type": "object",
32
+ "required": ["courseId", "title", "layout", "lessons"],
33
+ "properties": {
34
+ "courseId": { "$ref": "#/$defs/LessonkitId" },
35
+ "title": { "type": "string", "minLength": 1 },
36
+ "version": { "type": "string" },
37
+ "layout": { "enum": ["single-spa", "per-lesson-spa"] },
38
+ "lessons": {
39
+ "type": "array",
40
+ "minItems": 1,
41
+ "items": { "$ref": "#/$defs/LessonDescriptor" }
42
+ },
43
+ "assessments": {
44
+ "type": "array",
45
+ "items": { "$ref": "#/$defs/AssessmentDescriptor" }
46
+ },
47
+ "theme": {
48
+ "type": "object",
49
+ "properties": {
50
+ "preset": { "enum": ["default", "light", "dark", "brand"] },
51
+ "theme": { "type": "object" }
52
+ }
53
+ },
54
+ "tracking": {
55
+ "type": "object",
56
+ "properties": {
57
+ "completion": {
58
+ "type": "object",
59
+ "properties": {
60
+ "threshold": { "type": "number", "minimum": 0, "maximum": 1 }
61
+ }
62
+ },
63
+ "xapi": {
64
+ "type": "object",
65
+ "properties": {
66
+ "activityIri": { "type": "string", "minLength": 1 }
67
+ }
68
+ }
69
+ }
70
+ },
71
+ "spaDistDir": { "type": "string", "minLength": 1 },
72
+ "spaLessonId": { "$ref": "#/$defs/LessonkitId" }
73
+ }
74
+ },
75
+ "LessonDescriptor": {
76
+ "type": "object",
77
+ "required": ["id", "title"],
78
+ "properties": {
79
+ "id": { "$ref": "#/$defs/LessonkitId" },
80
+ "title": { "type": "string", "minLength": 1 },
81
+ "spaPath": { "type": "string", "minLength": 1 }
82
+ }
83
+ },
84
+ "AssessmentDescriptor": {
85
+ "type": "object",
86
+ "required": ["checkId", "question", "choices", "answer"],
87
+ "properties": {
88
+ "checkId": { "$ref": "#/$defs/LessonkitId" },
89
+ "question": { "type": "string", "minLength": 1 },
90
+ "choices": {
91
+ "type": "array",
92
+ "minItems": 1,
93
+ "items": { "type": "string", "minLength": 1 }
94
+ },
95
+ "answer": { "type": "string", "minLength": 1 },
96
+ "passingScore": { "type": "number", "exclusiveMinimum": 0 }
97
+ }
98
+ }
99
+ }
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/lxpack",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "private": false,
5
5
  "description": "LXPack export adapter for LessonKit courses (SCORM, standalone, xAPI, cmi5).",
6
6
  "license": "Apache-2.0",
@@ -38,10 +38,12 @@
38
38
  "types": "./dist/bridge.d.ts",
39
39
  "import": "./dist/bridge.js",
40
40
  "require": "./dist/bridge.cjs"
41
- }
41
+ },
42
+ "./lessonkit-manifest.v1.json": "./lessonkit-manifest.v1.json"
42
43
  },
43
44
  "files": [
44
- "dist"
45
+ "dist",
46
+ "lessonkit-manifest.v1.json"
45
47
  ],
46
48
  "scripts": {
47
49
  "build": "tsup src/index.ts src/bridge.ts --format esm,cjs --dts",
@@ -53,8 +55,8 @@
53
55
  "lint": "echo \"(no lint configured yet)\""
54
56
  },
55
57
  "dependencies": {
56
- "@lessonkit/core": "1.0.0",
57
- "@lessonkit/themes": "1.0.0",
58
+ "@lessonkit/core": "1.0.2",
59
+ "@lessonkit/themes": "1.0.2",
58
60
  "@lxpack/api": "^0.6.2",
59
61
  "@lxpack/spa-bridge": "^0.6.2",
60
62
  "@lxpack/tracking-schema": "^0.6.2",
@@ -64,6 +66,6 @@
64
66
  "@types/node": "^22.13.10",
65
67
  "tsup": "^8.5.0",
66
68
  "typescript": "^5.8.3",
67
- "vitest": "^3.2.4"
69
+ "vitest": "^4.1.8"
68
70
  }
69
71
  }