@lessonkit/lxpack 0.9.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -15
- package/dist/bridge.cjs +64 -0
- package/dist/bridge.d.cts +8 -2
- package/dist/bridge.d.ts +8 -2
- package/dist/bridge.js +64 -2
- package/dist/index.cjs +476 -120
- package/dist/index.d.cts +82 -2
- package/dist/index.d.ts +82 -2
- package/dist/index.js +474 -126
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -6,7 +6,14 @@ import {
|
|
|
6
6
|
import { validateId } from "@lessonkit/core";
|
|
7
7
|
|
|
8
8
|
// src/spaPath.ts
|
|
9
|
-
import {
|
|
9
|
+
import { realpathSync } from "fs";
|
|
10
|
+
import { relative, resolve, sep, win32 } from "path";
|
|
11
|
+
function resolveComparablePath(p) {
|
|
12
|
+
if (/^[a-zA-Z]:[/\\]/.test(p)) {
|
|
13
|
+
return win32.resolve(p);
|
|
14
|
+
}
|
|
15
|
+
return resolve(p);
|
|
16
|
+
}
|
|
10
17
|
function isSafeRelativeSpaPath(spaPath) {
|
|
11
18
|
if (!spaPath.length || spaPath.includes("\0")) return false;
|
|
12
19
|
if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
|
|
@@ -16,13 +23,43 @@ function isSafeRelativeSpaPath(spaPath) {
|
|
|
16
23
|
return true;
|
|
17
24
|
}
|
|
18
25
|
function assertResolvedPathUnderRoot(root, target) {
|
|
19
|
-
const rootResolved =
|
|
20
|
-
const targetResolved =
|
|
26
|
+
const rootResolved = resolveComparablePath(root);
|
|
27
|
+
const targetResolved = resolveComparablePath(target);
|
|
21
28
|
const prefix = rootResolved.endsWith(sep) ? rootResolved : rootResolved + sep;
|
|
22
|
-
|
|
29
|
+
const win32Prefix = rootResolved.endsWith(win32.sep) ? rootResolved : rootResolved + win32.sep;
|
|
30
|
+
if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix) && !targetResolved.startsWith(win32Prefix)) {
|
|
23
31
|
throw new Error(`unsafe path escapes project root: ${target}`);
|
|
24
32
|
}
|
|
25
33
|
}
|
|
34
|
+
function assertRealPathUnderRoot(root, target) {
|
|
35
|
+
const rootResolved = resolveComparablePath(root);
|
|
36
|
+
const targetResolved = resolveComparablePath(target);
|
|
37
|
+
let rootReal;
|
|
38
|
+
try {
|
|
39
|
+
rootReal = realpathSync(rootResolved);
|
|
40
|
+
} catch {
|
|
41
|
+
rootReal = rootResolved;
|
|
42
|
+
}
|
|
43
|
+
let targetCheck;
|
|
44
|
+
try {
|
|
45
|
+
targetCheck = realpathSync(targetResolved);
|
|
46
|
+
} catch {
|
|
47
|
+
const rel = relative(rootResolved, targetResolved);
|
|
48
|
+
if (rel.startsWith("..") || rel.includes(`..${sep}`)) {
|
|
49
|
+
throw new Error(`unsafe path escapes project root: ${target}`);
|
|
50
|
+
}
|
|
51
|
+
targetCheck = resolve(rootReal, rel);
|
|
52
|
+
}
|
|
53
|
+
assertResolvedPathUnderRoot(rootReal, targetCheck);
|
|
54
|
+
}
|
|
55
|
+
function isResolvedPathUnderRoot(root, target) {
|
|
56
|
+
const rootResolved = resolveComparablePath(root);
|
|
57
|
+
const targetResolved = resolveComparablePath(target);
|
|
58
|
+
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);
|
|
62
|
+
}
|
|
26
63
|
|
|
27
64
|
// src/theme.ts
|
|
28
65
|
import { getPresetTheme, themeToCssVariables } from "@lessonkit/themes";
|
|
@@ -198,7 +235,7 @@ function validateDescriptor(input) {
|
|
|
198
235
|
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
199
236
|
}
|
|
200
237
|
const passingScore = assessment.passingScore;
|
|
201
|
-
if (passingScore !== void 0 && !(passingScore > 0)) {
|
|
238
|
+
if (passingScore !== void 0 && !(Number.isFinite(passingScore) && passingScore > 0)) {
|
|
202
239
|
issues.push({
|
|
203
240
|
path: `${path}.passingScore`,
|
|
204
241
|
message: "passingScore must be greater than 0 (absolute point threshold)"
|
|
@@ -351,12 +388,12 @@ function descriptorToInterchange(descriptor) {
|
|
|
351
388
|
}
|
|
352
389
|
|
|
353
390
|
// src/writeProject.ts
|
|
354
|
-
import { join, resolve as resolve4 } from "path";
|
|
391
|
+
import { join as join2, resolve as resolve4 } from "path";
|
|
355
392
|
import { materializeLessonkitProject } from "@lxpack/validators";
|
|
356
393
|
|
|
357
394
|
// src/spaDirs.ts
|
|
358
395
|
import { access } from "fs/promises";
|
|
359
|
-
import { resolve as resolve3 } from "path";
|
|
396
|
+
import { join, resolve as resolve3 } from "path";
|
|
360
397
|
async function resolveSpaDirs(options) {
|
|
361
398
|
const { descriptor, spaDistDir, lessonSpaDirs, projectRoot } = options;
|
|
362
399
|
const spaLessons = resolveSpaLessons(descriptor);
|
|
@@ -364,13 +401,18 @@ async function resolveSpaDirs(options) {
|
|
|
364
401
|
const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? "dist";
|
|
365
402
|
const srcDist = projectRoot ? resolve3(projectRoot, spaDistRelative) : resolve3(spaDistRelative);
|
|
366
403
|
if (projectRoot) {
|
|
367
|
-
|
|
404
|
+
assertRealPathUnderRoot(resolve3(projectRoot), srcDist);
|
|
368
405
|
}
|
|
369
406
|
try {
|
|
370
407
|
await access(srcDist);
|
|
371
408
|
} catch {
|
|
372
409
|
throw new Error(`spaDistDir not found: ${srcDist}`);
|
|
373
410
|
}
|
|
411
|
+
try {
|
|
412
|
+
await access(join(srcDist, "index.html"));
|
|
413
|
+
} catch {
|
|
414
|
+
throw new Error(`spaDistDir must contain index.html: ${join(srcDist, "index.html")}`);
|
|
415
|
+
}
|
|
374
416
|
const lessonId = spaLessons[0]?.id ?? "main";
|
|
375
417
|
return { [lessonId]: srcDist };
|
|
376
418
|
}
|
|
@@ -381,7 +423,23 @@ async function resolveSpaDirs(options) {
|
|
|
381
423
|
if (!src) {
|
|
382
424
|
throw new Error(`lessonSpaDirs missing build output for lesson "${lesson.id}"`);
|
|
383
425
|
}
|
|
384
|
-
|
|
426
|
+
const resolved = projectRoot ? resolve3(projectRoot, src) : resolve3(src);
|
|
427
|
+
if (projectRoot) {
|
|
428
|
+
assertRealPathUnderRoot(resolve3(projectRoot), resolved);
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
await access(resolved);
|
|
432
|
+
} catch {
|
|
433
|
+
throw new Error(`lessonSpaDirs path not found for lesson "${lesson.id}": ${resolved}`);
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
await access(join(resolved, "index.html"));
|
|
437
|
+
} catch {
|
|
438
|
+
throw new Error(
|
|
439
|
+
`lessonSpaDirs must contain index.html for lesson "${lesson.id}": ${join(resolved, "index.html")}`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
dirs[lesson.id] = resolved;
|
|
385
443
|
}
|
|
386
444
|
return dirs;
|
|
387
445
|
}
|
|
@@ -415,36 +473,114 @@ async function writeLxpackProject(options) {
|
|
|
415
473
|
const courseDir = materialized.courseDir;
|
|
416
474
|
return {
|
|
417
475
|
outDir: courseDir,
|
|
418
|
-
courseYamlPath:
|
|
419
|
-
lessonkitJsonPath:
|
|
476
|
+
courseYamlPath: join2(courseDir, "course.yaml"),
|
|
477
|
+
lessonkitJsonPath: join2(courseDir, "lessonkit.json")
|
|
420
478
|
};
|
|
421
479
|
}
|
|
422
480
|
|
|
423
481
|
// src/packageCourse.ts
|
|
424
|
-
import
|
|
425
|
-
import
|
|
426
|
-
import { tmpdir } from "os";
|
|
482
|
+
import { resolve as resolve6 } from "path";
|
|
483
|
+
import * as fsp3 from "fs/promises";
|
|
427
484
|
import {
|
|
428
485
|
buildCourse,
|
|
429
|
-
packageLessonkit,
|
|
430
486
|
validateCourse
|
|
431
487
|
} from "@lxpack/api";
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
488
|
+
|
|
489
|
+
// src/packaging/validateInputs.ts
|
|
490
|
+
import { join as join3, resolve as resolve5, win32 as win322 } from "path";
|
|
491
|
+
function validatePackageInputs(options) {
|
|
492
|
+
const { target, output, outputBaseDir } = options;
|
|
493
|
+
const outDir = resolve5(options.outDir);
|
|
494
|
+
const projectRoot = options.projectRoot ? resolve5(options.projectRoot) : void 0;
|
|
495
|
+
if (projectRoot) {
|
|
496
|
+
try {
|
|
497
|
+
assertResolvedPathUnderRoot(projectRoot, outDir);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
return {
|
|
500
|
+
ok: false,
|
|
501
|
+
courseDir: outDir,
|
|
502
|
+
target,
|
|
503
|
+
issues: [{ path: "outDir", message: err instanceof Error ? err.message : String(err) }]
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (outputBaseDir && !isSafeRelativeSpaPath(outputBaseDir)) {
|
|
508
|
+
return {
|
|
509
|
+
ok: false,
|
|
510
|
+
courseDir: outDir,
|
|
511
|
+
target,
|
|
512
|
+
issues: [{ path: "outputBaseDir", message: `unsafe outputBaseDir: ${outputBaseDir}` }]
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
if (output && !projectRoot && !isSafeRelativeSpaPath(output)) {
|
|
516
|
+
return {
|
|
517
|
+
ok: false,
|
|
518
|
+
courseDir: outDir,
|
|
519
|
+
target,
|
|
520
|
+
issues: [{ path: "output", message: `unsafe output: ${output}` }]
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (projectRoot && outputBaseDir) {
|
|
524
|
+
const resolvedOutputBase = resolve5(projectRoot, outputBaseDir);
|
|
525
|
+
try {
|
|
526
|
+
assertRealPathUnderRoot(projectRoot, resolvedOutputBase);
|
|
527
|
+
} catch (err) {
|
|
528
|
+
return {
|
|
529
|
+
ok: false,
|
|
530
|
+
courseDir: outDir,
|
|
531
|
+
target,
|
|
532
|
+
issues: [
|
|
533
|
+
{
|
|
534
|
+
path: "outputBaseDir",
|
|
535
|
+
message: err instanceof Error ? err.message : String(err)
|
|
536
|
+
}
|
|
537
|
+
]
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (projectRoot && output) {
|
|
542
|
+
const resolvedOutput = resolve5(projectRoot, output);
|
|
543
|
+
try {
|
|
544
|
+
assertResolvedPathUnderRoot(projectRoot, resolvedOutput);
|
|
545
|
+
} catch (err) {
|
|
546
|
+
return {
|
|
547
|
+
ok: false,
|
|
548
|
+
courseDir: outDir,
|
|
549
|
+
target,
|
|
550
|
+
issues: [{ path: "output", message: err instanceof Error ? err.message : String(err) }]
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return { ok: true, outDir, projectRoot };
|
|
437
555
|
}
|
|
438
|
-
|
|
439
|
-
return
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
556
|
+
function validateArtifactInStaging(stagingRoot, artifactPath, field) {
|
|
557
|
+
if (!artifactPath) return null;
|
|
558
|
+
const resolved = resolveComparablePath(artifactPath);
|
|
559
|
+
if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
|
|
560
|
+
return {
|
|
561
|
+
path: field,
|
|
562
|
+
message: `${field} is outside the staging directory: ${artifactPath}`
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
|
|
568
|
+
if (!artifactPath) return void 0;
|
|
569
|
+
const resolved = resolveComparablePath(artifactPath);
|
|
570
|
+
if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
|
|
571
|
+
return artifactPath;
|
|
572
|
+
}
|
|
573
|
+
const stagingResolved = resolveComparablePath(stagingRoot);
|
|
574
|
+
const relative2 = resolved === stagingResolved ? "" : resolved.slice(stagingResolved.length).replace(/^[/\\]/, "");
|
|
575
|
+
if (!relative2) return outDir;
|
|
576
|
+
if (/^[a-zA-Z]:[/\\]/.test(outDir)) {
|
|
577
|
+
return win322.join(outDir, relative2.replace(/\//g, win322.sep));
|
|
578
|
+
}
|
|
579
|
+
return join3(outDir, relative2);
|
|
447
580
|
}
|
|
581
|
+
|
|
582
|
+
// src/packaging/promote.ts
|
|
583
|
+
import * as fsp from "fs/promises";
|
|
448
584
|
async function pathExists(path) {
|
|
449
585
|
try {
|
|
450
586
|
await fsp.access(path);
|
|
@@ -453,82 +589,76 @@ async function pathExists(path) {
|
|
|
453
589
|
return false;
|
|
454
590
|
}
|
|
455
591
|
}
|
|
592
|
+
async function renameOrCopy(from, to) {
|
|
593
|
+
try {
|
|
594
|
+
await fsp.rename(from, to);
|
|
595
|
+
} catch (err) {
|
|
596
|
+
const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
|
|
597
|
+
if (code !== "EXDEV") throw err;
|
|
598
|
+
await fsp.cp(from, to, { recursive: true });
|
|
599
|
+
await fsp.rm(from, { recursive: true, force: true });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
456
602
|
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
457
603
|
const tmpPromote = `${outDir}.tmp-promote`;
|
|
458
604
|
const backup = `${outDir}.bak`;
|
|
459
|
-
await
|
|
605
|
+
await renameOrCopy(stagingDir, tmpPromote);
|
|
460
606
|
const hadOutDir = await pathExists(outDir);
|
|
461
607
|
if (hadOutDir) {
|
|
462
|
-
await
|
|
608
|
+
await renameOrCopy(outDir, backup);
|
|
463
609
|
}
|
|
464
610
|
try {
|
|
465
|
-
await
|
|
611
|
+
await renameOrCopy(tmpPromote, outDir);
|
|
466
612
|
} catch (promoteError) {
|
|
467
613
|
if (hadOutDir) {
|
|
468
614
|
try {
|
|
469
|
-
await
|
|
615
|
+
await renameOrCopy(backup, outDir);
|
|
616
|
+
} catch (restoreError) {
|
|
617
|
+
const failedPromote2 = `${outDir}.failed-promote-${Date.now()}`;
|
|
618
|
+
try {
|
|
619
|
+
await renameOrCopy(tmpPromote, failedPromote2);
|
|
620
|
+
} catch {
|
|
621
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
622
|
+
}
|
|
623
|
+
const promoteMsg = promoteError instanceof Error ? promoteError.message : String(promoteError);
|
|
624
|
+
const restoreMsg = restoreError instanceof Error ? restoreError.message : String(restoreError);
|
|
625
|
+
throw new Error(
|
|
626
|
+
`[lessonkit/lxpack] promote failed (${promoteMsg}) and could not restore ${outDir} (${restoreMsg}). Recovery: previous output may be in ${backup}; staged package may be in ${failedPromote2}.`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
} else {
|
|
630
|
+
try {
|
|
631
|
+
await renameOrCopy(tmpPromote, stagingDir);
|
|
470
632
|
} catch (restoreError) {
|
|
471
633
|
console.warn(
|
|
472
|
-
`[lessonkit/lxpack] failed to restore ${
|
|
634
|
+
`[lessonkit/lxpack] failed to restore ${stagingDir} after promote error:`,
|
|
473
635
|
restoreError instanceof Error ? restoreError.message : restoreError
|
|
474
636
|
);
|
|
637
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
475
638
|
}
|
|
639
|
+
throw promoteError;
|
|
640
|
+
}
|
|
641
|
+
const failedPromote = `${outDir}.failed-promote-${Date.now()}`;
|
|
642
|
+
try {
|
|
643
|
+
await renameOrCopy(tmpPromote, failedPromote);
|
|
644
|
+
} catch {
|
|
645
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
476
646
|
}
|
|
477
|
-
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
478
647
|
throw promoteError;
|
|
479
648
|
}
|
|
480
649
|
if (hadOutDir) {
|
|
481
650
|
await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
|
|
482
651
|
}
|
|
483
652
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
ok: false,
|
|
494
|
-
courseDir: outDir,
|
|
495
|
-
target,
|
|
496
|
-
issues: [{ path: "outputBaseDir", message: `unsafe outputBaseDir: ${outputBaseDir}` }]
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
if (projectRoot && output) {
|
|
500
|
-
const resolvedOutput = resolve5(projectRoot, output);
|
|
501
|
-
try {
|
|
502
|
-
assertResolvedPathUnderRoot(projectRoot, resolvedOutput);
|
|
503
|
-
} catch (err) {
|
|
504
|
-
return {
|
|
505
|
-
ok: false,
|
|
506
|
-
courseDir: outDir,
|
|
507
|
-
target,
|
|
508
|
-
issues: [
|
|
509
|
-
{
|
|
510
|
-
path: "output",
|
|
511
|
-
message: err instanceof Error ? err.message : String(err)
|
|
512
|
-
}
|
|
513
|
-
]
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
const descriptorValidation = validateDescriptor(writeOpts.descriptor);
|
|
518
|
-
if (!descriptorValidation.ok) {
|
|
519
|
-
return {
|
|
520
|
-
ok: false,
|
|
521
|
-
courseDir: outDir,
|
|
522
|
-
target,
|
|
523
|
-
issues: descriptorValidation.issues.map((i) => ({
|
|
524
|
-
path: i.path,
|
|
525
|
-
message: i.message
|
|
526
|
-
}))
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
const descriptor = descriptorValidation.descriptor;
|
|
530
|
-
const stagingDir = await fsp.mkdtemp(join2(tmpdir(), "lessonkit-lxpack-"));
|
|
531
|
-
let promoted = false;
|
|
653
|
+
|
|
654
|
+
// src/packaging/staging.ts
|
|
655
|
+
import * as fsp2 from "fs/promises";
|
|
656
|
+
import { dirname, join as join4 } from "path";
|
|
657
|
+
import { tmpdir } from "os";
|
|
658
|
+
import { packageLessonkit } from "@lxpack/api";
|
|
659
|
+
async function buildStagingPackage(options) {
|
|
660
|
+
const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
|
|
661
|
+
const stagingDir = await fsp2.mkdtemp(join4(tmpdir(), "lessonkit-lxpack-"));
|
|
532
662
|
try {
|
|
533
663
|
let spaDirs;
|
|
534
664
|
try {
|
|
@@ -536,8 +666,7 @@ async function packageLessonkitCourse(options) {
|
|
|
536
666
|
} catch (err) {
|
|
537
667
|
return {
|
|
538
668
|
ok: false,
|
|
539
|
-
|
|
540
|
-
target,
|
|
669
|
+
stagingDir,
|
|
541
670
|
issues: [
|
|
542
671
|
{
|
|
543
672
|
path: "spaDirs",
|
|
@@ -548,8 +677,8 @@ async function packageLessonkitCourse(options) {
|
|
|
548
677
|
}
|
|
549
678
|
const interchange = descriptorToInterchange(descriptor);
|
|
550
679
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
551
|
-
await
|
|
552
|
-
const defaultOutput = output ?? (dir ?
|
|
680
|
+
await fsp2.mkdir(join4(stagingDir, outputBase), { recursive: true });
|
|
681
|
+
const defaultOutput = output ?? (dir ? join4(outputBase, target) : join4(outputBase, `course-${target}.zip`));
|
|
553
682
|
const build = await packageLessonkit({
|
|
554
683
|
interchange,
|
|
555
684
|
spaDirs,
|
|
@@ -562,15 +691,9 @@ async function packageLessonkitCourse(options) {
|
|
|
562
691
|
writeAuthoringFiles: true
|
|
563
692
|
});
|
|
564
693
|
if (!build.ok) {
|
|
565
|
-
const validation2 = {
|
|
566
|
-
ok: false,
|
|
567
|
-
issues: build.issues
|
|
568
|
-
};
|
|
569
694
|
return {
|
|
570
695
|
ok: false,
|
|
571
|
-
|
|
572
|
-
target,
|
|
573
|
-
validation: validation2,
|
|
696
|
+
stagingDir,
|
|
574
697
|
build,
|
|
575
698
|
issues: build.issues.map((i) => ({
|
|
576
699
|
path: i.path,
|
|
@@ -579,48 +702,266 @@ async function packageLessonkitCourse(options) {
|
|
|
579
702
|
}))
|
|
580
703
|
};
|
|
581
704
|
}
|
|
582
|
-
|
|
705
|
+
return {
|
|
583
706
|
ok: true,
|
|
584
|
-
|
|
585
|
-
|
|
707
|
+
stagingDir,
|
|
708
|
+
build,
|
|
709
|
+
outputPath: "outputPath" in build ? build.outputPath : void 0,
|
|
710
|
+
outputDir: "outputDir" in build ? build.outputDir : void 0
|
|
586
711
|
};
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
712
|
+
} catch (err) {
|
|
713
|
+
await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
714
|
+
throw err;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async function ensureOutDirParent(outDir) {
|
|
718
|
+
await fsp2.mkdir(dirname(outDir), { recursive: true });
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/packageCourse.ts
|
|
722
|
+
async function validateLessonkitProject(options) {
|
|
723
|
+
return validateCourse({
|
|
724
|
+
courseDir: resolve6(options.courseDir),
|
|
725
|
+
target: options.target
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
async function buildLessonkitProject(options) {
|
|
729
|
+
return buildCourse({
|
|
730
|
+
courseDir: resolve6(options.courseDir),
|
|
731
|
+
target: options.target,
|
|
732
|
+
output: options.output,
|
|
733
|
+
dir: options.dir,
|
|
734
|
+
outputBaseDir: options.outputBaseDir,
|
|
735
|
+
assessments: options.assessments
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
async function packageLessonkitCourse(options) {
|
|
739
|
+
const { target, output, dir, outputBaseDir, ...writeOpts } = options;
|
|
740
|
+
const inputValidation = validatePackageInputs({
|
|
741
|
+
target,
|
|
742
|
+
output,
|
|
743
|
+
outputBaseDir,
|
|
744
|
+
outDir: writeOpts.outDir,
|
|
745
|
+
projectRoot: writeOpts.projectRoot
|
|
746
|
+
});
|
|
747
|
+
if (!inputValidation.ok) {
|
|
748
|
+
return {
|
|
749
|
+
ok: false,
|
|
750
|
+
courseDir: inputValidation.courseDir,
|
|
751
|
+
target: inputValidation.target,
|
|
752
|
+
issues: inputValidation.issues
|
|
595
753
|
};
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
754
|
+
}
|
|
755
|
+
const outDir = inputValidation.outDir;
|
|
756
|
+
const descriptorValidation = validateDescriptor(writeOpts.descriptor);
|
|
757
|
+
if (!descriptorValidation.ok) {
|
|
758
|
+
return {
|
|
759
|
+
ok: false,
|
|
760
|
+
courseDir: outDir,
|
|
761
|
+
target,
|
|
762
|
+
issues: descriptorValidation.issues.map((i) => ({
|
|
763
|
+
path: i.path,
|
|
764
|
+
message: i.message
|
|
765
|
+
}))
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
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
|
+
};
|
|
609
783
|
}
|
|
784
|
+
}
|
|
785
|
+
const staged = await buildStagingPackage({
|
|
786
|
+
...writeOpts,
|
|
787
|
+
descriptor,
|
|
788
|
+
target,
|
|
789
|
+
output,
|
|
790
|
+
dir,
|
|
791
|
+
outputBaseDir
|
|
792
|
+
});
|
|
793
|
+
if (!staged.ok) {
|
|
794
|
+
await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
795
|
+
const validation2 = staged.build ? { ok: false, issues: staged.build.issues } : void 0;
|
|
610
796
|
return {
|
|
611
|
-
ok:
|
|
797
|
+
ok: false,
|
|
798
|
+
courseDir: outDir,
|
|
799
|
+
target,
|
|
800
|
+
validation: validation2,
|
|
801
|
+
build: staged.build,
|
|
802
|
+
issues: staged.issues
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
const { stagingDir, build } = staged;
|
|
806
|
+
const stagingRoot = await fsp3.realpath(stagingDir);
|
|
807
|
+
const artifactIssues = [
|
|
808
|
+
validateArtifactInStaging(stagingRoot, staged.outputPath, "outputPath"),
|
|
809
|
+
validateArtifactInStaging(stagingRoot, staged.outputDir, "outputDir")
|
|
810
|
+
].filter((issue) => issue != null);
|
|
811
|
+
if (artifactIssues.length > 0) {
|
|
812
|
+
await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
813
|
+
return {
|
|
814
|
+
ok: false,
|
|
815
|
+
courseDir: outDir,
|
|
816
|
+
target,
|
|
817
|
+
validation: { ok: true, manifest: build.manifest, issues: build.issues },
|
|
818
|
+
build,
|
|
819
|
+
issues: artifactIssues
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
const remappedOutputPath = remapArtifactPaths(stagingRoot, outDir, staged.outputPath);
|
|
823
|
+
const remappedOutputDir = remapArtifactPaths(stagingRoot, outDir, staged.outputDir);
|
|
824
|
+
const validation = {
|
|
825
|
+
ok: true,
|
|
826
|
+
manifest: build.manifest,
|
|
827
|
+
issues: build.issues
|
|
828
|
+
};
|
|
829
|
+
try {
|
|
830
|
+
await ensureOutDirParent(outDir);
|
|
831
|
+
await promoteStagingToOutDir(stagingDir, outDir);
|
|
832
|
+
} catch (err) {
|
|
833
|
+
return {
|
|
834
|
+
ok: false,
|
|
612
835
|
courseDir: outDir,
|
|
613
836
|
target,
|
|
614
|
-
outputPath: remappedOutputPath,
|
|
615
|
-
outputDir: remappedOutputDir,
|
|
616
|
-
fileCount: build.fileCount,
|
|
617
837
|
validation,
|
|
618
|
-
build
|
|
838
|
+
build,
|
|
839
|
+
issues: [
|
|
840
|
+
{
|
|
841
|
+
path: "promote",
|
|
842
|
+
message: err instanceof Error ? err.message : String(err)
|
|
843
|
+
}
|
|
844
|
+
]
|
|
619
845
|
};
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
|
|
846
|
+
}
|
|
847
|
+
const remappedBuild = { ...build };
|
|
848
|
+
if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
|
|
849
|
+
remappedBuild.outputPath = remappedOutputPath;
|
|
850
|
+
}
|
|
851
|
+
if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
|
|
852
|
+
remappedBuild.outputDir = remappedOutputDir;
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
ok: true,
|
|
856
|
+
courseDir: outDir,
|
|
857
|
+
target,
|
|
858
|
+
outputPath: remappedOutputPath,
|
|
859
|
+
outputDir: remappedOutputDir,
|
|
860
|
+
fileCount: build.fileCount,
|
|
861
|
+
validation,
|
|
862
|
+
build: remappedBuild
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/manifest.ts
|
|
867
|
+
var DEFAULT_PATHS = {
|
|
868
|
+
spaDistDir: "dist",
|
|
869
|
+
lxpackOutDir: ".lxpack/course",
|
|
870
|
+
outputBaseDir: ".lxpack/out"
|
|
871
|
+
};
|
|
872
|
+
function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
|
|
873
|
+
if (!raw || typeof raw !== "object") {
|
|
874
|
+
return { ok: false, issues: [{ path: label, message: "must be a JSON object" }] };
|
|
875
|
+
}
|
|
876
|
+
const config = raw;
|
|
877
|
+
const issues = [];
|
|
878
|
+
if (config.schemaVersion !== 1) {
|
|
879
|
+
issues.push({
|
|
880
|
+
path: "schemaVersion",
|
|
881
|
+
message: `must be 1 (got ${String(config.schemaVersion)})`
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
const name = config.name;
|
|
885
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
886
|
+
issues.push({ path: "name", message: "must be a non-empty string" });
|
|
887
|
+
}
|
|
888
|
+
const courseRaw = config.course;
|
|
889
|
+
if (Array.isArray(courseRaw)) {
|
|
890
|
+
issues.push({ path: "course", message: "must be an object, not an array" });
|
|
891
|
+
return { ok: false, issues };
|
|
892
|
+
}
|
|
893
|
+
if (!courseRaw || typeof courseRaw !== "object") {
|
|
894
|
+
issues.push({ path: "course", message: "must be an object" });
|
|
895
|
+
return { ok: false, issues };
|
|
896
|
+
}
|
|
897
|
+
const courseObj = courseRaw;
|
|
898
|
+
if (courseObj.lessons !== void 0 && !Array.isArray(courseObj.lessons)) {
|
|
899
|
+
issues.push({ path: "course.lessons", message: "must be an array" });
|
|
900
|
+
}
|
|
901
|
+
if (courseObj.assessments !== void 0 && !Array.isArray(courseObj.assessments)) {
|
|
902
|
+
issues.push({ path: "course.assessments", message: "must be an array" });
|
|
903
|
+
}
|
|
904
|
+
if (issues.length) return { ok: false, issues };
|
|
905
|
+
const validation = validateDescriptor(courseRaw);
|
|
906
|
+
if (!validation.ok) {
|
|
907
|
+
for (const i of validation.issues) {
|
|
908
|
+
issues.push({
|
|
909
|
+
path: i.path.startsWith("course.") ? i.path : `course.${i.path}`,
|
|
910
|
+
message: i.message
|
|
911
|
+
});
|
|
623
912
|
}
|
|
913
|
+
} else if (validation.descriptor.layout === "per-lesson-spa") {
|
|
914
|
+
issues.push({
|
|
915
|
+
path: "course.layout",
|
|
916
|
+
message: "per-lesson-spa is not supported by lessonkit package yet. Use single-spa or package via @lessonkit/lxpack directly."
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
const paths = { ...DEFAULT_PATHS };
|
|
920
|
+
const pathsRaw = config.paths;
|
|
921
|
+
if (pathsRaw !== void 0 && (typeof pathsRaw !== "object" || pathsRaw === null)) {
|
|
922
|
+
issues.push({ path: "paths", message: "must be an object" });
|
|
923
|
+
} else if (pathsRaw && typeof pathsRaw === "object") {
|
|
924
|
+
const p = pathsRaw;
|
|
925
|
+
for (const key of ["spaDistDir", "lxpackOutDir", "outputBaseDir"]) {
|
|
926
|
+
if (p[key] !== void 0) {
|
|
927
|
+
if (typeof p[key] !== "string" || !p[key].trim()) {
|
|
928
|
+
issues.push({ path: `paths.${key}`, message: "must be a non-empty string" });
|
|
929
|
+
} else {
|
|
930
|
+
paths[key] = p[key].trim();
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const courseSpaDistDir = validation.ok ? validation.descriptor.spaDistDir?.trim() : void 0;
|
|
936
|
+
if (courseSpaDistDir && courseSpaDistDir !== paths.spaDistDir) {
|
|
937
|
+
issues.push({
|
|
938
|
+
path: "course.spaDistDir",
|
|
939
|
+
message: `"course.spaDistDir" (${courseSpaDistDir}) differs from "paths.spaDistDir" (${paths.spaDistDir}). Use paths.spaDistDir for CLI build and package.`
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
if (projectRoot) {
|
|
943
|
+
const pathIssues = validateProjectPaths(projectRoot, paths);
|
|
944
|
+
for (const pi of pathIssues) {
|
|
945
|
+
issues.push({ path: pi.path, message: pi.message });
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (issues.length) return { ok: false, issues };
|
|
949
|
+
if (!validation.ok) return { ok: false, issues };
|
|
950
|
+
return {
|
|
951
|
+
ok: true,
|
|
952
|
+
manifest: {
|
|
953
|
+
schemaVersion: 1,
|
|
954
|
+
name,
|
|
955
|
+
course: validation.descriptor,
|
|
956
|
+
paths
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
async function loadLessonkitManifestFromFile(readJson, label = "lessonkit.json", projectRoot) {
|
|
961
|
+
try {
|
|
962
|
+
return parseLessonkitManifest(await readJson(), label, projectRoot);
|
|
963
|
+
} catch {
|
|
964
|
+
return { ok: false, issues: [{ path: label, message: "failed to read or parse JSON" }] };
|
|
624
965
|
}
|
|
625
966
|
}
|
|
626
967
|
|
|
@@ -639,21 +980,28 @@ export {
|
|
|
639
980
|
LESSONKIT_TELEMETRY_EVENTS,
|
|
640
981
|
assessmentDescriptorToLxpack,
|
|
641
982
|
buildLessonkitProject,
|
|
983
|
+
buildStagingPackage,
|
|
642
984
|
descriptorToInterchange,
|
|
985
|
+
ensureOutDirParent,
|
|
643
986
|
extractAssessments,
|
|
644
987
|
lessonkitInterchangeSchema,
|
|
988
|
+
loadLessonkitManifestFromFile,
|
|
645
989
|
mapLessonkitIds,
|
|
646
990
|
mapLessonkitTelemetryToBridgeAction,
|
|
647
991
|
mapLessonkitTelemetryToLxpack,
|
|
648
992
|
materializeLessonkitProject2 as materializeLessonkitProject,
|
|
649
993
|
packageLessonkitCourse,
|
|
650
994
|
parseLessonkitInterchange,
|
|
995
|
+
parseLessonkitManifest,
|
|
996
|
+
promoteStagingToOutDir,
|
|
997
|
+
remapArtifactPaths,
|
|
651
998
|
resolveSafePackageOutputOverride,
|
|
652
999
|
resolveSpaLessons,
|
|
653
1000
|
telemetryEventToLessonkit,
|
|
654
1001
|
themeToLxpackRuntime,
|
|
655
1002
|
validateDescriptor,
|
|
656
1003
|
validateLessonkitProject,
|
|
1004
|
+
validatePackageInputs,
|
|
657
1005
|
validateProjectPaths,
|
|
658
1006
|
writeLxpackProject
|
|
659
1007
|
};
|