@lessonkit/lxpack 0.9.3 → 1.0.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 +16 -15
- package/dist/bridge.cjs +81 -8
- package/dist/bridge.d.cts +8 -2
- package/dist/bridge.d.ts +8 -2
- package/dist/bridge.js +65 -3
- package/dist/chunk-DYQI222N.js +41 -0
- package/dist/index.cjs +626 -141
- package/dist/index.d.cts +92 -7
- package/dist/index.d.ts +92 -7
- package/dist/index.js +609 -142
- package/lessonkit-manifest.v1.json +100 -0
- package/package.json +12 -10
- package/dist/chunk-PSUSESH3.js +0 -32
package/dist/index.js
CHANGED
|
@@ -1,28 +1,78 @@
|
|
|
1
1
|
import {
|
|
2
2
|
telemetryEventToLessonkit
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-DYQI222N.js";
|
|
4
4
|
|
|
5
5
|
// src/validateDescriptor.ts
|
|
6
6
|
import { validateId } from "@lessonkit/core";
|
|
7
7
|
|
|
8
8
|
// src/spaPath.ts
|
|
9
|
-
import {
|
|
9
|
+
import { realpathSync } from "fs";
|
|
10
|
+
import { isAbsolute, 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;
|
|
13
|
-
if (/^[a-zA-Z]
|
|
14
|
-
|
|
20
|
+
if (/^[a-zA-Z]:/.test(spaPath)) return false;
|
|
21
|
+
if (spaPath === "." || spaPath === "./") return false;
|
|
22
|
+
const segments = spaPath.split(/[/\\]/).filter((s) => s.length > 0 && s !== ".");
|
|
15
23
|
if (segments.some((s) => s === "..")) return false;
|
|
16
|
-
return
|
|
24
|
+
return segments.length > 0;
|
|
17
25
|
}
|
|
18
26
|
function assertResolvedPathUnderRoot(root, target) {
|
|
19
|
-
const rootResolved =
|
|
20
|
-
const targetResolved =
|
|
27
|
+
const rootResolved = resolveComparablePath(root);
|
|
28
|
+
const targetResolved = resolveComparablePath(target);
|
|
21
29
|
const prefix = rootResolved.endsWith(sep) ? rootResolved : rootResolved + sep;
|
|
22
|
-
|
|
30
|
+
const win32Prefix = rootResolved.endsWith(win32.sep) ? rootResolved : rootResolved + win32.sep;
|
|
31
|
+
if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix) && !targetResolved.startsWith(win32Prefix)) {
|
|
23
32
|
throw new Error(`unsafe path escapes project root: ${target}`);
|
|
24
33
|
}
|
|
25
34
|
}
|
|
35
|
+
function assertRealPathUnderRoot(root, target) {
|
|
36
|
+
const rootResolved = resolveComparablePath(root);
|
|
37
|
+
const targetResolved = resolveComparablePath(target);
|
|
38
|
+
let rootReal;
|
|
39
|
+
try {
|
|
40
|
+
rootReal = realpathSync(rootResolved);
|
|
41
|
+
} catch {
|
|
42
|
+
rootReal = rootResolved;
|
|
43
|
+
}
|
|
44
|
+
let targetCheck;
|
|
45
|
+
try {
|
|
46
|
+
targetCheck = realpathSync(targetResolved);
|
|
47
|
+
} catch {
|
|
48
|
+
const rel = relative(rootResolved, targetResolved);
|
|
49
|
+
if (rel.startsWith("..") || rel.includes(`..${sep}`)) {
|
|
50
|
+
throw new Error(`unsafe path escapes project root: ${target}`);
|
|
51
|
+
}
|
|
52
|
+
targetCheck = resolve(rootReal, rel);
|
|
53
|
+
}
|
|
54
|
+
assertResolvedPathUnderRoot(rootReal, targetCheck);
|
|
55
|
+
}
|
|
56
|
+
function normalizePathForComparison(p) {
|
|
57
|
+
const resolved = resolveComparablePath(p);
|
|
58
|
+
return /^[a-zA-Z]:[/\\]/.test(resolved) ? resolved.toLowerCase() : resolved;
|
|
59
|
+
}
|
|
60
|
+
function relativePathUnderRoot(root, target) {
|
|
61
|
+
const rootResolved = normalizePathForComparison(root);
|
|
62
|
+
const targetResolved = normalizePathForComparison(target);
|
|
63
|
+
if (/^[a-zA-Z]:[/\\]/.test(rootResolved)) {
|
|
64
|
+
return win32.relative(rootResolved, targetResolved);
|
|
65
|
+
}
|
|
66
|
+
return relative(rootResolved, targetResolved);
|
|
67
|
+
}
|
|
68
|
+
function isResolvedPathUnderRoot(root, target) {
|
|
69
|
+
const rootResolved = normalizePathForComparison(root);
|
|
70
|
+
const targetResolved = normalizePathForComparison(target);
|
|
71
|
+
if (targetResolved === rootResolved) return true;
|
|
72
|
+
const rel = relativePathUnderRoot(root, target);
|
|
73
|
+
if (!rel) return true;
|
|
74
|
+
return !rel.startsWith("..") && !isAbsolute(rel);
|
|
75
|
+
}
|
|
26
76
|
|
|
27
77
|
// src/theme.ts
|
|
28
78
|
import { getPresetTheme, themeToCssVariables } from "@lessonkit/themes";
|
|
@@ -42,6 +92,70 @@ function themeToLxpackRuntime(input) {
|
|
|
42
92
|
// src/validateDescriptor.ts
|
|
43
93
|
var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
|
|
44
94
|
var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
|
|
95
|
+
function isRecord(value) {
|
|
96
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
97
|
+
}
|
|
98
|
+
function parseLessonDescriptor(raw) {
|
|
99
|
+
if (!isRecord(raw)) {
|
|
100
|
+
return { id: "", title: "" };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
id: typeof raw.id === "string" ? raw.id : "",
|
|
104
|
+
title: typeof raw.title === "string" ? raw.title : "",
|
|
105
|
+
spaPath: typeof raw.spaPath === "string" ? raw.spaPath : void 0
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function parseAssessmentDescriptor(raw) {
|
|
109
|
+
if (!isRecord(raw)) {
|
|
110
|
+
return { checkId: "", question: "", choices: [], answer: "" };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
checkId: typeof raw.checkId === "string" ? raw.checkId : "",
|
|
114
|
+
question: typeof raw.question === "string" ? raw.question : "",
|
|
115
|
+
choices: Array.isArray(raw.choices) ? raw.choices.filter((c) => typeof c === "string") : [],
|
|
116
|
+
answer: typeof raw.answer === "string" ? raw.answer : "",
|
|
117
|
+
passingScore: typeof raw.passingScore === "number" ? raw.passingScore : void 0
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function parseCourseDescriptorInput(input) {
|
|
121
|
+
if (!isRecord(input)) return null;
|
|
122
|
+
const trackingRaw = input.tracking;
|
|
123
|
+
let tracking;
|
|
124
|
+
if (isRecord(trackingRaw)) {
|
|
125
|
+
const completionRaw = trackingRaw.completion;
|
|
126
|
+
const xapiRaw = trackingRaw.xapi;
|
|
127
|
+
tracking = {
|
|
128
|
+
completion: isRecord(completionRaw) ? {
|
|
129
|
+
threshold: typeof completionRaw.threshold === "number" ? completionRaw.threshold : void 0
|
|
130
|
+
} : void 0,
|
|
131
|
+
xapi: isRecord(xapiRaw) ? {
|
|
132
|
+
activityIri: typeof xapiRaw.activityIri === "string" ? xapiRaw.activityIri : void 0
|
|
133
|
+
} : void 0
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const themeRaw = input.theme;
|
|
137
|
+
let theme;
|
|
138
|
+
if (isRecord(themeRaw)) {
|
|
139
|
+
theme = {
|
|
140
|
+
preset: typeof themeRaw.preset === "string" ? themeRaw.preset : void 0
|
|
141
|
+
};
|
|
142
|
+
if (isRecord(themeRaw.theme)) {
|
|
143
|
+
theme.theme = themeRaw.theme;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
courseId: typeof input.courseId === "string" ? input.courseId : "",
|
|
148
|
+
title: typeof input.title === "string" ? input.title : "",
|
|
149
|
+
version: typeof input.version === "string" ? input.version : void 0,
|
|
150
|
+
layout: typeof input.layout === "string" ? input.layout : void 0,
|
|
151
|
+
lessons: Array.isArray(input.lessons) ? input.lessons.map(parseLessonDescriptor) : [],
|
|
152
|
+
assessments: Array.isArray(input.assessments) ? input.assessments.map(parseAssessmentDescriptor) : void 0,
|
|
153
|
+
theme,
|
|
154
|
+
tracking,
|
|
155
|
+
spaDistDir: typeof input.spaDistDir === "string" ? input.spaDistDir : void 0,
|
|
156
|
+
spaLessonId: typeof input.spaLessonId === "string" ? input.spaLessonId : void 0
|
|
157
|
+
};
|
|
158
|
+
}
|
|
45
159
|
function normalizeDescriptor(input) {
|
|
46
160
|
const course = validateId(input.courseId, "courseId");
|
|
47
161
|
if (!course.ok) throw new Error("normalizeDescriptor called with invalid courseId");
|
|
@@ -75,6 +189,31 @@ function normalizeDescriptor(input) {
|
|
|
75
189
|
};
|
|
76
190
|
}
|
|
77
191
|
function validateDescriptor(input) {
|
|
192
|
+
const parsed = parseCourseDescriptorInput(input);
|
|
193
|
+
if (parsed === null) {
|
|
194
|
+
return { ok: false, issues: [{ path: "course", message: "must be an object" }] };
|
|
195
|
+
}
|
|
196
|
+
return validateDescriptorParsed(parsed);
|
|
197
|
+
}
|
|
198
|
+
function validateDescriptorForTarget(input, target) {
|
|
199
|
+
const result = validateDescriptor(input);
|
|
200
|
+
if (!result.ok || !target) return result;
|
|
201
|
+
if (target !== "xapi" && target !== "cmi5") return result;
|
|
202
|
+
const activityIri = result.descriptor.tracking?.xapi?.activityIri?.trim();
|
|
203
|
+
if (!activityIri) {
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
issues: [
|
|
207
|
+
{
|
|
208
|
+
path: "course.tracking.xapi.activityIri",
|
|
209
|
+
message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
function validateDescriptorParsed(input) {
|
|
78
217
|
const issues = [];
|
|
79
218
|
const course = validateId(input.courseId, "courseId");
|
|
80
219
|
if (!course.ok) issues.push(...course.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
@@ -198,7 +337,7 @@ function validateDescriptor(input) {
|
|
|
198
337
|
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
199
338
|
}
|
|
200
339
|
const passingScore = assessment.passingScore;
|
|
201
|
-
if (passingScore !== void 0 && !(passingScore > 0)) {
|
|
340
|
+
if (passingScore !== void 0 && !(Number.isFinite(passingScore) && passingScore > 0)) {
|
|
202
341
|
issues.push({
|
|
203
342
|
path: `${path}.passingScore`,
|
|
204
343
|
message: "passingScore must be greater than 0 (absolute point threshold)"
|
|
@@ -210,7 +349,7 @@ function validateDescriptor(input) {
|
|
|
210
349
|
}
|
|
211
350
|
|
|
212
351
|
// src/validateProjectPaths.ts
|
|
213
|
-
import { isAbsolute, resolve as resolve2 } from "path";
|
|
352
|
+
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
214
353
|
function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
215
354
|
if (!isSafeRelativeSpaPath(value)) {
|
|
216
355
|
issues.push({
|
|
@@ -220,7 +359,7 @@ function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
|
220
359
|
return;
|
|
221
360
|
}
|
|
222
361
|
try {
|
|
223
|
-
|
|
362
|
+
assertRealPathUnderRoot(projectRoot, resolve2(projectRoot, value));
|
|
224
363
|
} catch {
|
|
225
364
|
issues.push({
|
|
226
365
|
path: fieldPath,
|
|
@@ -248,16 +387,16 @@ function resolveSafePackageOutputOverride(projectRoot, override) {
|
|
|
248
387
|
if (!trimmed) {
|
|
249
388
|
throw new Error("output override must be a non-empty path");
|
|
250
389
|
}
|
|
251
|
-
if (
|
|
390
|
+
if (isAbsolute2(trimmed)) {
|
|
252
391
|
const resolved2 = resolve2(trimmed);
|
|
253
|
-
|
|
392
|
+
assertRealPathUnderRoot(root, resolved2);
|
|
254
393
|
return resolved2;
|
|
255
394
|
}
|
|
256
395
|
if (!isSafeRelativeSpaPath(trimmed)) {
|
|
257
396
|
throw new Error(`unsafe output path: ${override}`);
|
|
258
397
|
}
|
|
259
398
|
const resolved = resolve2(root, trimmed);
|
|
260
|
-
|
|
399
|
+
assertRealPathUnderRoot(root, resolved);
|
|
261
400
|
return resolved;
|
|
262
401
|
}
|
|
263
402
|
|
|
@@ -304,6 +443,18 @@ function extractAssessments(descriptor) {
|
|
|
304
443
|
}
|
|
305
444
|
|
|
306
445
|
// src/interchange.ts
|
|
446
|
+
function mapDescriptorTracking(tracking) {
|
|
447
|
+
if (!tracking) return void 0;
|
|
448
|
+
const mapped = {};
|
|
449
|
+
if (tracking.completion?.threshold !== void 0) {
|
|
450
|
+
mapped.completion = { threshold: tracking.completion.threshold };
|
|
451
|
+
}
|
|
452
|
+
const activityIri = tracking.xapi?.activityIri?.trim();
|
|
453
|
+
if (activityIri) {
|
|
454
|
+
mapped.xapi = { activityIri };
|
|
455
|
+
}
|
|
456
|
+
return Object.keys(mapped).length > 0 ? mapped : void 0;
|
|
457
|
+
}
|
|
307
458
|
function resolveSpaLessons(descriptor) {
|
|
308
459
|
const mapped = mapLessonkitIds(descriptor);
|
|
309
460
|
if (descriptor.layout === "single-spa") {
|
|
@@ -341,7 +492,7 @@ function descriptorToInterchange(descriptor) {
|
|
|
341
492
|
type: "spa",
|
|
342
493
|
path: l.path
|
|
343
494
|
})),
|
|
344
|
-
tracking: descriptor.tracking,
|
|
495
|
+
tracking: mapDescriptorTracking(descriptor.tracking),
|
|
345
496
|
runtime: runtime ? {
|
|
346
497
|
theme: runtime.theme,
|
|
347
498
|
cssVariables: runtime.cssVariables
|
|
@@ -351,12 +502,12 @@ function descriptorToInterchange(descriptor) {
|
|
|
351
502
|
}
|
|
352
503
|
|
|
353
504
|
// src/writeProject.ts
|
|
354
|
-
import { join, resolve as resolve4 } from "path";
|
|
505
|
+
import { join as join2, resolve as resolve4 } from "path";
|
|
355
506
|
import { materializeLessonkitProject } from "@lxpack/validators";
|
|
356
507
|
|
|
357
508
|
// src/spaDirs.ts
|
|
358
509
|
import { access } from "fs/promises";
|
|
359
|
-
import { resolve as resolve3 } from "path";
|
|
510
|
+
import { join, resolve as resolve3 } from "path";
|
|
360
511
|
async function resolveSpaDirs(options) {
|
|
361
512
|
const { descriptor, spaDistDir, lessonSpaDirs, projectRoot } = options;
|
|
362
513
|
const spaLessons = resolveSpaLessons(descriptor);
|
|
@@ -364,13 +515,18 @@ async function resolveSpaDirs(options) {
|
|
|
364
515
|
const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? "dist";
|
|
365
516
|
const srcDist = projectRoot ? resolve3(projectRoot, spaDistRelative) : resolve3(spaDistRelative);
|
|
366
517
|
if (projectRoot) {
|
|
367
|
-
|
|
518
|
+
assertRealPathUnderRoot(resolve3(projectRoot), srcDist);
|
|
368
519
|
}
|
|
369
520
|
try {
|
|
370
521
|
await access(srcDist);
|
|
371
522
|
} catch {
|
|
372
523
|
throw new Error(`spaDistDir not found: ${srcDist}`);
|
|
373
524
|
}
|
|
525
|
+
try {
|
|
526
|
+
await access(join(srcDist, "index.html"));
|
|
527
|
+
} catch {
|
|
528
|
+
throw new Error(`spaDistDir must contain index.html: ${join(srcDist, "index.html")}`);
|
|
529
|
+
}
|
|
374
530
|
const lessonId = spaLessons[0]?.id ?? "main";
|
|
375
531
|
return { [lessonId]: srcDist };
|
|
376
532
|
}
|
|
@@ -383,7 +539,19 @@ async function resolveSpaDirs(options) {
|
|
|
383
539
|
}
|
|
384
540
|
const resolved = projectRoot ? resolve3(projectRoot, src) : resolve3(src);
|
|
385
541
|
if (projectRoot) {
|
|
386
|
-
|
|
542
|
+
assertRealPathUnderRoot(resolve3(projectRoot), resolved);
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
await access(resolved);
|
|
546
|
+
} catch {
|
|
547
|
+
throw new Error(`lessonSpaDirs path not found for lesson "${lesson.id}": ${resolved}`);
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
await access(join(resolved, "index.html"));
|
|
551
|
+
} catch {
|
|
552
|
+
throw new Error(
|
|
553
|
+
`lessonSpaDirs must contain index.html for lesson "${lesson.id}": ${join(resolved, "index.html")}`
|
|
554
|
+
);
|
|
387
555
|
}
|
|
388
556
|
dirs[lesson.id] = resolved;
|
|
389
557
|
}
|
|
@@ -401,7 +569,7 @@ async function writeLxpackProject(options) {
|
|
|
401
569
|
const descriptor = validation.descriptor;
|
|
402
570
|
const outDir = resolve4(options.outDir);
|
|
403
571
|
if (options.projectRoot) {
|
|
404
|
-
|
|
572
|
+
assertRealPathUnderRoot(resolve4(options.projectRoot), outDir);
|
|
405
573
|
}
|
|
406
574
|
const spaDirs = await resolveSpaDirs({ ...options, descriptor });
|
|
407
575
|
const interchange = descriptorToInterchange(descriptor);
|
|
@@ -419,78 +587,36 @@ async function writeLxpackProject(options) {
|
|
|
419
587
|
const courseDir = materialized.courseDir;
|
|
420
588
|
return {
|
|
421
589
|
outDir: courseDir,
|
|
422
|
-
courseYamlPath:
|
|
423
|
-
lessonkitJsonPath:
|
|
590
|
+
courseYamlPath: join2(courseDir, "course.yaml"),
|
|
591
|
+
lessonkitJsonPath: join2(courseDir, "lessonkit.json")
|
|
424
592
|
};
|
|
425
593
|
}
|
|
426
594
|
|
|
427
595
|
// src/packageCourse.ts
|
|
428
|
-
import
|
|
429
|
-
import
|
|
430
|
-
import { tmpdir } from "os";
|
|
596
|
+
import { resolve as resolve6 } from "path";
|
|
597
|
+
import * as fsp3 from "fs/promises";
|
|
431
598
|
import {
|
|
432
599
|
buildCourse,
|
|
433
|
-
packageLessonkit,
|
|
434
600
|
validateCourse
|
|
435
601
|
} from "@lxpack/api";
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return buildCourse({
|
|
444
|
-
courseDir: resolve5(options.courseDir),
|
|
445
|
-
target: options.target,
|
|
446
|
-
output: options.output,
|
|
447
|
-
dir: options.dir,
|
|
448
|
-
outputBaseDir: options.outputBaseDir,
|
|
449
|
-
assessments: options.assessments
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
async function pathExists(path) {
|
|
453
|
-
try {
|
|
454
|
-
await fsp.access(path);
|
|
455
|
-
return true;
|
|
456
|
-
} catch {
|
|
457
|
-
return false;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
461
|
-
const tmpPromote = `${outDir}.tmp-promote`;
|
|
462
|
-
const backup = `${outDir}.bak`;
|
|
463
|
-
await fsp.rename(stagingDir, tmpPromote);
|
|
464
|
-
const hadOutDir = await pathExists(outDir);
|
|
465
|
-
if (hadOutDir) {
|
|
466
|
-
await fsp.rename(outDir, backup);
|
|
467
|
-
}
|
|
468
|
-
try {
|
|
469
|
-
await fsp.rename(tmpPromote, outDir);
|
|
470
|
-
} catch (promoteError) {
|
|
471
|
-
if (hadOutDir) {
|
|
472
|
-
try {
|
|
473
|
-
await fsp.rename(backup, outDir);
|
|
474
|
-
} catch (restoreError) {
|
|
475
|
-
console.warn(
|
|
476
|
-
`[lessonkit/lxpack] failed to restore ${outDir} after promote error:`,
|
|
477
|
-
restoreError instanceof Error ? restoreError.message : restoreError
|
|
478
|
-
);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
482
|
-
throw promoteError;
|
|
483
|
-
}
|
|
484
|
-
if (hadOutDir) {
|
|
485
|
-
await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
async function packageLessonkitCourse(options) {
|
|
489
|
-
const { target, output, dir, outputBaseDir, ...writeOpts } = options;
|
|
490
|
-
const outDir = resolve5(writeOpts.outDir);
|
|
491
|
-
const projectRoot = writeOpts.projectRoot ? resolve5(writeOpts.projectRoot) : void 0;
|
|
602
|
+
|
|
603
|
+
// src/packaging/validateInputs.ts
|
|
604
|
+
import { isAbsolute as isAbsolute3, join as join3, resolve as resolve5, win32 as win322 } from "path";
|
|
605
|
+
function validatePackageInputs(options) {
|
|
606
|
+
const { target, output, outputBaseDir } = options;
|
|
607
|
+
const outDir = resolve5(options.outDir);
|
|
608
|
+
const projectRoot = options.projectRoot ? resolve5(options.projectRoot) : void 0;
|
|
492
609
|
if (projectRoot) {
|
|
493
|
-
|
|
610
|
+
try {
|
|
611
|
+
assertRealPathUnderRoot(projectRoot, outDir);
|
|
612
|
+
} catch (err) {
|
|
613
|
+
return {
|
|
614
|
+
ok: false,
|
|
615
|
+
courseDir: outDir,
|
|
616
|
+
target,
|
|
617
|
+
issues: [{ path: "outDir", message: err instanceof Error ? err.message : String(err) }]
|
|
618
|
+
};
|
|
619
|
+
}
|
|
494
620
|
}
|
|
495
621
|
if (outputBaseDir && !isSafeRelativeSpaPath(outputBaseDir)) {
|
|
496
622
|
return {
|
|
@@ -500,10 +626,18 @@ async function packageLessonkitCourse(options) {
|
|
|
500
626
|
issues: [{ path: "outputBaseDir", message: `unsafe outputBaseDir: ${outputBaseDir}` }]
|
|
501
627
|
};
|
|
502
628
|
}
|
|
503
|
-
if (projectRoot && output) {
|
|
504
|
-
|
|
629
|
+
if (output && !projectRoot && !isSafeRelativeSpaPath(output)) {
|
|
630
|
+
return {
|
|
631
|
+
ok: false,
|
|
632
|
+
courseDir: outDir,
|
|
633
|
+
target,
|
|
634
|
+
issues: [{ path: "output", message: `unsafe output: ${output}` }]
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
if (projectRoot && outputBaseDir) {
|
|
638
|
+
const resolvedOutputBase = resolve5(projectRoot, outputBaseDir);
|
|
505
639
|
try {
|
|
506
|
-
|
|
640
|
+
assertRealPathUnderRoot(projectRoot, resolvedOutputBase);
|
|
507
641
|
} catch (err) {
|
|
508
642
|
return {
|
|
509
643
|
ok: false,
|
|
@@ -511,28 +645,152 @@ async function packageLessonkitCourse(options) {
|
|
|
511
645
|
target,
|
|
512
646
|
issues: [
|
|
513
647
|
{
|
|
514
|
-
path: "
|
|
648
|
+
path: "outputBaseDir",
|
|
515
649
|
message: err instanceof Error ? err.message : String(err)
|
|
516
650
|
}
|
|
517
651
|
]
|
|
518
652
|
};
|
|
519
653
|
}
|
|
520
654
|
}
|
|
521
|
-
|
|
522
|
-
|
|
655
|
+
if (projectRoot && output) {
|
|
656
|
+
const resolvedOutput = resolve5(projectRoot, output);
|
|
657
|
+
try {
|
|
658
|
+
assertRealPathUnderRoot(projectRoot, resolvedOutput);
|
|
659
|
+
} catch (err) {
|
|
660
|
+
return {
|
|
661
|
+
ok: false,
|
|
662
|
+
courseDir: outDir,
|
|
663
|
+
target,
|
|
664
|
+
issues: [{ path: "output", message: err instanceof Error ? err.message : String(err) }]
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return { ok: true, outDir, projectRoot };
|
|
669
|
+
}
|
|
670
|
+
function validateArtifactInStaging(stagingRoot, artifactPath, field) {
|
|
671
|
+
if (!artifactPath) return null;
|
|
672
|
+
const resolved = resolveComparablePath(artifactPath);
|
|
673
|
+
if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
|
|
523
674
|
return {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
target,
|
|
527
|
-
issues: descriptorValidation.issues.map((i) => ({
|
|
528
|
-
path: i.path,
|
|
529
|
-
message: i.message
|
|
530
|
-
}))
|
|
675
|
+
path: field,
|
|
676
|
+
message: `${field} is outside the staging directory: ${artifactPath}`
|
|
531
677
|
};
|
|
532
678
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
|
|
682
|
+
if (!artifactPath) return void 0;
|
|
683
|
+
const resolved = resolveComparablePath(artifactPath);
|
|
684
|
+
if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
|
|
685
|
+
return artifactPath;
|
|
686
|
+
}
|
|
687
|
+
const rel = relativePathUnderRoot(stagingRoot, resolved);
|
|
688
|
+
if (rel.startsWith("..") || isAbsolute3(rel)) {
|
|
689
|
+
return artifactPath;
|
|
690
|
+
}
|
|
691
|
+
if (!rel) return outDir;
|
|
692
|
+
if (/^[a-zA-Z]:[/\\]/.test(outDir)) {
|
|
693
|
+
return win322.join(outDir, rel.replace(/\//g, win322.sep));
|
|
694
|
+
}
|
|
695
|
+
return join3(outDir, rel);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/packaging/promote.ts
|
|
699
|
+
import * as fsp from "fs/promises";
|
|
700
|
+
import { randomUUID } from "crypto";
|
|
701
|
+
import { dirname, join as join4 } from "path";
|
|
702
|
+
async function pathExists(path) {
|
|
703
|
+
try {
|
|
704
|
+
await fsp.access(path);
|
|
705
|
+
return true;
|
|
706
|
+
} catch {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
async function renameOrCopy(from, to) {
|
|
711
|
+
try {
|
|
712
|
+
await fsp.rename(from, to);
|
|
713
|
+
} catch (err) {
|
|
714
|
+
const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
|
|
715
|
+
if (code !== "EXDEV") throw err;
|
|
716
|
+
await fsp.cp(from, to, { recursive: true });
|
|
717
|
+
await fsp.rm(from, { recursive: true, force: true });
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function assertNoLegacyPromoteArtifacts(outDir) {
|
|
721
|
+
const legacyTmp = `${outDir}.tmp-promote`;
|
|
722
|
+
const legacyBak = `${outDir}.bak`;
|
|
723
|
+
const stale = [];
|
|
724
|
+
if (await pathExists(legacyTmp)) stale.push(legacyTmp);
|
|
725
|
+
if (await pathExists(legacyBak)) stale.push(legacyBak);
|
|
726
|
+
if (stale.length) {
|
|
727
|
+
throw new Error(
|
|
728
|
+
`[lessonkit/lxpack] cannot promote: remove stale packaging artifacts from a previous failed run: ${stale.join(", ")}`
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
733
|
+
await assertNoLegacyPromoteArtifacts(outDir);
|
|
734
|
+
const parent = dirname(outDir);
|
|
735
|
+
const tmpPromote = await fsp.mkdtemp(join4(parent, ".lk-promote-"));
|
|
736
|
+
await renameOrCopy(stagingDir, tmpPromote);
|
|
737
|
+
const hadOutDir = await pathExists(outDir);
|
|
738
|
+
const backup = hadOutDir ? await fsp.mkdtemp(join4(parent, ".lk-backup-")) : void 0;
|
|
739
|
+
if (hadOutDir && backup) {
|
|
740
|
+
await renameOrCopy(outDir, backup);
|
|
741
|
+
}
|
|
742
|
+
try {
|
|
743
|
+
await renameOrCopy(tmpPromote, outDir);
|
|
744
|
+
} catch (promoteError) {
|
|
745
|
+
if (hadOutDir && backup) {
|
|
746
|
+
try {
|
|
747
|
+
await renameOrCopy(backup, outDir);
|
|
748
|
+
} catch (restoreError) {
|
|
749
|
+
const failedPromote2 = join4(parent, `.lk-failed-promote-${randomUUID()}`);
|
|
750
|
+
try {
|
|
751
|
+
await renameOrCopy(tmpPromote, failedPromote2);
|
|
752
|
+
} catch {
|
|
753
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
754
|
+
}
|
|
755
|
+
const promoteMsg = promoteError instanceof Error ? promoteError.message : String(promoteError);
|
|
756
|
+
const restoreMsg = restoreError instanceof Error ? restoreError.message : String(restoreError);
|
|
757
|
+
throw new Error(
|
|
758
|
+
`[lessonkit/lxpack] promote failed (${promoteMsg}) and could not restore ${outDir} (${restoreMsg}). Recovery: previous output may be in ${backup}; staged package may be in ${failedPromote2}.`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
} else {
|
|
762
|
+
try {
|
|
763
|
+
await renameOrCopy(tmpPromote, stagingDir);
|
|
764
|
+
} catch (restoreError) {
|
|
765
|
+
console.warn(
|
|
766
|
+
`[lessonkit/lxpack] failed to restore ${stagingDir} after promote error:`,
|
|
767
|
+
restoreError instanceof Error ? restoreError.message : restoreError
|
|
768
|
+
);
|
|
769
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
770
|
+
}
|
|
771
|
+
throw promoteError;
|
|
772
|
+
}
|
|
773
|
+
const failedPromote = join4(parent, `.lk-failed-promote-${randomUUID()}`);
|
|
774
|
+
try {
|
|
775
|
+
await renameOrCopy(tmpPromote, failedPromote);
|
|
776
|
+
} catch {
|
|
777
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
778
|
+
}
|
|
779
|
+
throw promoteError;
|
|
780
|
+
}
|
|
781
|
+
if (backup) {
|
|
782
|
+
await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/packaging/staging.ts
|
|
787
|
+
import * as fsp2 from "fs/promises";
|
|
788
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
789
|
+
import { tmpdir } from "os";
|
|
790
|
+
import { packageLessonkit } from "@lxpack/api";
|
|
791
|
+
async function buildStagingPackage(options) {
|
|
792
|
+
const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
|
|
793
|
+
const stagingDir = await fsp2.mkdtemp(join5(tmpdir(), "lessonkit-lxpack-"));
|
|
536
794
|
try {
|
|
537
795
|
let spaDirs;
|
|
538
796
|
try {
|
|
@@ -540,8 +798,7 @@ async function packageLessonkitCourse(options) {
|
|
|
540
798
|
} catch (err) {
|
|
541
799
|
return {
|
|
542
800
|
ok: false,
|
|
543
|
-
|
|
544
|
-
target,
|
|
801
|
+
stagingDir,
|
|
545
802
|
issues: [
|
|
546
803
|
{
|
|
547
804
|
path: "spaDirs",
|
|
@@ -552,8 +809,8 @@ async function packageLessonkitCourse(options) {
|
|
|
552
809
|
}
|
|
553
810
|
const interchange = descriptorToInterchange(descriptor);
|
|
554
811
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
555
|
-
await
|
|
556
|
-
const defaultOutput = output ?? (dir ?
|
|
812
|
+
await fsp2.mkdir(join5(stagingDir, outputBase), { recursive: true });
|
|
813
|
+
const defaultOutput = output ?? (dir ? join5(outputBase, target) : join5(outputBase, `course-${target}.zip`));
|
|
557
814
|
const build = await packageLessonkit({
|
|
558
815
|
interchange,
|
|
559
816
|
spaDirs,
|
|
@@ -566,15 +823,9 @@ async function packageLessonkitCourse(options) {
|
|
|
566
823
|
writeAuthoringFiles: true
|
|
567
824
|
});
|
|
568
825
|
if (!build.ok) {
|
|
569
|
-
const validation2 = {
|
|
570
|
-
ok: false,
|
|
571
|
-
issues: build.issues
|
|
572
|
-
};
|
|
573
826
|
return {
|
|
574
827
|
ok: false,
|
|
575
|
-
|
|
576
|
-
target,
|
|
577
|
-
validation: validation2,
|
|
828
|
+
stagingDir,
|
|
578
829
|
build,
|
|
579
830
|
issues: build.issues.map((i) => ({
|
|
580
831
|
path: i.path,
|
|
@@ -583,48 +834,256 @@ async function packageLessonkitCourse(options) {
|
|
|
583
834
|
}))
|
|
584
835
|
};
|
|
585
836
|
}
|
|
586
|
-
|
|
837
|
+
return {
|
|
587
838
|
ok: true,
|
|
588
|
-
|
|
589
|
-
|
|
839
|
+
stagingDir,
|
|
840
|
+
build,
|
|
841
|
+
outputPath: "outputPath" in build ? build.outputPath : void 0,
|
|
842
|
+
outputDir: "outputDir" in build ? build.outputDir : void 0
|
|
590
843
|
};
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
844
|
+
} catch (err) {
|
|
845
|
+
await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
846
|
+
throw err;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async function ensureOutDirParent(outDir) {
|
|
850
|
+
await fsp2.mkdir(dirname2(outDir), { recursive: true });
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/packageCourse.ts
|
|
854
|
+
async function validateLessonkitProject(options) {
|
|
855
|
+
return validateCourse({
|
|
856
|
+
courseDir: resolve6(options.courseDir),
|
|
857
|
+
target: options.target
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
async function buildLessonkitProject(options) {
|
|
861
|
+
const buildOptions = {
|
|
862
|
+
courseDir: resolve6(options.courseDir),
|
|
863
|
+
target: options.target,
|
|
864
|
+
output: options.output,
|
|
865
|
+
dir: options.dir,
|
|
866
|
+
outputBaseDir: options.outputBaseDir,
|
|
867
|
+
assessments: options.assessments
|
|
868
|
+
};
|
|
869
|
+
return buildCourse(buildOptions);
|
|
870
|
+
}
|
|
871
|
+
async function packageLessonkitCourse(options) {
|
|
872
|
+
const { target, output, dir, outputBaseDir, ...writeOpts } = options;
|
|
873
|
+
const inputValidation = validatePackageInputs({
|
|
874
|
+
target,
|
|
875
|
+
output,
|
|
876
|
+
outputBaseDir,
|
|
877
|
+
outDir: writeOpts.outDir,
|
|
878
|
+
projectRoot: writeOpts.projectRoot
|
|
879
|
+
});
|
|
880
|
+
if (!inputValidation.ok) {
|
|
881
|
+
return {
|
|
882
|
+
ok: false,
|
|
883
|
+
courseDir: inputValidation.courseDir,
|
|
884
|
+
target: inputValidation.target,
|
|
885
|
+
issues: inputValidation.issues
|
|
599
886
|
};
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
887
|
+
}
|
|
888
|
+
const outDir = inputValidation.outDir;
|
|
889
|
+
const descriptorValidation = validateDescriptorForTarget(writeOpts.descriptor, target);
|
|
890
|
+
if (!descriptorValidation.ok) {
|
|
891
|
+
return {
|
|
892
|
+
ok: false,
|
|
893
|
+
courseDir: outDir,
|
|
894
|
+
target,
|
|
895
|
+
issues: descriptorValidation.issues.map((i) => ({
|
|
896
|
+
path: i.path,
|
|
897
|
+
message: i.message
|
|
898
|
+
}))
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
const descriptor = descriptorValidation.descriptor;
|
|
902
|
+
const staged = await buildStagingPackage({
|
|
903
|
+
...writeOpts,
|
|
904
|
+
descriptor,
|
|
905
|
+
target,
|
|
906
|
+
output,
|
|
907
|
+
dir,
|
|
908
|
+
outputBaseDir
|
|
909
|
+
});
|
|
910
|
+
if (!staged.ok) {
|
|
911
|
+
await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
912
|
+
const validation2 = staged.build ? { ok: false, issues: staged.build.issues } : void 0;
|
|
913
|
+
return {
|
|
914
|
+
ok: false,
|
|
915
|
+
courseDir: outDir,
|
|
916
|
+
target,
|
|
917
|
+
validation: validation2,
|
|
918
|
+
build: staged.build,
|
|
919
|
+
issues: staged.issues
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
const { stagingDir, build } = staged;
|
|
923
|
+
const stagingRoot = await fsp3.realpath(stagingDir);
|
|
924
|
+
const artifactIssues = [
|
|
925
|
+
validateArtifactInStaging(stagingRoot, staged.outputPath, "outputPath"),
|
|
926
|
+
validateArtifactInStaging(stagingRoot, staged.outputDir, "outputDir")
|
|
927
|
+
].filter((issue) => issue != null);
|
|
928
|
+
if (artifactIssues.length > 0) {
|
|
929
|
+
await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
930
|
+
return {
|
|
931
|
+
ok: false,
|
|
932
|
+
courseDir: outDir,
|
|
933
|
+
target,
|
|
934
|
+
validation: { ok: true, manifest: build.manifest, issues: build.issues },
|
|
935
|
+
build,
|
|
936
|
+
issues: artifactIssues
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
const remappedOutputPath = remapArtifactPaths(stagingRoot, outDir, staged.outputPath);
|
|
940
|
+
const remappedOutputDir = remapArtifactPaths(stagingRoot, outDir, staged.outputDir);
|
|
941
|
+
const validation = {
|
|
942
|
+
ok: true,
|
|
943
|
+
manifest: build.manifest,
|
|
944
|
+
issues: build.issues
|
|
945
|
+
};
|
|
946
|
+
try {
|
|
947
|
+
await ensureOutDirParent(outDir);
|
|
605
948
|
await promoteStagingToOutDir(stagingDir, outDir);
|
|
606
|
-
|
|
607
|
-
const remappedBuild = { ...build };
|
|
608
|
-
if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
|
|
609
|
-
remappedBuild.outputPath = remappedOutputPath;
|
|
610
|
-
}
|
|
611
|
-
if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
|
|
612
|
-
remappedBuild.outputDir = remappedOutputDir;
|
|
613
|
-
}
|
|
949
|
+
} catch (err) {
|
|
614
950
|
return {
|
|
615
|
-
ok:
|
|
951
|
+
ok: false,
|
|
616
952
|
courseDir: outDir,
|
|
617
953
|
target,
|
|
618
|
-
outputPath: remappedOutputPath,
|
|
619
|
-
outputDir: remappedOutputDir,
|
|
620
|
-
fileCount: build.fileCount,
|
|
621
954
|
validation,
|
|
622
|
-
build
|
|
955
|
+
build,
|
|
956
|
+
issues: [
|
|
957
|
+
{
|
|
958
|
+
path: "promote",
|
|
959
|
+
message: err instanceof Error ? err.message : String(err)
|
|
960
|
+
}
|
|
961
|
+
]
|
|
623
962
|
};
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
|
|
963
|
+
}
|
|
964
|
+
const remappedBuild = { ...build };
|
|
965
|
+
if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
|
|
966
|
+
remappedBuild.outputPath = remappedOutputPath;
|
|
967
|
+
}
|
|
968
|
+
if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
|
|
969
|
+
remappedBuild.outputDir = remappedOutputDir;
|
|
970
|
+
}
|
|
971
|
+
return {
|
|
972
|
+
ok: true,
|
|
973
|
+
courseDir: outDir,
|
|
974
|
+
target,
|
|
975
|
+
outputPath: remappedOutputPath,
|
|
976
|
+
outputDir: remappedOutputDir,
|
|
977
|
+
fileCount: build.fileCount,
|
|
978
|
+
validation,
|
|
979
|
+
build: remappedBuild
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/manifest.ts
|
|
984
|
+
var DEFAULT_PATHS = {
|
|
985
|
+
spaDistDir: "dist",
|
|
986
|
+
lxpackOutDir: ".lxpack/course",
|
|
987
|
+
outputBaseDir: ".lxpack/out"
|
|
988
|
+
};
|
|
989
|
+
function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
|
|
990
|
+
if (!raw || typeof raw !== "object") {
|
|
991
|
+
return { ok: false, issues: [{ path: label, message: "must be a JSON object" }] };
|
|
992
|
+
}
|
|
993
|
+
const config = raw;
|
|
994
|
+
const issues = [];
|
|
995
|
+
let schemaVersion = config.schemaVersion;
|
|
996
|
+
if (schemaVersion === "1") {
|
|
997
|
+
schemaVersion = 1;
|
|
998
|
+
}
|
|
999
|
+
if (schemaVersion !== 1) {
|
|
1000
|
+
issues.push({
|
|
1001
|
+
path: "schemaVersion",
|
|
1002
|
+
message: `must be 1 (got ${String(config.schemaVersion)})`
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
const nameRaw = config.name;
|
|
1006
|
+
const name = typeof nameRaw === "string" ? nameRaw.trim() : "";
|
|
1007
|
+
if (!name) {
|
|
1008
|
+
issues.push({ path: "name", message: "must be a non-empty string" });
|
|
1009
|
+
}
|
|
1010
|
+
const courseRaw = config.course;
|
|
1011
|
+
if (Array.isArray(courseRaw)) {
|
|
1012
|
+
issues.push({ path: "course", message: "must be an object, not an array" });
|
|
1013
|
+
return { ok: false, issues };
|
|
1014
|
+
}
|
|
1015
|
+
if (!courseRaw || typeof courseRaw !== "object") {
|
|
1016
|
+
issues.push({ path: "course", message: "must be an object" });
|
|
1017
|
+
return { ok: false, issues };
|
|
1018
|
+
}
|
|
1019
|
+
const courseObj = courseRaw;
|
|
1020
|
+
if (courseObj.lessons !== void 0 && !Array.isArray(courseObj.lessons)) {
|
|
1021
|
+
issues.push({ path: "course.lessons", message: "must be an array" });
|
|
1022
|
+
}
|
|
1023
|
+
if (courseObj.assessments !== void 0 && !Array.isArray(courseObj.assessments)) {
|
|
1024
|
+
issues.push({ path: "course.assessments", message: "must be an array" });
|
|
1025
|
+
}
|
|
1026
|
+
if (issues.length) return { ok: false, issues };
|
|
1027
|
+
const validation = validateDescriptor(courseRaw);
|
|
1028
|
+
if (!validation.ok) {
|
|
1029
|
+
for (const i of validation.issues) {
|
|
1030
|
+
issues.push({
|
|
1031
|
+
path: i.path.startsWith("course.") ? i.path : `course.${i.path}`,
|
|
1032
|
+
message: i.message
|
|
1033
|
+
});
|
|
627
1034
|
}
|
|
1035
|
+
} else if (validation.descriptor.layout === "per-lesson-spa") {
|
|
1036
|
+
issues.push({
|
|
1037
|
+
path: "course.layout",
|
|
1038
|
+
message: "per-lesson-spa is not supported by lessonkit package yet. Use single-spa or package via @lessonkit/lxpack directly."
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
const paths = { ...DEFAULT_PATHS };
|
|
1042
|
+
const pathsRaw = config.paths;
|
|
1043
|
+
if (pathsRaw !== void 0 && (typeof pathsRaw !== "object" || pathsRaw === null)) {
|
|
1044
|
+
issues.push({ path: "paths", message: "must be an object" });
|
|
1045
|
+
} else if (pathsRaw && typeof pathsRaw === "object") {
|
|
1046
|
+
const p = pathsRaw;
|
|
1047
|
+
for (const key of ["spaDistDir", "lxpackOutDir", "outputBaseDir"]) {
|
|
1048
|
+
if (p[key] !== void 0) {
|
|
1049
|
+
if (typeof p[key] !== "string" || !p[key].trim()) {
|
|
1050
|
+
issues.push({ path: `paths.${key}`, message: "must be a non-empty string" });
|
|
1051
|
+
} else {
|
|
1052
|
+
paths[key] = p[key].trim();
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const courseSpaDistDir = validation.ok ? validation.descriptor.spaDistDir?.trim() : void 0;
|
|
1058
|
+
if (courseSpaDistDir && courseSpaDistDir !== paths.spaDistDir) {
|
|
1059
|
+
issues.push({
|
|
1060
|
+
path: "course.spaDistDir",
|
|
1061
|
+
message: `"course.spaDistDir" (${courseSpaDistDir}) differs from "paths.spaDistDir" (${paths.spaDistDir}). Use paths.spaDistDir for CLI build and package.`
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
if (projectRoot) {
|
|
1065
|
+
const pathIssues = validateProjectPaths(projectRoot, paths);
|
|
1066
|
+
for (const pi of pathIssues) {
|
|
1067
|
+
issues.push({ path: pi.path, message: pi.message });
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (issues.length) return { ok: false, issues };
|
|
1071
|
+
if (!validation.ok) return { ok: false, issues };
|
|
1072
|
+
return {
|
|
1073
|
+
ok: true,
|
|
1074
|
+
manifest: {
|
|
1075
|
+
schemaVersion: 1,
|
|
1076
|
+
name,
|
|
1077
|
+
course: validation.descriptor,
|
|
1078
|
+
paths
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
async function loadLessonkitManifestFromFile(readJson, label = "lessonkit.json", projectRoot) {
|
|
1083
|
+
try {
|
|
1084
|
+
return parseLessonkitManifest(await readJson(), label, projectRoot);
|
|
1085
|
+
} catch {
|
|
1086
|
+
return { ok: false, issues: [{ path: label, message: "failed to read or parse JSON" }] };
|
|
628
1087
|
}
|
|
629
1088
|
}
|
|
630
1089
|
|
|
@@ -643,21 +1102,29 @@ export {
|
|
|
643
1102
|
LESSONKIT_TELEMETRY_EVENTS,
|
|
644
1103
|
assessmentDescriptorToLxpack,
|
|
645
1104
|
buildLessonkitProject,
|
|
1105
|
+
buildStagingPackage,
|
|
646
1106
|
descriptorToInterchange,
|
|
1107
|
+
ensureOutDirParent,
|
|
647
1108
|
extractAssessments,
|
|
648
1109
|
lessonkitInterchangeSchema,
|
|
1110
|
+
loadLessonkitManifestFromFile,
|
|
649
1111
|
mapLessonkitIds,
|
|
650
1112
|
mapLessonkitTelemetryToBridgeAction,
|
|
651
1113
|
mapLessonkitTelemetryToLxpack,
|
|
652
1114
|
materializeLessonkitProject2 as materializeLessonkitProject,
|
|
653
1115
|
packageLessonkitCourse,
|
|
654
1116
|
parseLessonkitInterchange,
|
|
1117
|
+
parseLessonkitManifest,
|
|
1118
|
+
promoteStagingToOutDir,
|
|
1119
|
+
remapArtifactPaths,
|
|
655
1120
|
resolveSafePackageOutputOverride,
|
|
656
1121
|
resolveSpaLessons,
|
|
657
1122
|
telemetryEventToLessonkit,
|
|
658
1123
|
themeToLxpackRuntime,
|
|
659
1124
|
validateDescriptor,
|
|
1125
|
+
validateDescriptorForTarget,
|
|
660
1126
|
validateLessonkitProject,
|
|
1127
|
+
validatePackageInputs,
|
|
661
1128
|
validateProjectPaths,
|
|
662
1129
|
writeLxpackProject
|
|
663
1130
|
};
|