@lessonkit/lxpack 0.8.0 → 0.9.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
@@ -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,21 +17,38 @@ 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
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
33
+ LESSONKIT_TELEMETRY_EVENTS: () => import_tracking_schema2.LESSONKIT_TELEMETRY_EVENTS,
23
34
  assessmentDescriptorToLxpack: () => assessmentDescriptorToLxpack,
24
35
  buildLessonkitProject: () => buildLessonkitProject,
25
36
  descriptorToInterchange: () => descriptorToInterchange,
26
37
  extractAssessments: () => extractAssessments,
38
+ lessonkitInterchangeSchema: () => import_validators2.lessonkitInterchangeSchema,
27
39
  mapLessonkitIds: () => mapLessonkitIds,
40
+ mapLessonkitTelemetryToBridgeAction: () => import_tracking_schema2.mapLessonkitTelemetryToBridgeAction,
41
+ mapLessonkitTelemetryToLxpack: () => import_tracking_schema2.mapLessonkitTelemetryToLxpack,
42
+ materializeLessonkitProject: () => import_validators2.materializeLessonkitProject,
28
43
  packageLessonkitCourse: () => packageLessonkitCourse,
44
+ parseLessonkitInterchange: () => import_validators2.parseLessonkitInterchange,
45
+ resolveSafePackageOutputOverride: () => resolveSafePackageOutputOverride,
29
46
  resolveSpaLessons: () => resolveSpaLessons,
47
+ telemetryEventToLessonkit: () => telemetryEventToLessonkit,
30
48
  themeToLxpackRuntime: () => themeToLxpackRuntime,
31
49
  validateDescriptor: () => validateDescriptor,
32
50
  validateLessonkitProject: () => validateLessonkitProject,
51
+ validateProjectPaths: () => validateProjectPaths,
33
52
  writeLxpackProject: () => writeLxpackProject
34
53
  });
35
54
  module.exports = __toCommonJS(index_exports);
@@ -43,8 +62,8 @@ function isSafeRelativeSpaPath(spaPath) {
43
62
  if (!spaPath.length || spaPath.includes("\0")) return false;
44
63
  if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
45
64
  if (/^[a-zA-Z]:[/\\]/.test(spaPath)) return false;
46
- const segments = spaPath.split(/[/\\]/);
47
- if (segments.some((s) => s === "..")) return false;
65
+ const segments = spaPath.split(/[/\\]/).filter((s) => s.length > 0);
66
+ if (segments.some((s) => s === ".." || s === ".")) return false;
48
67
  return true;
49
68
  }
50
69
  function assertResolvedPathUnderRoot(root, target) {
@@ -56,6 +75,21 @@ function assertResolvedPathUnderRoot(root, target) {
56
75
  }
57
76
  }
58
77
 
78
+ // src/theme.ts
79
+ var import_themes = require("@lessonkit/themes");
80
+ function themeToLxpackRuntime(input) {
81
+ const theme = input.theme ?? (0, import_themes.getPresetTheme)(input.preset ?? "default");
82
+ const raw = (0, import_themes.themeToCssVariables)(theme);
83
+ const cssVariables = {};
84
+ for (const [key, value] of Object.entries(raw)) {
85
+ cssVariables[key] = String(value);
86
+ }
87
+ return {
88
+ theme: theme.name,
89
+ cssVariables
90
+ };
91
+ }
92
+
59
93
  // src/validateDescriptor.ts
60
94
  var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
61
95
  var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
@@ -117,6 +151,25 @@ function validateDescriptor(input) {
117
151
  message: `unknown preset; use one of: ${VALID_THEME_PRESETS.join(", ")}`
118
152
  });
119
153
  }
154
+ if (input.theme?.theme) {
155
+ try {
156
+ themeToLxpackRuntime({ preset: themePreset, theme: input.theme.theme });
157
+ } catch (err) {
158
+ issues.push({
159
+ path: "theme.theme",
160
+ message: err instanceof Error ? err.message : "invalid custom theme"
161
+ });
162
+ }
163
+ }
164
+ const completionThreshold = input.tracking?.completion?.threshold;
165
+ if (completionThreshold !== void 0) {
166
+ if (!Number.isFinite(completionThreshold) || completionThreshold < 0 || completionThreshold > 1) {
167
+ issues.push({
168
+ path: "tracking.completion.threshold",
169
+ message: "threshold must be a finite number between 0 and 1"
170
+ });
171
+ }
172
+ }
120
173
  if (layout === "single-spa" && (input.lessons?.length ?? 0) > 1) {
121
174
  issues.push({
122
175
  path: "lessons",
@@ -196,24 +249,69 @@ function validateDescriptor(input) {
196
249
  issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
197
250
  }
198
251
  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
- }
252
+ if (passingScore !== void 0 && !(passingScore > 0)) {
253
+ issues.push({
254
+ path: `${path}.passingScore`,
255
+ message: "passingScore must be greater than 0 (absolute point threshold)"
256
+ });
211
257
  }
212
258
  }
213
259
  if (issues.length) return { ok: false, issues };
214
260
  return { ok: true, descriptor: normalizeDescriptor(input) };
215
261
  }
216
262
 
263
+ // src/validateProjectPaths.ts
264
+ var import_node_path2 = require("path");
265
+ function validatePathField(value, fieldPath, projectRoot, issues) {
266
+ if (!isSafeRelativeSpaPath(value)) {
267
+ issues.push({
268
+ path: fieldPath,
269
+ message: "path must be relative without '..' segments or absolute prefixes"
270
+ });
271
+ return;
272
+ }
273
+ try {
274
+ assertResolvedPathUnderRoot(projectRoot, (0, import_node_path2.resolve)(projectRoot, value));
275
+ } catch {
276
+ issues.push({
277
+ path: fieldPath,
278
+ message: "path must resolve inside the project root"
279
+ });
280
+ }
281
+ }
282
+ function validateProjectPaths(projectRoot, paths) {
283
+ const issues = [];
284
+ const root = (0, import_node_path2.resolve)(projectRoot);
285
+ if (paths.spaDistDir?.trim()) {
286
+ validatePathField(paths.spaDistDir.trim(), "paths.spaDistDir", root, issues);
287
+ }
288
+ if (paths.lxpackOutDir?.trim()) {
289
+ validatePathField(paths.lxpackOutDir.trim(), "paths.lxpackOutDir", root, issues);
290
+ }
291
+ if (paths.outputBaseDir?.trim()) {
292
+ validatePathField(paths.outputBaseDir.trim(), "paths.outputBaseDir", root, issues);
293
+ }
294
+ return issues;
295
+ }
296
+ function resolveSafePackageOutputOverride(projectRoot, override) {
297
+ const root = (0, import_node_path2.resolve)(projectRoot);
298
+ const trimmed = override.trim();
299
+ if (!trimmed) {
300
+ throw new Error("output override must be a non-empty path");
301
+ }
302
+ if ((0, import_node_path2.isAbsolute)(trimmed)) {
303
+ const resolved2 = (0, import_node_path2.resolve)(trimmed);
304
+ assertResolvedPathUnderRoot(root, resolved2);
305
+ return resolved2;
306
+ }
307
+ if (!isSafeRelativeSpaPath(trimmed)) {
308
+ throw new Error(`unsafe output path: ${override}`);
309
+ }
310
+ const resolved = (0, import_node_path2.resolve)(root, trimmed);
311
+ assertResolvedPathUnderRoot(root, resolved);
312
+ return resolved;
313
+ }
314
+
217
315
  // src/mapIds.ts
218
316
  var import_core2 = require("@lessonkit/core");
219
317
  function mapLessonkitIds(descriptor) {
@@ -225,20 +323,36 @@ function mapLessonkitIds(descriptor) {
225
323
  return { courseId, lessonIds, checkIds };
226
324
  }
227
325
 
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
- }
326
+ // src/assessments.ts
327
+ function slugChoiceId(text, index) {
328
+ const base = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
329
+ const stem = base.length ? base : "choice";
330
+ return `${stem}-${index + 1}`;
331
+ }
332
+ function assessmentDescriptorToLxpack(assessment) {
333
+ const choices = assessment.choices.map((text, index) => {
334
+ const id = slugChoiceId(text, index);
335
+ return {
336
+ id,
337
+ text,
338
+ correct: text === assessment.answer
339
+ };
340
+ });
237
341
  return {
238
- theme: theme.name,
239
- cssVariables
342
+ id: assessment.checkId,
343
+ passingScore: assessment.passingScore ?? 1,
344
+ questions: [
345
+ {
346
+ id: "q1",
347
+ prompt: assessment.question,
348
+ choices
349
+ }
350
+ ]
240
351
  };
241
352
  }
353
+ function extractAssessments(descriptor) {
354
+ return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack);
355
+ }
242
356
 
243
357
  // src/interchange.ts
244
358
  function resolveSpaLessons(descriptor) {
@@ -263,6 +377,8 @@ function resolveSpaLessons(descriptor) {
263
377
  function descriptorToInterchange(descriptor) {
264
378
  const mapped = mapLessonkitIds(descriptor);
265
379
  const spaLessons = resolveSpaLessons(descriptor);
380
+ const runtime = descriptor.theme ? themeToLxpackRuntime(descriptor.theme) : void 0;
381
+ const assessments = extractAssessments(descriptor);
266
382
  return {
267
383
  format: "lessonkit",
268
384
  version: "1",
@@ -276,121 +392,52 @@ function descriptorToInterchange(descriptor) {
276
392
  type: "spa",
277
393
  path: l.path
278
394
  })),
279
- tracking: descriptor.tracking
280
- };
281
- }
282
-
283
- // src/assessments.ts
284
- function slugChoiceId(text, index) {
285
- const base = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
286
- const stem = base.length ? base : "choice";
287
- return `${stem}-${index + 1}`;
288
- }
289
- function assessmentDescriptorToLxpack(assessment) {
290
- const choices = assessment.choices.map((text, index) => {
291
- const id = slugChoiceId(text, index);
292
- return {
293
- id,
294
- text,
295
- correct: text === assessment.answer
296
- };
297
- });
298
- return {
299
- id: assessment.checkId,
300
- passingScore: assessment.passingScore ?? 1,
301
- questions: [
302
- {
303
- id: "q1",
304
- prompt: assessment.question,
305
- choices
306
- }
307
- ]
395
+ tracking: descriptor.tracking,
396
+ runtime: runtime ? {
397
+ theme: runtime.theme,
398
+ cssVariables: runtime.cssVariables
399
+ } : void 0,
400
+ assessments: assessments.length ? assessments : void 0
308
401
  };
309
402
  }
310
- function extractAssessments(descriptor) {
311
- return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack);
312
- }
313
403
 
314
404
  // src/writeProject.ts
315
- var import_promises = require("fs/promises");
316
- var import_node_path2 = require("path");
405
+ var import_node_path4 = require("path");
406
+ var import_validators = require("@lxpack/validators");
317
407
 
318
- // src/assessmentYaml.ts
319
- function yamlQuote(value) {
320
- return JSON.stringify(value);
321
- }
322
- function emitAssessmentYaml(assessment) {
323
- const lx = assessmentDescriptorToLxpack(assessment);
324
- const lines = [];
325
- lines.push(`id: ${lx.id}`);
326
- lines.push(`passingScore: ${lx.passingScore}`);
327
- lines.push("questions:");
328
- for (const question of lx.questions) {
329
- lines.push(` - id: ${question.id}`);
330
- lines.push(` prompt: ${yamlQuote(question.prompt)}`);
331
- lines.push(" choices:");
332
- for (const choice of question.choices) {
333
- lines.push(` - id: ${choice.id}`);
334
- lines.push(` text: ${yamlQuote(choice.text)}`);
335
- if (choice.correct) lines.push(" correct: true");
408
+ // src/spaDirs.ts
409
+ var import_promises = require("fs/promises");
410
+ var import_node_path3 = require("path");
411
+ async function resolveSpaDirs(options) {
412
+ const { descriptor, spaDistDir, lessonSpaDirs, projectRoot } = options;
413
+ const spaLessons = resolveSpaLessons(descriptor);
414
+ if (descriptor.layout === "single-spa") {
415
+ const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? "dist";
416
+ const srcDist = projectRoot ? (0, import_node_path3.resolve)(projectRoot, spaDistRelative) : (0, import_node_path3.resolve)(spaDistRelative);
417
+ if (projectRoot) {
418
+ assertResolvedPathUnderRoot((0, import_node_path3.resolve)(projectRoot), srcDist);
336
419
  }
337
- }
338
- return `${lines.join("\n")}
339
- `;
340
- }
341
-
342
- // src/yaml.ts
343
- function yamlQuote2(value) {
344
- if (/[:#\n\r]/.test(value) || value.startsWith(" ") || value.endsWith(" ")) {
345
- return JSON.stringify(value);
346
- }
347
- return value;
348
- }
349
- function emitCourseYaml(opts) {
350
- const lines = [];
351
- lines.push(`title: ${yamlQuote2(opts.title)}`);
352
- lines.push(`version: ${yamlQuote2(opts.version)}`);
353
- if (opts.description) lines.push(`description: ${yamlQuote2(opts.description)}`);
354
- if (opts.runtime) {
355
- lines.push("runtime:");
356
- lines.push(` theme: ${yamlQuote2(opts.runtime.theme)}`);
357
- if (opts.runtime.cssVariables && Object.keys(opts.runtime.cssVariables).length) {
358
- lines.push(" cssVariables:");
359
- for (const [key, value] of Object.entries(opts.runtime.cssVariables).sort(
360
- ([a], [b]) => a.localeCompare(b)
361
- )) {
362
- lines.push(` ${key}: ${JSON.stringify(String(value))}`);
363
- }
420
+ try {
421
+ await (0, import_promises.access)(srcDist);
422
+ } catch {
423
+ throw new Error(`spaDistDir not found: ${srcDist}`);
364
424
  }
425
+ const lessonId = spaLessons[0]?.id ?? "main";
426
+ return { [lessonId]: srcDist };
365
427
  }
366
- if (opts.tracking?.completion?.threshold !== void 0) {
367
- lines.push("tracking:");
368
- lines.push(" completion:");
369
- lines.push(` threshold: ${opts.tracking.completion.threshold}`);
370
- }
371
- lines.push("lessons:");
372
- for (const lesson of opts.lessons) {
373
- lines.push(` - id: ${lesson.id}`);
374
- lines.push(` title: ${yamlQuote2(lesson.title)}`);
375
- lines.push(` type: spa`);
376
- lines.push(` path: ${lesson.path}`);
377
- }
378
- if (opts.assessments.length) {
379
- lines.push("assessments:");
380
- for (const assessment of opts.assessments) {
381
- lines.push(` - id: ${assessment.id}`);
382
- lines.push(` file: ${assessment.file}`);
428
+ const dirs = {};
429
+ const lessonDirs = lessonSpaDirs ?? {};
430
+ for (const lesson of descriptor.lessons) {
431
+ const src = lessonDirs[lesson.id];
432
+ if (!src) {
433
+ throw new Error(`lessonSpaDirs missing build output for lesson "${lesson.id}"`);
383
434
  }
435
+ dirs[lesson.id] = (0, import_node_path3.resolve)(src);
384
436
  }
385
- return `${lines.join("\n")}
386
- `;
437
+ return dirs;
387
438
  }
388
439
 
389
440
  // src/writeProject.ts
390
- async function copyDir(src, dest) {
391
- await (0, import_promises.mkdir)((0, import_node_path2.dirname)(dest), { recursive: true });
392
- await (0, import_promises.cp)(src, dest, { recursive: true });
393
- }
394
441
  async function writeLxpackProject(options) {
395
442
  const validation = validateDescriptor(options.descriptor);
396
443
  if (!validation.ok) {
@@ -399,90 +446,42 @@ async function writeLxpackProject(options) {
399
446
  );
400
447
  }
401
448
  const descriptor = validation.descriptor;
402
- const outDir = (0, import_node_path2.resolve)(options.outDir);
403
- await (0, import_promises.mkdir)(outDir, { recursive: true });
404
- const spaLessons = resolveSpaLessons(descriptor);
405
- const runtime = descriptor.theme ? themeToLxpackRuntime(descriptor.theme) : void 0;
406
- const assessments = (descriptor.assessments ?? []).map((a) => ({
407
- id: a.checkId,
408
- file: `assessments/${a.checkId}.yaml`
409
- }));
410
- if (descriptor.layout === "single-spa") {
411
- const srcDist = (0, import_node_path2.resolve)(options.spaDistDir ?? descriptor.spaDistDir ?? "dist");
412
- try {
413
- await (0, import_promises.access)(srcDist);
414
- } catch {
415
- throw new Error(`spaDistDir not found: ${srcDist}`);
416
- }
417
- const destDist = (0, import_node_path2.join)(outDir, "dist");
418
- await (0, import_promises.rm)(destDist, { recursive: true, force: true });
419
- await copyDir(srcDist, destDist);
420
- } else {
421
- const lessonDirs = options.lessonSpaDirs ?? {};
422
- for (const lesson of descriptor.lessons) {
423
- const src = lessonDirs[lesson.id];
424
- if (!src) {
425
- throw new Error(`lessonSpaDirs missing build output for lesson "${lesson.id}"`);
426
- }
427
- const dest = (0, import_node_path2.join)(outDir, lesson.spaPath);
428
- assertResolvedPathUnderRoot(outDir, dest);
429
- await (0, import_promises.rm)(dest, { recursive: true, force: true });
430
- await copyDir((0, import_node_path2.resolve)(src), dest);
431
- }
432
- }
433
- if (assessments.length) {
434
- const assessmentsDir = (0, import_node_path2.join)(outDir, "assessments");
435
- await (0, import_promises.mkdir)(assessmentsDir, { recursive: true });
436
- for (const assessment of descriptor.assessments ?? []) {
437
- await (0, import_promises.writeFile)(
438
- (0, import_node_path2.join)(outDir, `assessments/${assessment.checkId}.yaml`),
439
- emitAssessmentYaml(assessment),
440
- "utf-8"
441
- );
442
- }
449
+ const outDir = (0, import_node_path4.resolve)(options.outDir);
450
+ const spaDirs = await resolveSpaDirs({ ...options, descriptor });
451
+ const interchange = descriptorToInterchange(descriptor);
452
+ const materialized = await (0, import_validators.materializeLessonkitProject)({
453
+ interchange,
454
+ spaDirs,
455
+ courseDir: outDir,
456
+ writeAuthoringFiles: true
457
+ });
458
+ if (!materialized.ok) {
459
+ throw new Error(
460
+ materialized.issues.map((i) => `${i.path ?? ""}: ${i.message}`.trim()).join("; ")
461
+ );
443
462
  }
444
- const courseYamlPath = (0, import_node_path2.join)(outDir, "course.yaml");
445
- await (0, import_promises.writeFile)(
446
- courseYamlPath,
447
- emitCourseYaml({
448
- title: descriptor.title,
449
- version: descriptor.version ?? "1.0.0",
450
- runtime,
451
- tracking: descriptor.tracking,
452
- lessons: spaLessons.map((l) => ({
453
- id: l.id,
454
- title: l.title,
455
- type: "spa",
456
- path: l.path
457
- })),
458
- assessments
459
- }),
460
- "utf-8"
461
- );
462
- const lessonkitJsonPath = (0, import_node_path2.join)(outDir, "lessonkit.json");
463
- await (0, import_promises.writeFile)(
464
- lessonkitJsonPath,
465
- `${JSON.stringify(descriptorToInterchange(descriptor), null, 2)}
466
- `,
467
- "utf-8"
468
- );
469
- return { outDir, courseYamlPath, lessonkitJsonPath };
463
+ const courseDir = materialized.courseDir;
464
+ return {
465
+ outDir: courseDir,
466
+ courseYamlPath: (0, import_node_path4.join)(courseDir, "course.yaml"),
467
+ lessonkitJsonPath: (0, import_node_path4.join)(courseDir, "lessonkit.json")
468
+ };
470
469
  }
471
470
 
472
471
  // src/packageCourse.ts
473
- var import_promises2 = require("fs/promises");
474
- var import_node_path3 = require("path");
472
+ var fsp = __toESM(require("fs/promises"), 1);
473
+ var import_node_path5 = require("path");
475
474
  var import_node_os = require("os");
476
475
  var import_api = require("@lxpack/api");
477
476
  async function validateLessonkitProject(options) {
478
477
  return (0, import_api.validateCourse)({
479
- courseDir: (0, import_node_path3.resolve)(options.courseDir),
478
+ courseDir: (0, import_node_path5.resolve)(options.courseDir),
480
479
  target: options.target
481
480
  });
482
481
  }
483
482
  async function buildLessonkitProject(options) {
484
483
  return (0, import_api.buildCourse)({
485
- courseDir: (0, import_node_path3.resolve)(options.courseDir),
484
+ courseDir: (0, import_node_path5.resolve)(options.courseDir),
486
485
  target: options.target,
487
486
  output: options.output,
488
487
  dir: options.dir,
@@ -490,9 +489,38 @@ async function buildLessonkitProject(options) {
490
489
  assessments: options.assessments
491
490
  });
492
491
  }
492
+ async function pathExists(path) {
493
+ try {
494
+ await fsp.access(path);
495
+ return true;
496
+ } catch {
497
+ return false;
498
+ }
499
+ }
500
+ async function promoteStagingToOutDir(stagingDir, outDir) {
501
+ const tmpPromote = `${outDir}.tmp-promote`;
502
+ const backup = `${outDir}.bak`;
503
+ await fsp.rename(stagingDir, tmpPromote);
504
+ const hadOutDir = await pathExists(outDir);
505
+ if (hadOutDir) {
506
+ await fsp.rename(outDir, backup);
507
+ }
508
+ try {
509
+ await fsp.rename(tmpPromote, outDir);
510
+ } catch (promoteError) {
511
+ if (hadOutDir) {
512
+ await fsp.rename(backup, outDir).catch(() => void 0);
513
+ }
514
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
515
+ throw promoteError;
516
+ }
517
+ if (hadOutDir) {
518
+ await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
519
+ }
520
+ }
493
521
  async function packageLessonkitCourse(options) {
494
522
  const { target, output, dir, outputBaseDir, ...writeOpts } = options;
495
- const outDir = (0, import_node_path3.resolve)(writeOpts.outDir);
523
+ const outDir = (0, import_node_path5.resolve)(writeOpts.outDir);
496
524
  const descriptorValidation = validateDescriptor(writeOpts.descriptor);
497
525
  if (!descriptorValidation.ok) {
498
526
  return {
@@ -506,42 +534,50 @@ async function packageLessonkitCourse(options) {
506
534
  };
507
535
  }
508
536
  const descriptor = descriptorValidation.descriptor;
509
- const stagingDir = await (0, import_promises2.mkdtemp)((0, import_node_path3.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
537
+ const stagingDir = await fsp.mkdtemp((0, import_node_path5.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
510
538
  let promoted = false;
511
539
  try {
512
- const written = await writeLxpackProject({ ...writeOpts, descriptor, outDir: stagingDir });
513
- const courseDir = written.outDir;
514
- const assessments = extractAssessments(descriptor);
515
- const validation = await validateLessonkitProject({ courseDir, target });
516
- if (!validation.ok) {
540
+ let spaDirs;
541
+ try {
542
+ spaDirs = await resolveSpaDirs({ ...writeOpts, descriptor });
543
+ } catch (err) {
517
544
  return {
518
545
  ok: false,
519
546
  courseDir: outDir,
520
547
  target,
521
- validation,
522
- issues: validation.issues.map((i) => ({
523
- path: i.path,
524
- message: i.message,
525
- severity: i.severity
526
- }))
548
+ issues: [
549
+ {
550
+ path: "spaDirs",
551
+ message: err instanceof Error ? err.message : String(err)
552
+ }
553
+ ]
527
554
  };
528
555
  }
556
+ const interchange = descriptorToInterchange(descriptor);
529
557
  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`));
532
- const build = await buildLessonkitProject({
533
- courseDir,
558
+ await fsp.mkdir((0, import_node_path5.join)(stagingDir, outputBase), { recursive: true });
559
+ const defaultOutput = output ?? (dir ? (0, import_node_path5.join)(outputBase, target) : (0, import_node_path5.join)(outputBase, `course-${target}.zip`));
560
+ const build = await (0, import_api.packageLessonkit)({
561
+ interchange,
562
+ spaDirs,
534
563
  target,
535
- output: defaultOutput.startsWith("/") ? defaultOutput : (0, import_node_path3.join)(courseDir, defaultOutput),
564
+ courseDir: stagingDir,
565
+ output: defaultOutput,
536
566
  dir,
537
- assessments: assessments.length ? assessments : void 0
567
+ outputBaseDir,
568
+ outputAnchorDir: stagingDir,
569
+ writeAuthoringFiles: true
538
570
  });
539
571
  if (!build.ok) {
572
+ const validation2 = {
573
+ ok: false,
574
+ issues: build.issues
575
+ };
540
576
  return {
541
577
  ok: false,
542
578
  courseDir: outDir,
543
579
  target,
544
- validation,
580
+ validation: validation2,
545
581
  build,
546
582
  issues: build.issues.map((i) => ({
547
583
  path: i.path,
@@ -550,46 +586,105 @@ async function packageLessonkitCourse(options) {
550
586
  }))
551
587
  };
552
588
  }
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);
556
- promoted = true;
589
+ const validation = {
590
+ ok: true,
591
+ manifest: build.manifest,
592
+ issues: build.issues
593
+ };
594
+ const stagingRoot = await fsp.realpath(stagingDir);
557
595
  const remapArtifactPath = (artifactPath) => {
558
596
  if (!artifactPath) return void 0;
559
- const resolved = (0, import_node_path3.resolve)(artifactPath);
560
- const stagingResolved = (0, import_node_path3.resolve)(stagingDir);
561
- if (resolved === stagingResolved || resolved.startsWith(stagingResolved + "/")) {
562
- return (0, import_node_path3.join)(outDir, resolved.slice(stagingResolved.length + 1));
597
+ const resolved = (0, import_node_path5.resolve)(artifactPath);
598
+ if (resolved === stagingRoot || resolved.startsWith(`${stagingRoot}/`)) {
599
+ return (0, import_node_path5.join)(outDir, resolved.slice(stagingRoot.length + 1));
563
600
  }
564
601
  return artifactPath;
565
602
  };
603
+ const remappedOutputPath = remapArtifactPath(
604
+ "outputPath" in build ? build.outputPath : void 0
605
+ );
606
+ const remappedOutputDir = remapArtifactPath("outputDir" in build ? build.outputDir : void 0);
607
+ await fsp.mkdir((0, import_node_path5.dirname)(outDir), { recursive: true });
608
+ await promoteStagingToOutDir(stagingDir, outDir);
609
+ promoted = true;
610
+ const remappedBuild = { ...build };
611
+ if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
612
+ remappedBuild.outputPath = remappedOutputPath;
613
+ }
614
+ if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
615
+ remappedBuild.outputDir = remappedOutputDir;
616
+ }
566
617
  return {
567
618
  ok: true,
568
619
  courseDir: outDir,
569
620
  target,
570
- outputPath: remapArtifactPath("outputPath" in build ? build.outputPath : void 0),
571
- outputDir: remapArtifactPath("outputDir" in build ? build.outputDir : void 0),
621
+ outputPath: remappedOutputPath,
622
+ outputDir: remappedOutputDir,
572
623
  fileCount: build.fileCount,
573
624
  validation,
574
- build
625
+ build: remappedBuild
575
626
  };
576
627
  } finally {
577
628
  if (!promoted) {
578
- await (0, import_promises2.rm)(stagingDir, { recursive: true, force: true }).catch(() => void 0);
629
+ await fsp.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
630
+ }
631
+ }
632
+ }
633
+
634
+ // src/index.ts
635
+ var import_tracking_schema2 = require("@lxpack/tracking-schema");
636
+
637
+ // src/telemetry.ts
638
+ var import_tracking_schema = require("@lxpack/tracking-schema");
639
+ var SUPPORTED = new Set(import_tracking_schema.LESSONKIT_TELEMETRY_EVENTS);
640
+ function telemetryEventToLessonkit(event) {
641
+ if (!SUPPORTED.has(event.name)) {
642
+ return null;
643
+ }
644
+ const name = event.name;
645
+ const mapped = {
646
+ name,
647
+ lessonId: event.lessonId
648
+ };
649
+ if (name === "quiz_completed" || name === "quiz_answered") {
650
+ const data = event.data;
651
+ mapped.assessmentId = data?.checkId;
652
+ if (data && "score" in data) {
653
+ mapped.score = data.score;
654
+ mapped.maxScore = data.maxScore;
655
+ mapped.passingScore = data.passingScore;
579
656
  }
657
+ if (data) {
658
+ mapped.data = data;
659
+ }
660
+ } else if (name === "interaction" && event.data) {
661
+ mapped.data = event.data;
580
662
  }
663
+ return mapped;
581
664
  }
665
+
666
+ // src/index.ts
667
+ var import_validators2 = require("@lxpack/validators");
582
668
  // Annotate the CommonJS export names for ESM import in node:
583
669
  0 && (module.exports = {
670
+ LESSONKIT_TELEMETRY_EVENTS,
584
671
  assessmentDescriptorToLxpack,
585
672
  buildLessonkitProject,
586
673
  descriptorToInterchange,
587
674
  extractAssessments,
675
+ lessonkitInterchangeSchema,
588
676
  mapLessonkitIds,
677
+ mapLessonkitTelemetryToBridgeAction,
678
+ mapLessonkitTelemetryToLxpack,
679
+ materializeLessonkitProject,
589
680
  packageLessonkitCourse,
681
+ parseLessonkitInterchange,
682
+ resolveSafePackageOutputOverride,
590
683
  resolveSpaLessons,
684
+ telemetryEventToLessonkit,
591
685
  themeToLxpackRuntime,
592
686
  validateDescriptor,
593
687
  validateLessonkitProject,
688
+ validateProjectPaths,
594
689
  writeLxpackProject
595
690
  });