@lessonkit/lxpack 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,15 @@
1
1
  # @lessonkit/lxpack
2
2
 
3
+ [![Documentation](https://readthedocs.org/projects/lessonkit/badge/?version=latest)](https://lessonkit.readthedocs.io/en/latest/)
4
+ [![npm](https://img.shields.io/npm/v/@lessonkit/lxpack.svg)](https://www.npmjs.com/package/@lessonkit/lxpack)
5
+ [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](https://github.com/eddiethedean/lessonkit/blob/main/LICENSE)
6
+
3
7
  LXPack export adapter for LessonKit — write `lessonkit.json` + `course.yaml`, copy SPA builds, and package to SCORM / standalone / xAPI / cmi5 via [`@lxpack/api`](https://www.npmjs.com/package/@lxpack/api).
4
8
 
5
9
  Requires **Node.js 20+**.
6
10
 
11
+ **Docs:** [Packaging reference](https://lessonkit.readthedocs.io/en/latest/reference/packaging.html) · [Packaging & CLI guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html) · [Live examples](https://lessonkit.readthedocs.io/en/latest/examples/index.html)
12
+
7
13
  ## Install
8
14
 
9
15
  ```bash
@@ -27,7 +33,7 @@ const result = await packageLessonkitCourse({
27
33
  if (!result.ok) throw new Error("packaging failed");
28
34
  ```
29
35
 
30
- See [`docs/PACKAGING.md`](../../docs/PACKAGING.md) and [`examples/lxpack-golden`](../../examples/lxpack-golden).
36
+ See the [packaging reference](https://lessonkit.readthedocs.io/en/latest/reference/packaging.html) and the [`examples/lxpack-golden`](https://github.com/eddiethedean/lessonkit/tree/main/examples/lxpack-golden) course.
31
37
 
32
38
  ## Browser bridge
33
39
 
@@ -38,3 +44,5 @@ import { notifyLxpackLessonComplete } from "@lessonkit/lxpack/bridge";
38
44
  ```
39
45
 
40
46
  `@lessonkit/react` forwards `lesson_completed`, `course_completed`, and `quiz_completed` automatically when `window.parent.lxpackBridge.v1` is present (`config.lxpack.bridge: "off"` to disable).
47
+
48
+ For interoperability notes, see [LXPack upgrades](https://lessonkit.readthedocs.io/en/latest/reference/lxpack-upgrades.html). LXPack maintainers: [upgrade plan for maintainers](https://github.com/eddiethedean/lessonkit/blob/main/docs/LXPACK_UPGRADE_PLAN_FOR_MAINTAINERS.md).
package/dist/bridge.cjs CHANGED
@@ -33,10 +33,15 @@ function normalizeAssessmentScore(opts) {
33
33
  return null;
34
34
  }
35
35
  const maxScore = typeof opts.maxScore === "number" && opts.maxScore > 0 ? opts.maxScore : 1;
36
- return opts.score / maxScore;
36
+ return Math.min(1, opts.score / maxScore);
37
37
  }
38
- function normalizeAssessmentPassingScore(passingScore) {
39
- return typeof passingScore === "number" && passingScore > 0 ? passingScore : 1;
38
+ function normalizeAssessmentPassingScore(opts) {
39
+ const passingScore = opts?.passingScore;
40
+ if (typeof passingScore !== "number" || !Number.isFinite(passingScore) || passingScore <= 0) {
41
+ return 1;
42
+ }
43
+ const maxScore = typeof opts?.maxScore === "number" && opts.maxScore > 0 ? opts.maxScore : 1;
44
+ return Math.min(1, passingScore / maxScore);
40
45
  }
41
46
  function getBridge() {
42
47
  if (typeof window === "undefined") return null;
package/dist/bridge.d.cts CHANGED
@@ -28,8 +28,14 @@ declare function normalizeAssessmentScore(opts: {
28
28
  score?: number;
29
29
  maxScore?: number;
30
30
  }): number | null;
31
- /** Bridge passing threshold (0–1 scale). Defaults to 1 when omitted or invalid. */
32
- declare function normalizeAssessmentPassingScore(passingScore?: number): number;
31
+ /**
32
+ * Scale a raw passing threshold to 0–1 for the LXPack parent bridge.
33
+ * Uses the same `maxScore` denominator as `normalizeAssessmentScore`. Defaults to 1 when omitted or invalid.
34
+ */
35
+ declare function normalizeAssessmentPassingScore(opts?: {
36
+ passingScore?: number;
37
+ maxScore?: number;
38
+ }): number;
33
39
  declare function createLxpackBridge(): LxpackBridgeV1 | null;
34
40
  declare function notifyLxpackLessonComplete(lessonId: LessonId): boolean;
35
41
  declare function notifyLxpackCourseComplete(): boolean;
package/dist/bridge.d.ts CHANGED
@@ -28,8 +28,14 @@ declare function normalizeAssessmentScore(opts: {
28
28
  score?: number;
29
29
  maxScore?: number;
30
30
  }): number | null;
31
- /** Bridge passing threshold (0–1 scale). Defaults to 1 when omitted or invalid. */
32
- declare function normalizeAssessmentPassingScore(passingScore?: number): number;
31
+ /**
32
+ * Scale a raw passing threshold to 0–1 for the LXPack parent bridge.
33
+ * Uses the same `maxScore` denominator as `normalizeAssessmentScore`. Defaults to 1 when omitted or invalid.
34
+ */
35
+ declare function normalizeAssessmentPassingScore(opts?: {
36
+ passingScore?: number;
37
+ maxScore?: number;
38
+ }): number;
33
39
  declare function createLxpackBridge(): LxpackBridgeV1 | null;
34
40
  declare function notifyLxpackLessonComplete(lessonId: LessonId): boolean;
35
41
  declare function notifyLxpackCourseComplete(): boolean;
package/dist/bridge.js CHANGED
@@ -4,10 +4,15 @@ function normalizeAssessmentScore(opts) {
4
4
  return null;
5
5
  }
6
6
  const maxScore = typeof opts.maxScore === "number" && opts.maxScore > 0 ? opts.maxScore : 1;
7
- return opts.score / maxScore;
7
+ return Math.min(1, opts.score / maxScore);
8
8
  }
9
- function normalizeAssessmentPassingScore(passingScore) {
10
- return typeof passingScore === "number" && passingScore > 0 ? passingScore : 1;
9
+ function normalizeAssessmentPassingScore(opts) {
10
+ const passingScore = opts?.passingScore;
11
+ if (typeof passingScore !== "number" || !Number.isFinite(passingScore) || passingScore <= 0) {
12
+ return 1;
13
+ }
14
+ const maxScore = typeof opts?.maxScore === "number" && opts.maxScore > 0 ? opts.maxScore : 1;
15
+ return Math.min(1, passingScore / maxScore);
11
16
  }
12
17
  function getBridge() {
13
18
  if (typeof window === "undefined") return null;
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -26,10 +36,12 @@ __export(index_exports, {
26
36
  extractAssessments: () => extractAssessments,
27
37
  mapLessonkitIds: () => mapLessonkitIds,
28
38
  packageLessonkitCourse: () => packageLessonkitCourse,
39
+ resolveSafePackageOutputOverride: () => resolveSafePackageOutputOverride,
29
40
  resolveSpaLessons: () => resolveSpaLessons,
30
41
  themeToLxpackRuntime: () => themeToLxpackRuntime,
31
42
  validateDescriptor: () => validateDescriptor,
32
43
  validateLessonkitProject: () => validateLessonkitProject,
44
+ validateProjectPaths: () => validateProjectPaths,
33
45
  writeLxpackProject: () => writeLxpackProject
34
46
  });
35
47
  module.exports = __toCommonJS(index_exports);
@@ -43,8 +55,8 @@ function isSafeRelativeSpaPath(spaPath) {
43
55
  if (!spaPath.length || spaPath.includes("\0")) return false;
44
56
  if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
45
57
  if (/^[a-zA-Z]:[/\\]/.test(spaPath)) return false;
46
- const segments = spaPath.split(/[/\\]/);
47
- if (segments.some((s) => s === "..")) return false;
58
+ const segments = spaPath.split(/[/\\]/).filter((s) => s.length > 0);
59
+ if (segments.some((s) => s === ".." || s === ".")) return false;
48
60
  return true;
49
61
  }
50
62
  function assertResolvedPathUnderRoot(root, target) {
@@ -56,6 +68,21 @@ function assertResolvedPathUnderRoot(root, target) {
56
68
  }
57
69
  }
58
70
 
71
+ // src/theme.ts
72
+ var import_themes = require("@lessonkit/themes");
73
+ function themeToLxpackRuntime(input) {
74
+ const theme = input.theme ?? (0, import_themes.getPresetTheme)(input.preset ?? "default");
75
+ const raw = (0, import_themes.themeToCssVariables)(theme);
76
+ const cssVariables = {};
77
+ for (const [key, value] of Object.entries(raw)) {
78
+ cssVariables[key] = String(value);
79
+ }
80
+ return {
81
+ theme: theme.name,
82
+ cssVariables
83
+ };
84
+ }
85
+
59
86
  // src/validateDescriptor.ts
60
87
  var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
61
88
  var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
@@ -117,6 +144,25 @@ function validateDescriptor(input) {
117
144
  message: `unknown preset; use one of: ${VALID_THEME_PRESETS.join(", ")}`
118
145
  });
119
146
  }
147
+ if (input.theme?.theme) {
148
+ try {
149
+ themeToLxpackRuntime({ preset: themePreset, theme: input.theme.theme });
150
+ } catch (err) {
151
+ issues.push({
152
+ path: "theme.theme",
153
+ message: err instanceof Error ? err.message : "invalid custom theme"
154
+ });
155
+ }
156
+ }
157
+ const completionThreshold = input.tracking?.completion?.threshold;
158
+ if (completionThreshold !== void 0) {
159
+ if (!Number.isFinite(completionThreshold) || completionThreshold < 0 || completionThreshold > 1) {
160
+ issues.push({
161
+ path: "tracking.completion.threshold",
162
+ message: "threshold must be a finite number between 0 and 1"
163
+ });
164
+ }
165
+ }
120
166
  if (layout === "single-spa" && (input.lessons?.length ?? 0) > 1) {
121
167
  issues.push({
122
168
  path: "lessons",
@@ -196,24 +242,69 @@ function validateDescriptor(input) {
196
242
  issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
197
243
  }
198
244
  const passingScore = assessment.passingScore;
199
- if (passingScore !== void 0) {
200
- if (!(passingScore > 0)) {
201
- issues.push({
202
- path: `${path}.passingScore`,
203
- message: "passingScore must be greater than 0"
204
- });
205
- } else if (trimmedChoices.length && passingScore > trimmedChoices.length) {
206
- issues.push({
207
- path: `${path}.passingScore`,
208
- message: "passingScore must not exceed the number of choices"
209
- });
210
- }
245
+ if (passingScore !== void 0 && !(passingScore > 0)) {
246
+ issues.push({
247
+ path: `${path}.passingScore`,
248
+ message: "passingScore must be greater than 0 (absolute point threshold)"
249
+ });
211
250
  }
212
251
  }
213
252
  if (issues.length) return { ok: false, issues };
214
253
  return { ok: true, descriptor: normalizeDescriptor(input) };
215
254
  }
216
255
 
256
+ // src/validateProjectPaths.ts
257
+ var import_node_path2 = require("path");
258
+ function validatePathField(value, fieldPath, projectRoot, issues) {
259
+ if (!isSafeRelativeSpaPath(value)) {
260
+ issues.push({
261
+ path: fieldPath,
262
+ message: "path must be relative without '..' segments or absolute prefixes"
263
+ });
264
+ return;
265
+ }
266
+ try {
267
+ assertResolvedPathUnderRoot(projectRoot, (0, import_node_path2.resolve)(projectRoot, value));
268
+ } catch {
269
+ issues.push({
270
+ path: fieldPath,
271
+ message: "path must resolve inside the project root"
272
+ });
273
+ }
274
+ }
275
+ function validateProjectPaths(projectRoot, paths) {
276
+ const issues = [];
277
+ const root = (0, import_node_path2.resolve)(projectRoot);
278
+ if (paths.spaDistDir?.trim()) {
279
+ validatePathField(paths.spaDistDir.trim(), "paths.spaDistDir", root, issues);
280
+ }
281
+ if (paths.lxpackOutDir?.trim()) {
282
+ validatePathField(paths.lxpackOutDir.trim(), "paths.lxpackOutDir", root, issues);
283
+ }
284
+ if (paths.outputBaseDir?.trim()) {
285
+ validatePathField(paths.outputBaseDir.trim(), "paths.outputBaseDir", root, issues);
286
+ }
287
+ return issues;
288
+ }
289
+ function resolveSafePackageOutputOverride(projectRoot, override) {
290
+ const root = (0, import_node_path2.resolve)(projectRoot);
291
+ const trimmed = override.trim();
292
+ if (!trimmed) {
293
+ throw new Error("output override must be a non-empty path");
294
+ }
295
+ if ((0, import_node_path2.isAbsolute)(trimmed)) {
296
+ const resolved2 = (0, import_node_path2.resolve)(trimmed);
297
+ assertResolvedPathUnderRoot(root, resolved2);
298
+ return resolved2;
299
+ }
300
+ if (!isSafeRelativeSpaPath(trimmed)) {
301
+ throw new Error(`unsafe output path: ${override}`);
302
+ }
303
+ const resolved = (0, import_node_path2.resolve)(root, trimmed);
304
+ assertResolvedPathUnderRoot(root, resolved);
305
+ return resolved;
306
+ }
307
+
217
308
  // src/mapIds.ts
218
309
  var import_core2 = require("@lessonkit/core");
219
310
  function mapLessonkitIds(descriptor) {
@@ -225,21 +316,6 @@ function mapLessonkitIds(descriptor) {
225
316
  return { courseId, lessonIds, checkIds };
226
317
  }
227
318
 
228
- // src/theme.ts
229
- var import_themes = require("@lessonkit/themes");
230
- function themeToLxpackRuntime(input) {
231
- const theme = input.theme ?? (0, import_themes.getPresetTheme)(input.preset ?? "default");
232
- const raw = (0, import_themes.themeToCssVariables)(theme);
233
- const cssVariables = {};
234
- for (const [key, value] of Object.entries(raw)) {
235
- cssVariables[key] = String(value);
236
- }
237
- return {
238
- theme: theme.name,
239
- cssVariables
240
- };
241
- }
242
-
243
319
  // src/interchange.ts
244
320
  function resolveSpaLessons(descriptor) {
245
321
  const mapped = mapLessonkitIds(descriptor);
@@ -313,7 +389,7 @@ function extractAssessments(descriptor) {
313
389
 
314
390
  // src/writeProject.ts
315
391
  var import_promises = require("fs/promises");
316
- var import_node_path2 = require("path");
392
+ var import_node_path3 = require("path");
317
393
 
318
394
  // src/assessmentYaml.ts
319
395
  function yamlQuote(value) {
@@ -388,7 +464,7 @@ function emitCourseYaml(opts) {
388
464
 
389
465
  // src/writeProject.ts
390
466
  async function copyDir(src, dest) {
391
- await (0, import_promises.mkdir)((0, import_node_path2.dirname)(dest), { recursive: true });
467
+ await (0, import_promises.mkdir)((0, import_node_path3.dirname)(dest), { recursive: true });
392
468
  await (0, import_promises.cp)(src, dest, { recursive: true });
393
469
  }
394
470
  async function writeLxpackProject(options) {
@@ -399,7 +475,7 @@ async function writeLxpackProject(options) {
399
475
  );
400
476
  }
401
477
  const descriptor = validation.descriptor;
402
- const outDir = (0, import_node_path2.resolve)(options.outDir);
478
+ const outDir = (0, import_node_path3.resolve)(options.outDir);
403
479
  await (0, import_promises.mkdir)(outDir, { recursive: true });
404
480
  const spaLessons = resolveSpaLessons(descriptor);
405
481
  const runtime = descriptor.theme ? themeToLxpackRuntime(descriptor.theme) : void 0;
@@ -408,13 +484,17 @@ async function writeLxpackProject(options) {
408
484
  file: `assessments/${a.checkId}.yaml`
409
485
  }));
410
486
  if (descriptor.layout === "single-spa") {
411
- const srcDist = (0, import_node_path2.resolve)(options.spaDistDir ?? descriptor.spaDistDir ?? "dist");
487
+ const spaDistRelative = options.spaDistDir ?? descriptor.spaDistDir ?? "dist";
488
+ const srcDist = options.projectRoot ? (0, import_node_path3.resolve)(options.projectRoot, spaDistRelative) : (0, import_node_path3.resolve)(spaDistRelative);
489
+ if (options.projectRoot) {
490
+ assertResolvedPathUnderRoot((0, import_node_path3.resolve)(options.projectRoot), srcDist);
491
+ }
412
492
  try {
413
493
  await (0, import_promises.access)(srcDist);
414
494
  } catch {
415
495
  throw new Error(`spaDistDir not found: ${srcDist}`);
416
496
  }
417
- const destDist = (0, import_node_path2.join)(outDir, "dist");
497
+ const destDist = (0, import_node_path3.join)(outDir, "dist");
418
498
  await (0, import_promises.rm)(destDist, { recursive: true, force: true });
419
499
  await copyDir(srcDist, destDist);
420
500
  } else {
@@ -424,24 +504,24 @@ async function writeLxpackProject(options) {
424
504
  if (!src) {
425
505
  throw new Error(`lessonSpaDirs missing build output for lesson "${lesson.id}"`);
426
506
  }
427
- const dest = (0, import_node_path2.join)(outDir, lesson.spaPath);
507
+ const dest = (0, import_node_path3.join)(outDir, lesson.spaPath);
428
508
  assertResolvedPathUnderRoot(outDir, dest);
429
509
  await (0, import_promises.rm)(dest, { recursive: true, force: true });
430
- await copyDir((0, import_node_path2.resolve)(src), dest);
510
+ await copyDir((0, import_node_path3.resolve)(src), dest);
431
511
  }
432
512
  }
433
513
  if (assessments.length) {
434
- const assessmentsDir = (0, import_node_path2.join)(outDir, "assessments");
514
+ const assessmentsDir = (0, import_node_path3.join)(outDir, "assessments");
435
515
  await (0, import_promises.mkdir)(assessmentsDir, { recursive: true });
436
516
  for (const assessment of descriptor.assessments ?? []) {
437
517
  await (0, import_promises.writeFile)(
438
- (0, import_node_path2.join)(outDir, `assessments/${assessment.checkId}.yaml`),
518
+ (0, import_node_path3.join)(outDir, `assessments/${assessment.checkId}.yaml`),
439
519
  emitAssessmentYaml(assessment),
440
520
  "utf-8"
441
521
  );
442
522
  }
443
523
  }
444
- const courseYamlPath = (0, import_node_path2.join)(outDir, "course.yaml");
524
+ const courseYamlPath = (0, import_node_path3.join)(outDir, "course.yaml");
445
525
  await (0, import_promises.writeFile)(
446
526
  courseYamlPath,
447
527
  emitCourseYaml({
@@ -459,7 +539,7 @@ async function writeLxpackProject(options) {
459
539
  }),
460
540
  "utf-8"
461
541
  );
462
- const lessonkitJsonPath = (0, import_node_path2.join)(outDir, "lessonkit.json");
542
+ const lessonkitJsonPath = (0, import_node_path3.join)(outDir, "lessonkit.json");
463
543
  await (0, import_promises.writeFile)(
464
544
  lessonkitJsonPath,
465
545
  `${JSON.stringify(descriptorToInterchange(descriptor), null, 2)}
@@ -470,19 +550,19 @@ async function writeLxpackProject(options) {
470
550
  }
471
551
 
472
552
  // src/packageCourse.ts
473
- var import_promises2 = require("fs/promises");
474
- var import_node_path3 = require("path");
553
+ var fsp = __toESM(require("fs/promises"), 1);
554
+ var import_node_path4 = require("path");
475
555
  var import_node_os = require("os");
476
556
  var import_api = require("@lxpack/api");
477
557
  async function validateLessonkitProject(options) {
478
558
  return (0, import_api.validateCourse)({
479
- courseDir: (0, import_node_path3.resolve)(options.courseDir),
559
+ courseDir: (0, import_node_path4.resolve)(options.courseDir),
480
560
  target: options.target
481
561
  });
482
562
  }
483
563
  async function buildLessonkitProject(options) {
484
564
  return (0, import_api.buildCourse)({
485
- courseDir: (0, import_node_path3.resolve)(options.courseDir),
565
+ courseDir: (0, import_node_path4.resolve)(options.courseDir),
486
566
  target: options.target,
487
567
  output: options.output,
488
568
  dir: options.dir,
@@ -490,9 +570,38 @@ async function buildLessonkitProject(options) {
490
570
  assessments: options.assessments
491
571
  });
492
572
  }
573
+ async function pathExists(path) {
574
+ try {
575
+ await fsp.access(path);
576
+ return true;
577
+ } catch {
578
+ return false;
579
+ }
580
+ }
581
+ async function promoteStagingToOutDir(stagingDir, outDir) {
582
+ const tmpPromote = `${outDir}.tmp-promote`;
583
+ const backup = `${outDir}.bak`;
584
+ await fsp.rename(stagingDir, tmpPromote);
585
+ const hadOutDir = await pathExists(outDir);
586
+ if (hadOutDir) {
587
+ await fsp.rename(outDir, backup);
588
+ }
589
+ try {
590
+ await fsp.rename(tmpPromote, outDir);
591
+ } catch (promoteError) {
592
+ if (hadOutDir) {
593
+ await fsp.rename(backup, outDir).catch(() => void 0);
594
+ }
595
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
596
+ throw promoteError;
597
+ }
598
+ if (hadOutDir) {
599
+ await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
600
+ }
601
+ }
493
602
  async function packageLessonkitCourse(options) {
494
603
  const { target, output, dir, outputBaseDir, ...writeOpts } = options;
495
- const outDir = (0, import_node_path3.resolve)(writeOpts.outDir);
604
+ const outDir = (0, import_node_path4.resolve)(writeOpts.outDir);
496
605
  const descriptorValidation = validateDescriptor(writeOpts.descriptor);
497
606
  if (!descriptorValidation.ok) {
498
607
  return {
@@ -506,7 +615,7 @@ async function packageLessonkitCourse(options) {
506
615
  };
507
616
  }
508
617
  const descriptor = descriptorValidation.descriptor;
509
- const stagingDir = await (0, import_promises2.mkdtemp)((0, import_node_path3.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
618
+ const stagingDir = await fsp.mkdtemp((0, import_node_path4.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
510
619
  let promoted = false;
511
620
  try {
512
621
  const written = await writeLxpackProject({ ...writeOpts, descriptor, outDir: stagingDir });
@@ -527,12 +636,12 @@ async function packageLessonkitCourse(options) {
527
636
  };
528
637
  }
529
638
  const outputBase = outputBaseDir ?? ".lxpack/out";
530
- await (0, import_promises2.mkdir)((0, import_node_path3.join)(courseDir, outputBase), { recursive: true });
531
- const defaultOutput = output ?? (dir ? (0, import_node_path3.join)(outputBase, target) : (0, import_node_path3.join)(outputBase, `course-${target}.zip`));
639
+ await fsp.mkdir((0, import_node_path4.join)(courseDir, outputBase), { recursive: true });
640
+ const defaultOutput = output ?? (dir ? (0, import_node_path4.join)(outputBase, target) : (0, import_node_path4.join)(outputBase, `course-${target}.zip`));
532
641
  const build = await buildLessonkitProject({
533
642
  courseDir,
534
643
  target,
535
- output: defaultOutput.startsWith("/") ? defaultOutput : (0, import_node_path3.join)(courseDir, defaultOutput),
644
+ output: defaultOutput.startsWith("/") ? defaultOutput : (0, import_node_path4.join)(courseDir, defaultOutput),
536
645
  dir,
537
646
  assessments: assessments.length ? assessments : void 0
538
647
  });
@@ -550,32 +659,42 @@ async function packageLessonkitCourse(options) {
550
659
  }))
551
660
  };
552
661
  }
553
- await (0, import_promises2.rm)(outDir, { recursive: true, force: true });
554
- await (0, import_promises2.mkdir)((0, import_node_path3.dirname)(outDir), { recursive: true });
555
- await (0, import_promises2.rename)(stagingDir, outDir);
662
+ await fsp.mkdir((0, import_node_path4.dirname)(outDir), { recursive: true });
663
+ await promoteStagingToOutDir(stagingDir, outDir);
556
664
  promoted = true;
557
665
  const remapArtifactPath = (artifactPath) => {
558
666
  if (!artifactPath) return void 0;
559
- const resolved = (0, import_node_path3.resolve)(artifactPath);
560
- const stagingResolved = (0, import_node_path3.resolve)(stagingDir);
667
+ const resolved = (0, import_node_path4.resolve)(artifactPath);
668
+ const stagingResolved = (0, import_node_path4.resolve)(stagingDir);
561
669
  if (resolved === stagingResolved || resolved.startsWith(stagingResolved + "/")) {
562
- return (0, import_node_path3.join)(outDir, resolved.slice(stagingResolved.length + 1));
670
+ return (0, import_node_path4.join)(outDir, resolved.slice(stagingResolved.length + 1));
563
671
  }
564
672
  return artifactPath;
565
673
  };
674
+ const remappedOutputPath = remapArtifactPath(
675
+ "outputPath" in build ? build.outputPath : void 0
676
+ );
677
+ const remappedOutputDir = remapArtifactPath("outputDir" in build ? build.outputDir : void 0);
678
+ const remappedBuild = { ...build };
679
+ if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
680
+ remappedBuild.outputPath = remappedOutputPath;
681
+ }
682
+ if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
683
+ remappedBuild.outputDir = remappedOutputDir;
684
+ }
566
685
  return {
567
686
  ok: true,
568
687
  courseDir: outDir,
569
688
  target,
570
- outputPath: remapArtifactPath("outputPath" in build ? build.outputPath : void 0),
571
- outputDir: remapArtifactPath("outputDir" in build ? build.outputDir : void 0),
689
+ outputPath: remappedOutputPath,
690
+ outputDir: remappedOutputDir,
572
691
  fileCount: build.fileCount,
573
692
  validation,
574
- build
693
+ build: remappedBuild
575
694
  };
576
695
  } finally {
577
696
  if (!promoted) {
578
- await (0, import_promises2.rm)(stagingDir, { recursive: true, force: true }).catch(() => void 0);
697
+ await fsp.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
579
698
  }
580
699
  }
581
700
  }
@@ -587,9 +706,11 @@ async function packageLessonkitCourse(options) {
587
706
  extractAssessments,
588
707
  mapLessonkitIds,
589
708
  packageLessonkitCourse,
709
+ resolveSafePackageOutputOverride,
590
710
  resolveSpaLessons,
591
711
  themeToLxpackRuntime,
592
712
  validateDescriptor,
593
713
  validateLessonkitProject,
714
+ validateProjectPaths,
594
715
  writeLxpackProject
595
716
  });
package/dist/index.d.cts CHANGED
@@ -76,6 +76,18 @@ type DescriptorValidationResult = {
76
76
  };
77
77
  declare function validateDescriptor(input: LessonkitCourseDescriptor): DescriptorValidationResult;
78
78
 
79
+ type ProjectPathsInput = {
80
+ spaDistDir?: string;
81
+ lxpackOutDir?: string;
82
+ outputBaseDir?: string;
83
+ };
84
+ /** Validate lessonkit.json paths.* entries stay under projectRoot. */
85
+ declare function validateProjectPaths(projectRoot: string, paths: ProjectPathsInput): DescriptorValidationIssue[];
86
+ /**
87
+ * Resolve a package --out override under projectRoot and ensure it stays inside the project.
88
+ */
89
+ declare function resolveSafePackageOutputOverride(projectRoot: string, override: string): string;
90
+
79
91
  /**
80
92
  * Stable 1:1 mapping from LessonKit ids to LXPack manifest ids.
81
93
  * LessonKit slug rules already match LXPack `activityIdSchema` (letter-first, alphanumeric + _ -).
@@ -129,6 +141,8 @@ type WriteLxpackProjectOptions = {
129
141
  * For `per-lesson-spa`: map lesson id → absolute path to that lesson's built SPA folder.
130
142
  */
131
143
  lessonSpaDirs?: Record<string, string>;
144
+ /** When set, relative `spaDistDir` is resolved under this directory instead of `process.cwd()`. */
145
+ projectRoot?: string;
132
146
  };
133
147
  type WriteLxpackProjectResult = {
134
148
  outDir: string;
@@ -181,4 +195,4 @@ declare function validateLessonkitProject(options: ValidateLessonkitProjectOptio
181
195
  declare function buildLessonkitProject(options: BuildLessonkitProjectOptions): Promise<BuildCourseResult>;
182
196
  declare function packageLessonkitCourse(options: PackageLessonkitCourseOptions): Promise<PackageLessonkitCourseResult>;
183
197
 
184
- export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitInterchangeV1, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, descriptorToInterchange, extractAssessments, mapLessonkitIds, packageLessonkitCourse, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateLessonkitProject, writeLxpackProject };
198
+ export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitInterchangeV1, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, descriptorToInterchange, extractAssessments, mapLessonkitIds, packageLessonkitCourse, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateLessonkitProject, validateProjectPaths, writeLxpackProject };
package/dist/index.d.ts CHANGED
@@ -76,6 +76,18 @@ type DescriptorValidationResult = {
76
76
  };
77
77
  declare function validateDescriptor(input: LessonkitCourseDescriptor): DescriptorValidationResult;
78
78
 
79
+ type ProjectPathsInput = {
80
+ spaDistDir?: string;
81
+ lxpackOutDir?: string;
82
+ outputBaseDir?: string;
83
+ };
84
+ /** Validate lessonkit.json paths.* entries stay under projectRoot. */
85
+ declare function validateProjectPaths(projectRoot: string, paths: ProjectPathsInput): DescriptorValidationIssue[];
86
+ /**
87
+ * Resolve a package --out override under projectRoot and ensure it stays inside the project.
88
+ */
89
+ declare function resolveSafePackageOutputOverride(projectRoot: string, override: string): string;
90
+
79
91
  /**
80
92
  * Stable 1:1 mapping from LessonKit ids to LXPack manifest ids.
81
93
  * LessonKit slug rules already match LXPack `activityIdSchema` (letter-first, alphanumeric + _ -).
@@ -129,6 +141,8 @@ type WriteLxpackProjectOptions = {
129
141
  * For `per-lesson-spa`: map lesson id → absolute path to that lesson's built SPA folder.
130
142
  */
131
143
  lessonSpaDirs?: Record<string, string>;
144
+ /** When set, relative `spaDistDir` is resolved under this directory instead of `process.cwd()`. */
145
+ projectRoot?: string;
132
146
  };
133
147
  type WriteLxpackProjectResult = {
134
148
  outDir: string;
@@ -181,4 +195,4 @@ declare function validateLessonkitProject(options: ValidateLessonkitProjectOptio
181
195
  declare function buildLessonkitProject(options: BuildLessonkitProjectOptions): Promise<BuildCourseResult>;
182
196
  declare function packageLessonkitCourse(options: PackageLessonkitCourseOptions): Promise<PackageLessonkitCourseResult>;
183
197
 
184
- export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitInterchangeV1, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, descriptorToInterchange, extractAssessments, mapLessonkitIds, packageLessonkitCourse, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateLessonkitProject, writeLxpackProject };
198
+ export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitInterchangeV1, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, descriptorToInterchange, extractAssessments, mapLessonkitIds, packageLessonkitCourse, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateLessonkitProject, validateProjectPaths, writeLxpackProject };
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ function isSafeRelativeSpaPath(spaPath) {
7
7
  if (!spaPath.length || spaPath.includes("\0")) return false;
8
8
  if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
9
9
  if (/^[a-zA-Z]:[/\\]/.test(spaPath)) return false;
10
- const segments = spaPath.split(/[/\\]/);
11
- if (segments.some((s) => s === "..")) return false;
10
+ const segments = spaPath.split(/[/\\]/).filter((s) => s.length > 0);
11
+ if (segments.some((s) => s === ".." || s === ".")) return false;
12
12
  return true;
13
13
  }
14
14
  function assertResolvedPathUnderRoot(root, target) {
@@ -20,6 +20,21 @@ function assertResolvedPathUnderRoot(root, target) {
20
20
  }
21
21
  }
22
22
 
23
+ // src/theme.ts
24
+ import { getPresetTheme, themeToCssVariables } from "@lessonkit/themes";
25
+ function themeToLxpackRuntime(input) {
26
+ const theme = input.theme ?? getPresetTheme(input.preset ?? "default");
27
+ const raw = themeToCssVariables(theme);
28
+ const cssVariables = {};
29
+ for (const [key, value] of Object.entries(raw)) {
30
+ cssVariables[key] = String(value);
31
+ }
32
+ return {
33
+ theme: theme.name,
34
+ cssVariables
35
+ };
36
+ }
37
+
23
38
  // src/validateDescriptor.ts
24
39
  var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
25
40
  var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
@@ -81,6 +96,25 @@ function validateDescriptor(input) {
81
96
  message: `unknown preset; use one of: ${VALID_THEME_PRESETS.join(", ")}`
82
97
  });
83
98
  }
99
+ if (input.theme?.theme) {
100
+ try {
101
+ themeToLxpackRuntime({ preset: themePreset, theme: input.theme.theme });
102
+ } catch (err) {
103
+ issues.push({
104
+ path: "theme.theme",
105
+ message: err instanceof Error ? err.message : "invalid custom theme"
106
+ });
107
+ }
108
+ }
109
+ const completionThreshold = input.tracking?.completion?.threshold;
110
+ if (completionThreshold !== void 0) {
111
+ if (!Number.isFinite(completionThreshold) || completionThreshold < 0 || completionThreshold > 1) {
112
+ issues.push({
113
+ path: "tracking.completion.threshold",
114
+ message: "threshold must be a finite number between 0 and 1"
115
+ });
116
+ }
117
+ }
84
118
  if (layout === "single-spa" && (input.lessons?.length ?? 0) > 1) {
85
119
  issues.push({
86
120
  path: "lessons",
@@ -160,24 +194,69 @@ function validateDescriptor(input) {
160
194
  issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
161
195
  }
162
196
  const passingScore = assessment.passingScore;
163
- if (passingScore !== void 0) {
164
- if (!(passingScore > 0)) {
165
- issues.push({
166
- path: `${path}.passingScore`,
167
- message: "passingScore must be greater than 0"
168
- });
169
- } else if (trimmedChoices.length && passingScore > trimmedChoices.length) {
170
- issues.push({
171
- path: `${path}.passingScore`,
172
- message: "passingScore must not exceed the number of choices"
173
- });
174
- }
197
+ if (passingScore !== void 0 && !(passingScore > 0)) {
198
+ issues.push({
199
+ path: `${path}.passingScore`,
200
+ message: "passingScore must be greater than 0 (absolute point threshold)"
201
+ });
175
202
  }
176
203
  }
177
204
  if (issues.length) return { ok: false, issues };
178
205
  return { ok: true, descriptor: normalizeDescriptor(input) };
179
206
  }
180
207
 
208
+ // src/validateProjectPaths.ts
209
+ import { isAbsolute, resolve as resolve2 } from "path";
210
+ function validatePathField(value, fieldPath, projectRoot, issues) {
211
+ if (!isSafeRelativeSpaPath(value)) {
212
+ issues.push({
213
+ path: fieldPath,
214
+ message: "path must be relative without '..' segments or absolute prefixes"
215
+ });
216
+ return;
217
+ }
218
+ try {
219
+ assertResolvedPathUnderRoot(projectRoot, resolve2(projectRoot, value));
220
+ } catch {
221
+ issues.push({
222
+ path: fieldPath,
223
+ message: "path must resolve inside the project root"
224
+ });
225
+ }
226
+ }
227
+ function validateProjectPaths(projectRoot, paths) {
228
+ const issues = [];
229
+ const root = resolve2(projectRoot);
230
+ if (paths.spaDistDir?.trim()) {
231
+ validatePathField(paths.spaDistDir.trim(), "paths.spaDistDir", root, issues);
232
+ }
233
+ if (paths.lxpackOutDir?.trim()) {
234
+ validatePathField(paths.lxpackOutDir.trim(), "paths.lxpackOutDir", root, issues);
235
+ }
236
+ if (paths.outputBaseDir?.trim()) {
237
+ validatePathField(paths.outputBaseDir.trim(), "paths.outputBaseDir", root, issues);
238
+ }
239
+ return issues;
240
+ }
241
+ function resolveSafePackageOutputOverride(projectRoot, override) {
242
+ const root = resolve2(projectRoot);
243
+ const trimmed = override.trim();
244
+ if (!trimmed) {
245
+ throw new Error("output override must be a non-empty path");
246
+ }
247
+ if (isAbsolute(trimmed)) {
248
+ const resolved2 = resolve2(trimmed);
249
+ assertResolvedPathUnderRoot(root, resolved2);
250
+ return resolved2;
251
+ }
252
+ if (!isSafeRelativeSpaPath(trimmed)) {
253
+ throw new Error(`unsafe output path: ${override}`);
254
+ }
255
+ const resolved = resolve2(root, trimmed);
256
+ assertResolvedPathUnderRoot(root, resolved);
257
+ return resolved;
258
+ }
259
+
181
260
  // src/mapIds.ts
182
261
  import { assertValidId } from "@lessonkit/core";
183
262
  function mapLessonkitIds(descriptor) {
@@ -189,21 +268,6 @@ function mapLessonkitIds(descriptor) {
189
268
  return { courseId, lessonIds, checkIds };
190
269
  }
191
270
 
192
- // src/theme.ts
193
- import { getPresetTheme, themeToCssVariables } from "@lessonkit/themes";
194
- function themeToLxpackRuntime(input) {
195
- const theme = input.theme ?? getPresetTheme(input.preset ?? "default");
196
- const raw = themeToCssVariables(theme);
197
- const cssVariables = {};
198
- for (const [key, value] of Object.entries(raw)) {
199
- cssVariables[key] = String(value);
200
- }
201
- return {
202
- theme: theme.name,
203
- cssVariables
204
- };
205
- }
206
-
207
271
  // src/interchange.ts
208
272
  function resolveSpaLessons(descriptor) {
209
273
  const mapped = mapLessonkitIds(descriptor);
@@ -277,7 +341,7 @@ function extractAssessments(descriptor) {
277
341
 
278
342
  // src/writeProject.ts
279
343
  import { access, cp, mkdir, rm, writeFile } from "fs/promises";
280
- import { dirname, join, resolve as resolve2 } from "path";
344
+ import { dirname, join, resolve as resolve3 } from "path";
281
345
 
282
346
  // src/assessmentYaml.ts
283
347
  function yamlQuote(value) {
@@ -363,7 +427,7 @@ async function writeLxpackProject(options) {
363
427
  );
364
428
  }
365
429
  const descriptor = validation.descriptor;
366
- const outDir = resolve2(options.outDir);
430
+ const outDir = resolve3(options.outDir);
367
431
  await mkdir(outDir, { recursive: true });
368
432
  const spaLessons = resolveSpaLessons(descriptor);
369
433
  const runtime = descriptor.theme ? themeToLxpackRuntime(descriptor.theme) : void 0;
@@ -372,7 +436,11 @@ async function writeLxpackProject(options) {
372
436
  file: `assessments/${a.checkId}.yaml`
373
437
  }));
374
438
  if (descriptor.layout === "single-spa") {
375
- const srcDist = resolve2(options.spaDistDir ?? descriptor.spaDistDir ?? "dist");
439
+ const spaDistRelative = options.spaDistDir ?? descriptor.spaDistDir ?? "dist";
440
+ const srcDist = options.projectRoot ? resolve3(options.projectRoot, spaDistRelative) : resolve3(spaDistRelative);
441
+ if (options.projectRoot) {
442
+ assertResolvedPathUnderRoot(resolve3(options.projectRoot), srcDist);
443
+ }
376
444
  try {
377
445
  await access(srcDist);
378
446
  } catch {
@@ -391,7 +459,7 @@ async function writeLxpackProject(options) {
391
459
  const dest = join(outDir, lesson.spaPath);
392
460
  assertResolvedPathUnderRoot(outDir, dest);
393
461
  await rm(dest, { recursive: true, force: true });
394
- await copyDir(resolve2(src), dest);
462
+ await copyDir(resolve3(src), dest);
395
463
  }
396
464
  }
397
465
  if (assessments.length) {
@@ -434,8 +502,8 @@ async function writeLxpackProject(options) {
434
502
  }
435
503
 
436
504
  // src/packageCourse.ts
437
- import { mkdir as mkdir2, mkdtemp, rename, rm as rm2 } from "fs/promises";
438
- import { dirname as dirname2, join as join2, resolve as resolve3 } from "path";
505
+ import * as fsp from "fs/promises";
506
+ import { dirname as dirname2, join as join2, resolve as resolve4 } from "path";
439
507
  import { tmpdir } from "os";
440
508
  import {
441
509
  buildCourse,
@@ -443,13 +511,13 @@ import {
443
511
  } from "@lxpack/api";
444
512
  async function validateLessonkitProject(options) {
445
513
  return validateCourse({
446
- courseDir: resolve3(options.courseDir),
514
+ courseDir: resolve4(options.courseDir),
447
515
  target: options.target
448
516
  });
449
517
  }
450
518
  async function buildLessonkitProject(options) {
451
519
  return buildCourse({
452
- courseDir: resolve3(options.courseDir),
520
+ courseDir: resolve4(options.courseDir),
453
521
  target: options.target,
454
522
  output: options.output,
455
523
  dir: options.dir,
@@ -457,9 +525,38 @@ async function buildLessonkitProject(options) {
457
525
  assessments: options.assessments
458
526
  });
459
527
  }
528
+ async function pathExists(path) {
529
+ try {
530
+ await fsp.access(path);
531
+ return true;
532
+ } catch {
533
+ return false;
534
+ }
535
+ }
536
+ async function promoteStagingToOutDir(stagingDir, outDir) {
537
+ const tmpPromote = `${outDir}.tmp-promote`;
538
+ const backup = `${outDir}.bak`;
539
+ await fsp.rename(stagingDir, tmpPromote);
540
+ const hadOutDir = await pathExists(outDir);
541
+ if (hadOutDir) {
542
+ await fsp.rename(outDir, backup);
543
+ }
544
+ try {
545
+ await fsp.rename(tmpPromote, outDir);
546
+ } catch (promoteError) {
547
+ if (hadOutDir) {
548
+ await fsp.rename(backup, outDir).catch(() => void 0);
549
+ }
550
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
551
+ throw promoteError;
552
+ }
553
+ if (hadOutDir) {
554
+ await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
555
+ }
556
+ }
460
557
  async function packageLessonkitCourse(options) {
461
558
  const { target, output, dir, outputBaseDir, ...writeOpts } = options;
462
- const outDir = resolve3(writeOpts.outDir);
559
+ const outDir = resolve4(writeOpts.outDir);
463
560
  const descriptorValidation = validateDescriptor(writeOpts.descriptor);
464
561
  if (!descriptorValidation.ok) {
465
562
  return {
@@ -473,7 +570,7 @@ async function packageLessonkitCourse(options) {
473
570
  };
474
571
  }
475
572
  const descriptor = descriptorValidation.descriptor;
476
- const stagingDir = await mkdtemp(join2(tmpdir(), "lessonkit-lxpack-"));
573
+ const stagingDir = await fsp.mkdtemp(join2(tmpdir(), "lessonkit-lxpack-"));
477
574
  let promoted = false;
478
575
  try {
479
576
  const written = await writeLxpackProject({ ...writeOpts, descriptor, outDir: stagingDir });
@@ -494,7 +591,7 @@ async function packageLessonkitCourse(options) {
494
591
  };
495
592
  }
496
593
  const outputBase = outputBaseDir ?? ".lxpack/out";
497
- await mkdir2(join2(courseDir, outputBase), { recursive: true });
594
+ await fsp.mkdir(join2(courseDir, outputBase), { recursive: true });
498
595
  const defaultOutput = output ?? (dir ? join2(outputBase, target) : join2(outputBase, `course-${target}.zip`));
499
596
  const build = await buildLessonkitProject({
500
597
  courseDir,
@@ -517,32 +614,42 @@ async function packageLessonkitCourse(options) {
517
614
  }))
518
615
  };
519
616
  }
520
- await rm2(outDir, { recursive: true, force: true });
521
- await mkdir2(dirname2(outDir), { recursive: true });
522
- await rename(stagingDir, outDir);
617
+ await fsp.mkdir(dirname2(outDir), { recursive: true });
618
+ await promoteStagingToOutDir(stagingDir, outDir);
523
619
  promoted = true;
524
620
  const remapArtifactPath = (artifactPath) => {
525
621
  if (!artifactPath) return void 0;
526
- const resolved = resolve3(artifactPath);
527
- const stagingResolved = resolve3(stagingDir);
622
+ const resolved = resolve4(artifactPath);
623
+ const stagingResolved = resolve4(stagingDir);
528
624
  if (resolved === stagingResolved || resolved.startsWith(stagingResolved + "/")) {
529
625
  return join2(outDir, resolved.slice(stagingResolved.length + 1));
530
626
  }
531
627
  return artifactPath;
532
628
  };
629
+ const remappedOutputPath = remapArtifactPath(
630
+ "outputPath" in build ? build.outputPath : void 0
631
+ );
632
+ const remappedOutputDir = remapArtifactPath("outputDir" in build ? build.outputDir : void 0);
633
+ const remappedBuild = { ...build };
634
+ if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
635
+ remappedBuild.outputPath = remappedOutputPath;
636
+ }
637
+ if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
638
+ remappedBuild.outputDir = remappedOutputDir;
639
+ }
533
640
  return {
534
641
  ok: true,
535
642
  courseDir: outDir,
536
643
  target,
537
- outputPath: remapArtifactPath("outputPath" in build ? build.outputPath : void 0),
538
- outputDir: remapArtifactPath("outputDir" in build ? build.outputDir : void 0),
644
+ outputPath: remappedOutputPath,
645
+ outputDir: remappedOutputDir,
539
646
  fileCount: build.fileCount,
540
647
  validation,
541
- build
648
+ build: remappedBuild
542
649
  };
543
650
  } finally {
544
651
  if (!promoted) {
545
- await rm2(stagingDir, { recursive: true, force: true }).catch(() => void 0);
652
+ await fsp.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
546
653
  }
547
654
  }
548
655
  }
@@ -553,9 +660,11 @@ export {
553
660
  extractAssessments,
554
661
  mapLessonkitIds,
555
662
  packageLessonkitCourse,
663
+ resolveSafePackageOutputOverride,
556
664
  resolveSpaLessons,
557
665
  themeToLxpackRuntime,
558
666
  validateDescriptor,
559
667
  validateLessonkitProject,
668
+ validateProjectPaths,
560
669
  writeLxpackProject
561
670
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/lxpack",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
5
  "description": "LXPack export adapter for LessonKit courses (SCORM, standalone, xAPI, cmi5).",
6
6
  "license": "Apache-2.0",
@@ -53,8 +53,8 @@
53
53
  "lint": "echo \"(no lint configured yet)\""
54
54
  },
55
55
  "dependencies": {
56
- "@lessonkit/core": "0.7.0",
57
- "@lessonkit/themes": "0.7.0",
56
+ "@lessonkit/core": "0.8.1",
57
+ "@lessonkit/themes": "0.8.1",
58
58
  "@lxpack/api": "^0.4.0"
59
59
  },
60
60
  "devDependencies": {