@lessonkit/lxpack 0.9.3 → 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 +472 -120
- package/dist/index.d.cts +82 -2
- package/dist/index.d.ts +82 -2
- package/dist/index.js +470 -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
|
}
|
|
@@ -383,7 +425,19 @@ async function resolveSpaDirs(options) {
|
|
|
383
425
|
}
|
|
384
426
|
const resolved = projectRoot ? resolve3(projectRoot, src) : resolve3(src);
|
|
385
427
|
if (projectRoot) {
|
|
386
|
-
|
|
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
|
+
);
|
|
387
441
|
}
|
|
388
442
|
dirs[lesson.id] = resolved;
|
|
389
443
|
}
|
|
@@ -419,36 +473,114 @@ async function writeLxpackProject(options) {
|
|
|
419
473
|
const courseDir = materialized.courseDir;
|
|
420
474
|
return {
|
|
421
475
|
outDir: courseDir,
|
|
422
|
-
courseYamlPath:
|
|
423
|
-
lessonkitJsonPath:
|
|
476
|
+
courseYamlPath: join2(courseDir, "course.yaml"),
|
|
477
|
+
lessonkitJsonPath: join2(courseDir, "lessonkit.json")
|
|
424
478
|
};
|
|
425
479
|
}
|
|
426
480
|
|
|
427
481
|
// src/packageCourse.ts
|
|
428
|
-
import
|
|
429
|
-
import
|
|
430
|
-
import { tmpdir } from "os";
|
|
482
|
+
import { resolve as resolve6 } from "path";
|
|
483
|
+
import * as fsp3 from "fs/promises";
|
|
431
484
|
import {
|
|
432
485
|
buildCourse,
|
|
433
|
-
packageLessonkit,
|
|
434
486
|
validateCourse
|
|
435
487
|
} from "@lxpack/api";
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
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 };
|
|
441
555
|
}
|
|
442
|
-
|
|
443
|
-
return
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
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;
|
|
451
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);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/packaging/promote.ts
|
|
583
|
+
import * as fsp from "fs/promises";
|
|
452
584
|
async function pathExists(path) {
|
|
453
585
|
try {
|
|
454
586
|
await fsp.access(path);
|
|
@@ -457,82 +589,76 @@ async function pathExists(path) {
|
|
|
457
589
|
return false;
|
|
458
590
|
}
|
|
459
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
|
+
}
|
|
460
602
|
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
461
603
|
const tmpPromote = `${outDir}.tmp-promote`;
|
|
462
604
|
const backup = `${outDir}.bak`;
|
|
463
|
-
await
|
|
605
|
+
await renameOrCopy(stagingDir, tmpPromote);
|
|
464
606
|
const hadOutDir = await pathExists(outDir);
|
|
465
607
|
if (hadOutDir) {
|
|
466
|
-
await
|
|
608
|
+
await renameOrCopy(outDir, backup);
|
|
467
609
|
}
|
|
468
610
|
try {
|
|
469
|
-
await
|
|
611
|
+
await renameOrCopy(tmpPromote, outDir);
|
|
470
612
|
} catch (promoteError) {
|
|
471
613
|
if (hadOutDir) {
|
|
472
614
|
try {
|
|
473
|
-
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);
|
|
474
632
|
} catch (restoreError) {
|
|
475
633
|
console.warn(
|
|
476
|
-
`[lessonkit/lxpack] failed to restore ${
|
|
634
|
+
`[lessonkit/lxpack] failed to restore ${stagingDir} after promote error:`,
|
|
477
635
|
restoreError instanceof Error ? restoreError.message : restoreError
|
|
478
636
|
);
|
|
637
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
479
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);
|
|
480
646
|
}
|
|
481
|
-
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
482
647
|
throw promoteError;
|
|
483
648
|
}
|
|
484
649
|
if (hadOutDir) {
|
|
485
650
|
await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
|
|
486
651
|
}
|
|
487
652
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
ok: false,
|
|
498
|
-
courseDir: outDir,
|
|
499
|
-
target,
|
|
500
|
-
issues: [{ path: "outputBaseDir", message: `unsafe outputBaseDir: ${outputBaseDir}` }]
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
if (projectRoot && output) {
|
|
504
|
-
const resolvedOutput = resolve5(projectRoot, output);
|
|
505
|
-
try {
|
|
506
|
-
assertResolvedPathUnderRoot(projectRoot, resolvedOutput);
|
|
507
|
-
} catch (err) {
|
|
508
|
-
return {
|
|
509
|
-
ok: false,
|
|
510
|
-
courseDir: outDir,
|
|
511
|
-
target,
|
|
512
|
-
issues: [
|
|
513
|
-
{
|
|
514
|
-
path: "output",
|
|
515
|
-
message: err instanceof Error ? err.message : String(err)
|
|
516
|
-
}
|
|
517
|
-
]
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
const descriptorValidation = validateDescriptor(writeOpts.descriptor);
|
|
522
|
-
if (!descriptorValidation.ok) {
|
|
523
|
-
return {
|
|
524
|
-
ok: false,
|
|
525
|
-
courseDir: outDir,
|
|
526
|
-
target,
|
|
527
|
-
issues: descriptorValidation.issues.map((i) => ({
|
|
528
|
-
path: i.path,
|
|
529
|
-
message: i.message
|
|
530
|
-
}))
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
const descriptor = descriptorValidation.descriptor;
|
|
534
|
-
const stagingDir = await fsp.mkdtemp(join2(tmpdir(), "lessonkit-lxpack-"));
|
|
535
|
-
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-"));
|
|
536
662
|
try {
|
|
537
663
|
let spaDirs;
|
|
538
664
|
try {
|
|
@@ -540,8 +666,7 @@ async function packageLessonkitCourse(options) {
|
|
|
540
666
|
} catch (err) {
|
|
541
667
|
return {
|
|
542
668
|
ok: false,
|
|
543
|
-
|
|
544
|
-
target,
|
|
669
|
+
stagingDir,
|
|
545
670
|
issues: [
|
|
546
671
|
{
|
|
547
672
|
path: "spaDirs",
|
|
@@ -552,8 +677,8 @@ async function packageLessonkitCourse(options) {
|
|
|
552
677
|
}
|
|
553
678
|
const interchange = descriptorToInterchange(descriptor);
|
|
554
679
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
555
|
-
await
|
|
556
|
-
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`));
|
|
557
682
|
const build = await packageLessonkit({
|
|
558
683
|
interchange,
|
|
559
684
|
spaDirs,
|
|
@@ -566,15 +691,9 @@ async function packageLessonkitCourse(options) {
|
|
|
566
691
|
writeAuthoringFiles: true
|
|
567
692
|
});
|
|
568
693
|
if (!build.ok) {
|
|
569
|
-
const validation2 = {
|
|
570
|
-
ok: false,
|
|
571
|
-
issues: build.issues
|
|
572
|
-
};
|
|
573
694
|
return {
|
|
574
695
|
ok: false,
|
|
575
|
-
|
|
576
|
-
target,
|
|
577
|
-
validation: validation2,
|
|
696
|
+
stagingDir,
|
|
578
697
|
build,
|
|
579
698
|
issues: build.issues.map((i) => ({
|
|
580
699
|
path: i.path,
|
|
@@ -583,48 +702,266 @@ async function packageLessonkitCourse(options) {
|
|
|
583
702
|
}))
|
|
584
703
|
};
|
|
585
704
|
}
|
|
586
|
-
|
|
705
|
+
return {
|
|
587
706
|
ok: true,
|
|
588
|
-
|
|
589
|
-
|
|
707
|
+
stagingDir,
|
|
708
|
+
build,
|
|
709
|
+
outputPath: "outputPath" in build ? build.outputPath : void 0,
|
|
710
|
+
outputDir: "outputDir" in build ? build.outputDir : void 0
|
|
590
711
|
};
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
|
599
753
|
};
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
+
};
|
|
613
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;
|
|
614
796
|
return {
|
|
615
|
-
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,
|
|
616
835
|
courseDir: outDir,
|
|
617
836
|
target,
|
|
618
|
-
outputPath: remappedOutputPath,
|
|
619
|
-
outputDir: remappedOutputDir,
|
|
620
|
-
fileCount: build.fileCount,
|
|
621
837
|
validation,
|
|
622
|
-
build
|
|
838
|
+
build,
|
|
839
|
+
issues: [
|
|
840
|
+
{
|
|
841
|
+
path: "promote",
|
|
842
|
+
message: err instanceof Error ? err.message : String(err)
|
|
843
|
+
}
|
|
844
|
+
]
|
|
623
845
|
};
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
|
|
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
|
+
});
|
|
627
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" }] };
|
|
628
965
|
}
|
|
629
966
|
}
|
|
630
967
|
|
|
@@ -643,21 +980,28 @@ export {
|
|
|
643
980
|
LESSONKIT_TELEMETRY_EVENTS,
|
|
644
981
|
assessmentDescriptorToLxpack,
|
|
645
982
|
buildLessonkitProject,
|
|
983
|
+
buildStagingPackage,
|
|
646
984
|
descriptorToInterchange,
|
|
985
|
+
ensureOutDirParent,
|
|
647
986
|
extractAssessments,
|
|
648
987
|
lessonkitInterchangeSchema,
|
|
988
|
+
loadLessonkitManifestFromFile,
|
|
649
989
|
mapLessonkitIds,
|
|
650
990
|
mapLessonkitTelemetryToBridgeAction,
|
|
651
991
|
mapLessonkitTelemetryToLxpack,
|
|
652
992
|
materializeLessonkitProject2 as materializeLessonkitProject,
|
|
653
993
|
packageLessonkitCourse,
|
|
654
994
|
parseLessonkitInterchange,
|
|
995
|
+
parseLessonkitManifest,
|
|
996
|
+
promoteStagingToOutDir,
|
|
997
|
+
remapArtifactPaths,
|
|
655
998
|
resolveSafePackageOutputOverride,
|
|
656
999
|
resolveSpaLessons,
|
|
657
1000
|
telemetryEventToLessonkit,
|
|
658
1001
|
themeToLxpackRuntime,
|
|
659
1002
|
validateDescriptor,
|
|
660
1003
|
validateLessonkitProject,
|
|
1004
|
+
validatePackageInputs,
|
|
661
1005
|
validateProjectPaths,
|
|
662
1006
|
writeLxpackProject
|
|
663
1007
|
};
|