@lessonkit/lxpack 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge.cjs +18 -9
- package/dist/bridge.js +2 -2
- package/dist/chunk-DYQI222N.js +41 -0
- package/dist/index.cjs +199 -66
- package/dist/index.d.cts +12 -7
- package/dist/index.d.ts +12 -7
- package/dist/index.js +183 -60
- package/lessonkit-manifest.v1.json +100 -0
- package/package.json +8 -6
- package/dist/chunk-PSUSESH3.js +0 -32
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { CheckId, CourseId, LessonId } from '@lessonkit/core';
|
|
2
2
|
import { ThemePresetName, LessonkitThemeV1 } from '@lessonkit/themes';
|
|
3
|
-
import { LessonkitInterchangeV1 } from '@lxpack/validators';
|
|
4
|
-
export { LessonkitInterchangeV1, MaterializeLessonkitOptions, MaterializeLessonkitResult, lessonkitInterchangeSchema, materializeLessonkitProject, parseLessonkitInterchange } from '@lxpack/validators';
|
|
5
3
|
import { ExportTarget, BuildCourseResult, ValidateCourseResult } from '@lxpack/api';
|
|
6
4
|
export { ExportTarget } from '@lxpack/api';
|
|
5
|
+
import { LessonkitInterchangeV1 } from '@lxpack/validators';
|
|
6
|
+
export { LessonkitInterchangeV1, MaterializeLessonkitOptions, MaterializeLessonkitResult, lessonkitInterchangeSchema, materializeLessonkitProject, parseLessonkitInterchange } from '@lxpack/validators';
|
|
7
7
|
export { LESSONKIT_TELEMETRY_EVENTS, LessonkitBridgeAction, LessonkitTelemetryEvent, LessonkitTelemetryEventName, TrackingSchemaEvent, mapLessonkitTelemetryToBridgeAction, mapLessonkitTelemetryToLxpack } from '@lxpack/tracking-schema';
|
|
8
8
|
export { t as telemetryEventToLessonkit } from './telemetry-gCxlwc7I.js';
|
|
9
9
|
|
|
@@ -53,10 +53,14 @@ type MappedLessonkitIds = {
|
|
|
53
53
|
checkIds: CheckId[];
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
/** Shared validation issue shape across lxpack parsers. */
|
|
57
|
+
type ValidationIssue = {
|
|
57
58
|
path: string;
|
|
58
59
|
message: string;
|
|
60
|
+
severity?: string;
|
|
59
61
|
};
|
|
62
|
+
|
|
63
|
+
type DescriptorValidationIssue = ValidationIssue;
|
|
60
64
|
type DescriptorValidationResult = {
|
|
61
65
|
ok: true;
|
|
62
66
|
descriptor: LessonkitCourseDescriptor;
|
|
@@ -64,7 +68,8 @@ type DescriptorValidationResult = {
|
|
|
64
68
|
ok: false;
|
|
65
69
|
issues: DescriptorValidationIssue[];
|
|
66
70
|
};
|
|
67
|
-
declare function validateDescriptor(input:
|
|
71
|
+
declare function validateDescriptor(input: unknown): DescriptorValidationResult;
|
|
72
|
+
declare function validateDescriptorForTarget(input: unknown, target?: ExportTarget): DescriptorValidationResult;
|
|
68
73
|
|
|
69
74
|
type ProjectPathsInput = {
|
|
70
75
|
spaDistDir?: string;
|
|
@@ -94,7 +99,7 @@ declare function themeToLxpackRuntime(input: {
|
|
|
94
99
|
}): LxpackRuntimeTheme;
|
|
95
100
|
|
|
96
101
|
type SpaLessonEntry = {
|
|
97
|
-
id:
|
|
102
|
+
id: LessonId;
|
|
98
103
|
title: string;
|
|
99
104
|
path: string;
|
|
100
105
|
};
|
|
@@ -188,7 +193,7 @@ type BuildLessonkitProjectOptions = {
|
|
|
188
193
|
output?: string;
|
|
189
194
|
dir?: boolean;
|
|
190
195
|
outputBaseDir?: string;
|
|
191
|
-
assessments?:
|
|
196
|
+
assessments?: LxpackInjectedAssessment[];
|
|
192
197
|
};
|
|
193
198
|
type PackageLessonkitCourseOptions = WriteLxpackProjectOptions & {
|
|
194
199
|
target: ExportTarget;
|
|
@@ -268,4 +273,4 @@ type ParseManifestResult = {
|
|
|
268
273
|
declare function parseLessonkitManifest(raw: unknown, label?: string, projectRoot?: string): ParseManifestResult;
|
|
269
274
|
declare function loadLessonkitManifestFromFile(readJson: () => Promise<unknown>, label?: string, projectRoot?: string): Promise<ParseManifestResult>;
|
|
270
275
|
|
|
271
|
-
export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitManifest, type LessonkitManifestPaths, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type ManifestParseIssue, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type PackageValidationIssue, type ParseManifestResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type ValidatePackageInputsResult, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, buildStagingPackage, descriptorToInterchange, ensureOutDirParent, extractAssessments, loadLessonkitManifestFromFile, mapLessonkitIds, packageLessonkitCourse, parseLessonkitManifest, promoteStagingToOutDir, remapArtifactPaths, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateLessonkitProject, validatePackageInputs, validateProjectPaths, writeLxpackProject };
|
|
276
|
+
export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitManifest, type LessonkitManifestPaths, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type ManifestParseIssue, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type PackageValidationIssue, type ParseManifestResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type ValidatePackageInputsResult, type ValidationIssue, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, buildStagingPackage, descriptorToInterchange, ensureOutDirParent, extractAssessments, loadLessonkitManifestFromFile, mapLessonkitIds, packageLessonkitCourse, parseLessonkitManifest, promoteStagingToOutDir, remapArtifactPaths, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateDescriptorForTarget, validateLessonkitProject, validatePackageInputs, validateProjectPaths, writeLxpackProject };
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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
9
|
import { realpathSync } from "fs";
|
|
10
|
-
import { relative, resolve, sep, win32 } from "path";
|
|
10
|
+
import { isAbsolute, relative, resolve, sep, win32 } from "path";
|
|
11
11
|
function resolveComparablePath(p) {
|
|
12
12
|
if (/^[a-zA-Z]:[/\\]/.test(p)) {
|
|
13
13
|
return win32.resolve(p);
|
|
@@ -17,10 +17,11 @@ function resolveComparablePath(p) {
|
|
|
17
17
|
function isSafeRelativeSpaPath(spaPath) {
|
|
18
18
|
if (!spaPath.length || spaPath.includes("\0")) return false;
|
|
19
19
|
if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
|
|
20
|
-
if (/^[a-zA-Z]
|
|
21
|
-
|
|
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 !== ".");
|
|
22
23
|
if (segments.some((s) => s === "..")) return false;
|
|
23
|
-
return
|
|
24
|
+
return segments.length > 0;
|
|
24
25
|
}
|
|
25
26
|
function assertResolvedPathUnderRoot(root, target) {
|
|
26
27
|
const rootResolved = resolveComparablePath(root);
|
|
@@ -52,13 +53,25 @@ function assertRealPathUnderRoot(root, target) {
|
|
|
52
53
|
}
|
|
53
54
|
assertResolvedPathUnderRoot(rootReal, targetCheck);
|
|
54
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
|
+
}
|
|
55
68
|
function isResolvedPathUnderRoot(root, target) {
|
|
56
|
-
const rootResolved =
|
|
57
|
-
const targetResolved =
|
|
69
|
+
const rootResolved = normalizePathForComparison(root);
|
|
70
|
+
const targetResolved = normalizePathForComparison(target);
|
|
58
71
|
if (targetResolved === rootResolved) return true;
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
return
|
|
72
|
+
const rel = relativePathUnderRoot(root, target);
|
|
73
|
+
if (!rel) return true;
|
|
74
|
+
return !rel.startsWith("..") && !isAbsolute(rel);
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
// src/theme.ts
|
|
@@ -79,6 +92,70 @@ function themeToLxpackRuntime(input) {
|
|
|
79
92
|
// src/validateDescriptor.ts
|
|
80
93
|
var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
|
|
81
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
|
+
}
|
|
82
159
|
function normalizeDescriptor(input) {
|
|
83
160
|
const course = validateId(input.courseId, "courseId");
|
|
84
161
|
if (!course.ok) throw new Error("normalizeDescriptor called with invalid courseId");
|
|
@@ -112,6 +189,31 @@ function normalizeDescriptor(input) {
|
|
|
112
189
|
};
|
|
113
190
|
}
|
|
114
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) {
|
|
115
217
|
const issues = [];
|
|
116
218
|
const course = validateId(input.courseId, "courseId");
|
|
117
219
|
if (!course.ok) issues.push(...course.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
@@ -247,7 +349,7 @@ function validateDescriptor(input) {
|
|
|
247
349
|
}
|
|
248
350
|
|
|
249
351
|
// src/validateProjectPaths.ts
|
|
250
|
-
import { isAbsolute, resolve as resolve2 } from "path";
|
|
352
|
+
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
251
353
|
function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
252
354
|
if (!isSafeRelativeSpaPath(value)) {
|
|
253
355
|
issues.push({
|
|
@@ -257,7 +359,7 @@ function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
|
257
359
|
return;
|
|
258
360
|
}
|
|
259
361
|
try {
|
|
260
|
-
|
|
362
|
+
assertRealPathUnderRoot(projectRoot, resolve2(projectRoot, value));
|
|
261
363
|
} catch {
|
|
262
364
|
issues.push({
|
|
263
365
|
path: fieldPath,
|
|
@@ -285,16 +387,16 @@ function resolveSafePackageOutputOverride(projectRoot, override) {
|
|
|
285
387
|
if (!trimmed) {
|
|
286
388
|
throw new Error("output override must be a non-empty path");
|
|
287
389
|
}
|
|
288
|
-
if (
|
|
390
|
+
if (isAbsolute2(trimmed)) {
|
|
289
391
|
const resolved2 = resolve2(trimmed);
|
|
290
|
-
|
|
392
|
+
assertRealPathUnderRoot(root, resolved2);
|
|
291
393
|
return resolved2;
|
|
292
394
|
}
|
|
293
395
|
if (!isSafeRelativeSpaPath(trimmed)) {
|
|
294
396
|
throw new Error(`unsafe output path: ${override}`);
|
|
295
397
|
}
|
|
296
398
|
const resolved = resolve2(root, trimmed);
|
|
297
|
-
|
|
399
|
+
assertRealPathUnderRoot(root, resolved);
|
|
298
400
|
return resolved;
|
|
299
401
|
}
|
|
300
402
|
|
|
@@ -341,6 +443,18 @@ function extractAssessments(descriptor) {
|
|
|
341
443
|
}
|
|
342
444
|
|
|
343
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
|
+
}
|
|
344
458
|
function resolveSpaLessons(descriptor) {
|
|
345
459
|
const mapped = mapLessonkitIds(descriptor);
|
|
346
460
|
if (descriptor.layout === "single-spa") {
|
|
@@ -378,7 +492,7 @@ function descriptorToInterchange(descriptor) {
|
|
|
378
492
|
type: "spa",
|
|
379
493
|
path: l.path
|
|
380
494
|
})),
|
|
381
|
-
tracking: descriptor.tracking,
|
|
495
|
+
tracking: mapDescriptorTracking(descriptor.tracking),
|
|
382
496
|
runtime: runtime ? {
|
|
383
497
|
theme: runtime.theme,
|
|
384
498
|
cssVariables: runtime.cssVariables
|
|
@@ -455,7 +569,7 @@ async function writeLxpackProject(options) {
|
|
|
455
569
|
const descriptor = validation.descriptor;
|
|
456
570
|
const outDir = resolve4(options.outDir);
|
|
457
571
|
if (options.projectRoot) {
|
|
458
|
-
|
|
572
|
+
assertRealPathUnderRoot(resolve4(options.projectRoot), outDir);
|
|
459
573
|
}
|
|
460
574
|
const spaDirs = await resolveSpaDirs({ ...options, descriptor });
|
|
461
575
|
const interchange = descriptorToInterchange(descriptor);
|
|
@@ -487,14 +601,14 @@ import {
|
|
|
487
601
|
} from "@lxpack/api";
|
|
488
602
|
|
|
489
603
|
// src/packaging/validateInputs.ts
|
|
490
|
-
import { join as join3, resolve as resolve5, win32 as win322 } from "path";
|
|
604
|
+
import { isAbsolute as isAbsolute3, join as join3, resolve as resolve5, win32 as win322 } from "path";
|
|
491
605
|
function validatePackageInputs(options) {
|
|
492
606
|
const { target, output, outputBaseDir } = options;
|
|
493
607
|
const outDir = resolve5(options.outDir);
|
|
494
608
|
const projectRoot = options.projectRoot ? resolve5(options.projectRoot) : void 0;
|
|
495
609
|
if (projectRoot) {
|
|
496
610
|
try {
|
|
497
|
-
|
|
611
|
+
assertRealPathUnderRoot(projectRoot, outDir);
|
|
498
612
|
} catch (err) {
|
|
499
613
|
return {
|
|
500
614
|
ok: false,
|
|
@@ -541,7 +655,7 @@ function validatePackageInputs(options) {
|
|
|
541
655
|
if (projectRoot && output) {
|
|
542
656
|
const resolvedOutput = resolve5(projectRoot, output);
|
|
543
657
|
try {
|
|
544
|
-
|
|
658
|
+
assertRealPathUnderRoot(projectRoot, resolvedOutput);
|
|
545
659
|
} catch (err) {
|
|
546
660
|
return {
|
|
547
661
|
ok: false,
|
|
@@ -570,17 +684,21 @@ function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
|
|
|
570
684
|
if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
|
|
571
685
|
return artifactPath;
|
|
572
686
|
}
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
687
|
+
const rel = relativePathUnderRoot(stagingRoot, resolved);
|
|
688
|
+
if (rel.startsWith("..") || isAbsolute3(rel)) {
|
|
689
|
+
return artifactPath;
|
|
690
|
+
}
|
|
691
|
+
if (!rel) return outDir;
|
|
576
692
|
if (/^[a-zA-Z]:[/\\]/.test(outDir)) {
|
|
577
|
-
return win322.join(outDir,
|
|
693
|
+
return win322.join(outDir, rel.replace(/\//g, win322.sep));
|
|
578
694
|
}
|
|
579
|
-
return join3(outDir,
|
|
695
|
+
return join3(outDir, rel);
|
|
580
696
|
}
|
|
581
697
|
|
|
582
698
|
// src/packaging/promote.ts
|
|
583
699
|
import * as fsp from "fs/promises";
|
|
700
|
+
import { randomUUID } from "crypto";
|
|
701
|
+
import { dirname, join as join4 } from "path";
|
|
584
702
|
async function pathExists(path) {
|
|
585
703
|
try {
|
|
586
704
|
await fsp.access(path);
|
|
@@ -599,22 +717,36 @@ async function renameOrCopy(from, to) {
|
|
|
599
717
|
await fsp.rm(from, { recursive: true, force: true });
|
|
600
718
|
}
|
|
601
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
|
+
}
|
|
602
732
|
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
603
|
-
|
|
604
|
-
const
|
|
733
|
+
await assertNoLegacyPromoteArtifacts(outDir);
|
|
734
|
+
const parent = dirname(outDir);
|
|
735
|
+
const tmpPromote = await fsp.mkdtemp(join4(parent, ".lk-promote-"));
|
|
605
736
|
await renameOrCopy(stagingDir, tmpPromote);
|
|
606
737
|
const hadOutDir = await pathExists(outDir);
|
|
607
|
-
|
|
738
|
+
const backup = hadOutDir ? await fsp.mkdtemp(join4(parent, ".lk-backup-")) : void 0;
|
|
739
|
+
if (hadOutDir && backup) {
|
|
608
740
|
await renameOrCopy(outDir, backup);
|
|
609
741
|
}
|
|
610
742
|
try {
|
|
611
743
|
await renameOrCopy(tmpPromote, outDir);
|
|
612
744
|
} catch (promoteError) {
|
|
613
|
-
if (hadOutDir) {
|
|
745
|
+
if (hadOutDir && backup) {
|
|
614
746
|
try {
|
|
615
747
|
await renameOrCopy(backup, outDir);
|
|
616
748
|
} catch (restoreError) {
|
|
617
|
-
const failedPromote2 =
|
|
749
|
+
const failedPromote2 = join4(parent, `.lk-failed-promote-${randomUUID()}`);
|
|
618
750
|
try {
|
|
619
751
|
await renameOrCopy(tmpPromote, failedPromote2);
|
|
620
752
|
} catch {
|
|
@@ -638,7 +770,7 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
638
770
|
}
|
|
639
771
|
throw promoteError;
|
|
640
772
|
}
|
|
641
|
-
const failedPromote =
|
|
773
|
+
const failedPromote = join4(parent, `.lk-failed-promote-${randomUUID()}`);
|
|
642
774
|
try {
|
|
643
775
|
await renameOrCopy(tmpPromote, failedPromote);
|
|
644
776
|
} catch {
|
|
@@ -646,19 +778,19 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
|
646
778
|
}
|
|
647
779
|
throw promoteError;
|
|
648
780
|
}
|
|
649
|
-
if (
|
|
781
|
+
if (backup) {
|
|
650
782
|
await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
|
|
651
783
|
}
|
|
652
784
|
}
|
|
653
785
|
|
|
654
786
|
// src/packaging/staging.ts
|
|
655
787
|
import * as fsp2 from "fs/promises";
|
|
656
|
-
import { dirname, join as
|
|
788
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
657
789
|
import { tmpdir } from "os";
|
|
658
790
|
import { packageLessonkit } from "@lxpack/api";
|
|
659
791
|
async function buildStagingPackage(options) {
|
|
660
792
|
const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
|
|
661
|
-
const stagingDir = await fsp2.mkdtemp(
|
|
793
|
+
const stagingDir = await fsp2.mkdtemp(join5(tmpdir(), "lessonkit-lxpack-"));
|
|
662
794
|
try {
|
|
663
795
|
let spaDirs;
|
|
664
796
|
try {
|
|
@@ -677,8 +809,8 @@ async function buildStagingPackage(options) {
|
|
|
677
809
|
}
|
|
678
810
|
const interchange = descriptorToInterchange(descriptor);
|
|
679
811
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
680
|
-
await fsp2.mkdir(
|
|
681
|
-
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`));
|
|
682
814
|
const build = await packageLessonkit({
|
|
683
815
|
interchange,
|
|
684
816
|
spaDirs,
|
|
@@ -715,7 +847,7 @@ async function buildStagingPackage(options) {
|
|
|
715
847
|
}
|
|
716
848
|
}
|
|
717
849
|
async function ensureOutDirParent(outDir) {
|
|
718
|
-
await fsp2.mkdir(
|
|
850
|
+
await fsp2.mkdir(dirname2(outDir), { recursive: true });
|
|
719
851
|
}
|
|
720
852
|
|
|
721
853
|
// src/packageCourse.ts
|
|
@@ -726,14 +858,15 @@ async function validateLessonkitProject(options) {
|
|
|
726
858
|
});
|
|
727
859
|
}
|
|
728
860
|
async function buildLessonkitProject(options) {
|
|
729
|
-
|
|
861
|
+
const buildOptions = {
|
|
730
862
|
courseDir: resolve6(options.courseDir),
|
|
731
863
|
target: options.target,
|
|
732
864
|
output: options.output,
|
|
733
865
|
dir: options.dir,
|
|
734
866
|
outputBaseDir: options.outputBaseDir,
|
|
735
867
|
assessments: options.assessments
|
|
736
|
-
}
|
|
868
|
+
};
|
|
869
|
+
return buildCourse(buildOptions);
|
|
737
870
|
}
|
|
738
871
|
async function packageLessonkitCourse(options) {
|
|
739
872
|
const { target, output, dir, outputBaseDir, ...writeOpts } = options;
|
|
@@ -753,11 +886,11 @@ async function packageLessonkitCourse(options) {
|
|
|
753
886
|
};
|
|
754
887
|
}
|
|
755
888
|
const outDir = inputValidation.outDir;
|
|
756
|
-
const descriptorValidation =
|
|
889
|
+
const descriptorValidation = validateDescriptorForTarget(writeOpts.descriptor, target);
|
|
757
890
|
if (!descriptorValidation.ok) {
|
|
758
891
|
return {
|
|
759
892
|
ok: false,
|
|
760
|
-
courseDir: outDir,
|
|
893
|
+
courseDir: resolve6(writeOpts.outDir),
|
|
761
894
|
target,
|
|
762
895
|
issues: descriptorValidation.issues.map((i) => ({
|
|
763
896
|
path: i.path,
|
|
@@ -766,22 +899,6 @@ async function packageLessonkitCourse(options) {
|
|
|
766
899
|
};
|
|
767
900
|
}
|
|
768
901
|
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
|
-
};
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
902
|
const staged = await buildStagingPackage({
|
|
786
903
|
...writeOpts,
|
|
787
904
|
descriptor,
|
|
@@ -875,14 +992,19 @@ function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
|
|
|
875
992
|
}
|
|
876
993
|
const config = raw;
|
|
877
994
|
const issues = [];
|
|
878
|
-
|
|
995
|
+
let schemaVersion = config.schemaVersion;
|
|
996
|
+
if (schemaVersion === "1") {
|
|
997
|
+
schemaVersion = 1;
|
|
998
|
+
}
|
|
999
|
+
if (schemaVersion !== 1) {
|
|
879
1000
|
issues.push({
|
|
880
1001
|
path: "schemaVersion",
|
|
881
1002
|
message: `must be 1 (got ${String(config.schemaVersion)})`
|
|
882
1003
|
});
|
|
883
1004
|
}
|
|
884
|
-
const
|
|
885
|
-
|
|
1005
|
+
const nameRaw = config.name;
|
|
1006
|
+
const name = typeof nameRaw === "string" ? nameRaw.trim() : "";
|
|
1007
|
+
if (!name) {
|
|
886
1008
|
issues.push({ path: "name", message: "must be a non-empty string" });
|
|
887
1009
|
}
|
|
888
1010
|
const courseRaw = config.course;
|
|
@@ -1000,6 +1122,7 @@ export {
|
|
|
1000
1122
|
telemetryEventToLessonkit,
|
|
1001
1123
|
themeToLxpackRuntime,
|
|
1002
1124
|
validateDescriptor,
|
|
1125
|
+
validateDescriptorForTarget,
|
|
1003
1126
|
validateLessonkitProject,
|
|
1004
1127
|
validatePackageInputs,
|
|
1005
1128
|
validateProjectPaths,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://lessonkit.dev/schemas/lessonkit-manifest.v1.json",
|
|
4
|
+
"title": "LessonkitManifestV1",
|
|
5
|
+
"description": "Root lessonkit.json project manifest (schemaVersion 1)",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schemaVersion", "name", "course", "paths"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": { "const": 1 },
|
|
11
|
+
"name": { "type": "string", "minLength": 1 },
|
|
12
|
+
"course": { "$ref": "#/$defs/LessonkitCourseDescriptor" },
|
|
13
|
+
"paths": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": false,
|
|
16
|
+
"required": ["spaDistDir", "lxpackOutDir", "outputBaseDir"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"spaDistDir": { "type": "string", "minLength": 1 },
|
|
19
|
+
"lxpackOutDir": { "type": "string", "minLength": 1 },
|
|
20
|
+
"outputBaseDir": { "type": "string", "minLength": 1 }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"$defs": {
|
|
25
|
+
"LessonkitId": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
|
|
28
|
+
"maxLength": 64
|
|
29
|
+
},
|
|
30
|
+
"LessonkitCourseDescriptor": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"required": ["courseId", "title", "layout", "lessons"],
|
|
33
|
+
"properties": {
|
|
34
|
+
"courseId": { "$ref": "#/$defs/LessonkitId" },
|
|
35
|
+
"title": { "type": "string", "minLength": 1 },
|
|
36
|
+
"version": { "type": "string" },
|
|
37
|
+
"layout": { "enum": ["single-spa", "per-lesson-spa"] },
|
|
38
|
+
"lessons": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"minItems": 1,
|
|
41
|
+
"items": { "$ref": "#/$defs/LessonDescriptor" }
|
|
42
|
+
},
|
|
43
|
+
"assessments": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"items": { "$ref": "#/$defs/AssessmentDescriptor" }
|
|
46
|
+
},
|
|
47
|
+
"theme": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"properties": {
|
|
50
|
+
"preset": { "enum": ["default", "light", "dark", "brand"] },
|
|
51
|
+
"theme": { "type": "object" }
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"tracking": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"properties": {
|
|
57
|
+
"completion": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"properties": {
|
|
60
|
+
"threshold": { "type": "number", "minimum": 0, "maximum": 1 }
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"xapi": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"properties": {
|
|
66
|
+
"activityIri": { "type": "string", "minLength": 1 }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"spaDistDir": { "type": "string", "minLength": 1 },
|
|
72
|
+
"spaLessonId": { "$ref": "#/$defs/LessonkitId" }
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"LessonDescriptor": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"required": ["id", "title"],
|
|
78
|
+
"properties": {
|
|
79
|
+
"id": { "$ref": "#/$defs/LessonkitId" },
|
|
80
|
+
"title": { "type": "string", "minLength": 1 },
|
|
81
|
+
"spaPath": { "type": "string", "minLength": 1 }
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"AssessmentDescriptor": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"required": ["checkId", "question", "choices", "answer"],
|
|
87
|
+
"properties": {
|
|
88
|
+
"checkId": { "$ref": "#/$defs/LessonkitId" },
|
|
89
|
+
"question": { "type": "string", "minLength": 1 },
|
|
90
|
+
"choices": {
|
|
91
|
+
"type": "array",
|
|
92
|
+
"minItems": 1,
|
|
93
|
+
"items": { "type": "string", "minLength": 1 }
|
|
94
|
+
},
|
|
95
|
+
"answer": { "type": "string", "minLength": 1 },
|
|
96
|
+
"passingScore": { "type": "number", "exclusiveMinimum": 0 }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/lxpack",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "LXPack export adapter for LessonKit courses (SCORM, standalone, xAPI, cmi5).",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -38,10 +38,12 @@
|
|
|
38
38
|
"types": "./dist/bridge.d.ts",
|
|
39
39
|
"import": "./dist/bridge.js",
|
|
40
40
|
"require": "./dist/bridge.cjs"
|
|
41
|
-
}
|
|
41
|
+
},
|
|
42
|
+
"./lessonkit-manifest.v1.json": "./lessonkit-manifest.v1.json"
|
|
42
43
|
},
|
|
43
44
|
"files": [
|
|
44
|
-
"dist"
|
|
45
|
+
"dist",
|
|
46
|
+
"lessonkit-manifest.v1.json"
|
|
45
47
|
],
|
|
46
48
|
"scripts": {
|
|
47
49
|
"build": "tsup src/index.ts src/bridge.ts --format esm,cjs --dts",
|
|
@@ -53,8 +55,8 @@
|
|
|
53
55
|
"lint": "echo \"(no lint configured yet)\""
|
|
54
56
|
},
|
|
55
57
|
"dependencies": {
|
|
56
|
-
"@lessonkit/core": "1.0.
|
|
57
|
-
"@lessonkit/themes": "1.0.
|
|
58
|
+
"@lessonkit/core": "1.0.2",
|
|
59
|
+
"@lessonkit/themes": "1.0.2",
|
|
58
60
|
"@lxpack/api": "^0.6.2",
|
|
59
61
|
"@lxpack/spa-bridge": "^0.6.2",
|
|
60
62
|
"@lxpack/tracking-schema": "^0.6.2",
|
|
@@ -64,6 +66,6 @@
|
|
|
64
66
|
"@types/node": "^22.13.10",
|
|
65
67
|
"tsup": "^8.5.0",
|
|
66
68
|
"typescript": "^5.8.3",
|
|
67
|
-
"vitest": "^
|
|
69
|
+
"vitest": "^4.1.8"
|
|
68
70
|
}
|
|
69
71
|
}
|