@lessonkit/lxpack 0.6.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 +40 -0
- package/dist/bridge.cjs +76 -0
- package/dist/bridge.d.cts +46 -0
- package/dist/bridge.d.ts +46 -0
- package/dist/bridge.js +46 -0
- package/dist/index.cjs +577 -0
- package/dist/index.d.cts +184 -0
- package/dist/index.d.ts +184 -0
- package/dist/index.js +543 -0
- package/package.json +66 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
// src/validateDescriptor.ts
|
|
2
|
+
import { validateId } from "@lessonkit/core";
|
|
3
|
+
|
|
4
|
+
// src/spaPath.ts
|
|
5
|
+
import { resolve, sep } from "path";
|
|
6
|
+
function isSafeRelativeSpaPath(spaPath) {
|
|
7
|
+
if (!spaPath.length || spaPath.includes("\0")) return false;
|
|
8
|
+
if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
|
|
9
|
+
if (/^[a-zA-Z]:[/\\]/.test(spaPath)) return false;
|
|
10
|
+
const segments = spaPath.split(/[/\\]/);
|
|
11
|
+
if (segments.some((s) => s === "..")) return false;
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
function assertResolvedPathUnderRoot(root, target) {
|
|
15
|
+
const rootResolved = resolve(root);
|
|
16
|
+
const targetResolved = resolve(target);
|
|
17
|
+
const prefix = rootResolved.endsWith(sep) ? rootResolved : rootResolved + sep;
|
|
18
|
+
if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix)) {
|
|
19
|
+
throw new Error(`unsafe path escapes project root: ${target}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/validateDescriptor.ts
|
|
24
|
+
function normalizeDescriptor(input) {
|
|
25
|
+
const course = validateId(input.courseId, "courseId");
|
|
26
|
+
if (!course.ok) throw new Error("normalizeDescriptor called with invalid courseId");
|
|
27
|
+
return {
|
|
28
|
+
...input,
|
|
29
|
+
courseId: course.id,
|
|
30
|
+
title: input.title.trim(),
|
|
31
|
+
version: input.version?.trim() || void 0,
|
|
32
|
+
spaLessonId: input.spaLessonId?.trim() || void 0,
|
|
33
|
+
lessons: input.lessons.map((lesson) => {
|
|
34
|
+
const idResult = validateId(lesson.id, "lessonId");
|
|
35
|
+
if (!idResult.ok) throw new Error("normalizeDescriptor called with invalid lesson id");
|
|
36
|
+
return {
|
|
37
|
+
...lesson,
|
|
38
|
+
id: idResult.id,
|
|
39
|
+
title: lesson.title.trim(),
|
|
40
|
+
spaPath: lesson.spaPath?.trim() || void 0
|
|
41
|
+
};
|
|
42
|
+
}),
|
|
43
|
+
assessments: input.assessments?.map((assessment) => {
|
|
44
|
+
const check = validateId(assessment.checkId, "checkId");
|
|
45
|
+
if (!check.ok) throw new Error("normalizeDescriptor called with invalid checkId");
|
|
46
|
+
return {
|
|
47
|
+
...assessment,
|
|
48
|
+
checkId: check.id,
|
|
49
|
+
question: assessment.question.trim(),
|
|
50
|
+
choices: assessment.choices.map((c) => c.trim()).filter((c) => c.length > 0),
|
|
51
|
+
answer: assessment.answer.trim()
|
|
52
|
+
};
|
|
53
|
+
})
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function validateDescriptor(input) {
|
|
57
|
+
const issues = [];
|
|
58
|
+
const course = validateId(input.courseId, "courseId");
|
|
59
|
+
if (!course.ok) issues.push(...course.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
60
|
+
if (!input.title?.trim()) {
|
|
61
|
+
issues.push({ path: "title", message: "title is required" });
|
|
62
|
+
}
|
|
63
|
+
if (!input.lessons?.length) {
|
|
64
|
+
issues.push({ path: "lessons", message: "at least one lesson is required" });
|
|
65
|
+
}
|
|
66
|
+
if (input.layout === "single-spa" && (input.lessons?.length ?? 0) > 1) {
|
|
67
|
+
issues.push({
|
|
68
|
+
path: "lessons",
|
|
69
|
+
message: "single-spa layout packages one SPA lesson; remove extra lesson entries or use per-lesson-spa"
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const lessonIds = /* @__PURE__ */ new Set();
|
|
73
|
+
const spaPaths = /* @__PURE__ */ new Set();
|
|
74
|
+
for (const [index, lesson] of (input.lessons ?? []).entries()) {
|
|
75
|
+
const path = `lessons[${index}]`;
|
|
76
|
+
const lessonResult = validateId(lesson.id, `${path}.id`);
|
|
77
|
+
if (!lessonResult.ok) {
|
|
78
|
+
issues.push(...lessonResult.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
79
|
+
} else if (lessonIds.has(lessonResult.id)) {
|
|
80
|
+
issues.push({ path: `${path}.id`, message: "duplicate lesson id" });
|
|
81
|
+
} else {
|
|
82
|
+
lessonIds.add(lessonResult.id);
|
|
83
|
+
}
|
|
84
|
+
if (!lesson.title?.trim()) {
|
|
85
|
+
issues.push({ path: `${path}.title`, message: "lesson title is required" });
|
|
86
|
+
}
|
|
87
|
+
if (input.layout === "per-lesson-spa") {
|
|
88
|
+
const spaPath = lesson.spaPath?.trim();
|
|
89
|
+
if (!spaPath) {
|
|
90
|
+
issues.push({
|
|
91
|
+
path: `${path}.spaPath`,
|
|
92
|
+
message: "spaPath is required for per-lesson-spa layout"
|
|
93
|
+
});
|
|
94
|
+
} else if (!isSafeRelativeSpaPath(spaPath)) {
|
|
95
|
+
issues.push({
|
|
96
|
+
path: `${path}.spaPath`,
|
|
97
|
+
message: "spaPath must be a relative path without '..' segments or absolute prefixes"
|
|
98
|
+
});
|
|
99
|
+
} else if (spaPaths.has(spaPath)) {
|
|
100
|
+
issues.push({ path: `${path}.spaPath`, message: "duplicate spaPath" });
|
|
101
|
+
} else {
|
|
102
|
+
spaPaths.add(spaPath);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (input.layout === "single-spa" && input.spaLessonId?.trim()) {
|
|
107
|
+
const spaId = input.spaLessonId.trim();
|
|
108
|
+
const spaResult = validateId(spaId, "spaLessonId");
|
|
109
|
+
if (!spaResult.ok) {
|
|
110
|
+
issues.push(...spaResult.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
111
|
+
} else if (!lessonIds.has(spaResult.id)) {
|
|
112
|
+
issues.push({
|
|
113
|
+
path: "spaLessonId",
|
|
114
|
+
message: "spaLessonId must match a lesson id in lessons"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const checkIds = /* @__PURE__ */ new Set();
|
|
119
|
+
for (const [index, assessment] of (input.assessments ?? []).entries()) {
|
|
120
|
+
const path = `assessments[${index}]`;
|
|
121
|
+
const check = validateId(assessment.checkId, `${path}.checkId`);
|
|
122
|
+
if (!check.ok) {
|
|
123
|
+
issues.push(...check.issues.map((i) => ({ path: i.path, message: i.message })));
|
|
124
|
+
} else if (checkIds.has(check.id)) {
|
|
125
|
+
issues.push({ path: `${path}.checkId`, message: "duplicate checkId" });
|
|
126
|
+
} else {
|
|
127
|
+
checkIds.add(check.id);
|
|
128
|
+
}
|
|
129
|
+
if (!assessment.question?.trim()) {
|
|
130
|
+
issues.push({ path: `${path}.question`, message: "question is required" });
|
|
131
|
+
}
|
|
132
|
+
const trimmedChoices = (assessment.choices ?? []).map((c) => c.trim()).filter((c) => c.length > 0);
|
|
133
|
+
if (!trimmedChoices.length) {
|
|
134
|
+
issues.push({
|
|
135
|
+
path: `${path}.choices`,
|
|
136
|
+
message: "at least one non-empty choice is required"
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (!assessment.answer?.trim()) {
|
|
140
|
+
issues.push({ path: `${path}.answer`, message: "answer is required" });
|
|
141
|
+
} else if (trimmedChoices.length && !trimmedChoices.includes(assessment.answer.trim())) {
|
|
142
|
+
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
143
|
+
}
|
|
144
|
+
const passingScore = assessment.passingScore;
|
|
145
|
+
if (passingScore !== void 0) {
|
|
146
|
+
if (!(passingScore > 0)) {
|
|
147
|
+
issues.push({
|
|
148
|
+
path: `${path}.passingScore`,
|
|
149
|
+
message: "passingScore must be greater than 0"
|
|
150
|
+
});
|
|
151
|
+
} else if (trimmedChoices.length && passingScore > trimmedChoices.length) {
|
|
152
|
+
issues.push({
|
|
153
|
+
path: `${path}.passingScore`,
|
|
154
|
+
message: "passingScore must not exceed the number of choices"
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (issues.length) return { ok: false, issues };
|
|
160
|
+
return { ok: true, descriptor: normalizeDescriptor(input) };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/mapIds.ts
|
|
164
|
+
import { assertValidId } from "@lessonkit/core";
|
|
165
|
+
function mapLessonkitIds(descriptor) {
|
|
166
|
+
const courseId = assertValidId(descriptor.courseId, "courseId");
|
|
167
|
+
const lessonIds = descriptor.lessons.map((l) => assertValidId(l.id, "lessonId"));
|
|
168
|
+
const checkIds = (descriptor.assessments ?? []).map(
|
|
169
|
+
(a) => assertValidId(a.checkId, "checkId")
|
|
170
|
+
);
|
|
171
|
+
return { courseId, lessonIds, checkIds };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/theme.ts
|
|
175
|
+
import { getPresetTheme, themeToCssVariables } from "@lessonkit/themes";
|
|
176
|
+
function themeToLxpackRuntime(input) {
|
|
177
|
+
const theme = input.theme ?? getPresetTheme(input.preset ?? "default");
|
|
178
|
+
const raw = themeToCssVariables(theme);
|
|
179
|
+
const cssVariables = {};
|
|
180
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
181
|
+
cssVariables[key] = String(value);
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
theme: theme.name,
|
|
185
|
+
cssVariables
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/interchange.ts
|
|
190
|
+
function resolveSpaLessons(descriptor) {
|
|
191
|
+
const mapped = mapLessonkitIds(descriptor);
|
|
192
|
+
if (descriptor.layout === "single-spa") {
|
|
193
|
+
const spaLessonId = descriptor.spaLessonId ?? mapped.lessonIds[0] ?? "main";
|
|
194
|
+
const firstLesson = descriptor.lessons.find((l) => l.id === spaLessonId);
|
|
195
|
+
return [
|
|
196
|
+
{
|
|
197
|
+
id: spaLessonId,
|
|
198
|
+
title: firstLesson?.title ?? descriptor.title,
|
|
199
|
+
path: "dist"
|
|
200
|
+
}
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
return descriptor.lessons.map((lesson) => ({
|
|
204
|
+
id: lesson.id,
|
|
205
|
+
title: lesson.title,
|
|
206
|
+
path: lesson.spaPath
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
function descriptorToInterchange(descriptor) {
|
|
210
|
+
const mapped = mapLessonkitIds(descriptor);
|
|
211
|
+
const spaLessons = resolveSpaLessons(descriptor);
|
|
212
|
+
return {
|
|
213
|
+
format: "lessonkit",
|
|
214
|
+
version: "1",
|
|
215
|
+
course: {
|
|
216
|
+
id: mapped.courseId,
|
|
217
|
+
title: descriptor.title
|
|
218
|
+
},
|
|
219
|
+
lessons: spaLessons.map((l) => ({
|
|
220
|
+
id: l.id,
|
|
221
|
+
title: l.title,
|
|
222
|
+
type: "spa",
|
|
223
|
+
path: l.path
|
|
224
|
+
})),
|
|
225
|
+
tracking: descriptor.tracking
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/assessments.ts
|
|
230
|
+
function slugChoiceId(text, index) {
|
|
231
|
+
const base = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
|
|
232
|
+
const stem = base.length ? base : "choice";
|
|
233
|
+
return `${stem}-${index + 1}`;
|
|
234
|
+
}
|
|
235
|
+
function assessmentDescriptorToLxpack(assessment) {
|
|
236
|
+
const choices = assessment.choices.map((text, index) => {
|
|
237
|
+
const id = slugChoiceId(text, index);
|
|
238
|
+
return {
|
|
239
|
+
id,
|
|
240
|
+
text,
|
|
241
|
+
correct: text === assessment.answer
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
id: assessment.checkId,
|
|
246
|
+
passingScore: assessment.passingScore ?? 1,
|
|
247
|
+
questions: [
|
|
248
|
+
{
|
|
249
|
+
id: "q1",
|
|
250
|
+
prompt: assessment.question,
|
|
251
|
+
choices
|
|
252
|
+
}
|
|
253
|
+
]
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function extractAssessments(descriptor) {
|
|
257
|
+
return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/writeProject.ts
|
|
261
|
+
import { access, cp, mkdir, rm, writeFile } from "fs/promises";
|
|
262
|
+
import { dirname, join, resolve as resolve2 } from "path";
|
|
263
|
+
|
|
264
|
+
// src/assessmentYaml.ts
|
|
265
|
+
function yamlQuote(value) {
|
|
266
|
+
return JSON.stringify(value);
|
|
267
|
+
}
|
|
268
|
+
function emitAssessmentYaml(assessment) {
|
|
269
|
+
const lx = assessmentDescriptorToLxpack(assessment);
|
|
270
|
+
const lines = [];
|
|
271
|
+
lines.push(`id: ${lx.id}`);
|
|
272
|
+
lines.push(`passingScore: ${lx.passingScore}`);
|
|
273
|
+
lines.push("questions:");
|
|
274
|
+
for (const question of lx.questions) {
|
|
275
|
+
lines.push(` - id: ${question.id}`);
|
|
276
|
+
lines.push(` prompt: ${yamlQuote(question.prompt)}`);
|
|
277
|
+
lines.push(" choices:");
|
|
278
|
+
for (const choice of question.choices) {
|
|
279
|
+
lines.push(` - id: ${choice.id}`);
|
|
280
|
+
lines.push(` text: ${yamlQuote(choice.text)}`);
|
|
281
|
+
if (choice.correct) lines.push(" correct: true");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return `${lines.join("\n")}
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/yaml.ts
|
|
289
|
+
function yamlQuote2(value) {
|
|
290
|
+
if (/[:#\n\r]/.test(value) || value.startsWith(" ") || value.endsWith(" ")) {
|
|
291
|
+
return JSON.stringify(value);
|
|
292
|
+
}
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
function emitCourseYaml(opts) {
|
|
296
|
+
const lines = [];
|
|
297
|
+
lines.push(`title: ${yamlQuote2(opts.title)}`);
|
|
298
|
+
lines.push(`version: ${yamlQuote2(opts.version)}`);
|
|
299
|
+
if (opts.description) lines.push(`description: ${yamlQuote2(opts.description)}`);
|
|
300
|
+
if (opts.runtime) {
|
|
301
|
+
lines.push("runtime:");
|
|
302
|
+
lines.push(` theme: ${yamlQuote2(opts.runtime.theme)}`);
|
|
303
|
+
if (opts.runtime.cssVariables && Object.keys(opts.runtime.cssVariables).length) {
|
|
304
|
+
lines.push(" cssVariables:");
|
|
305
|
+
for (const [key, value] of Object.entries(opts.runtime.cssVariables).sort(
|
|
306
|
+
([a], [b]) => a.localeCompare(b)
|
|
307
|
+
)) {
|
|
308
|
+
lines.push(` ${key}: ${JSON.stringify(String(value))}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (opts.tracking?.completion?.threshold !== void 0) {
|
|
313
|
+
lines.push("tracking:");
|
|
314
|
+
lines.push(" completion:");
|
|
315
|
+
lines.push(` threshold: ${opts.tracking.completion.threshold}`);
|
|
316
|
+
}
|
|
317
|
+
lines.push("lessons:");
|
|
318
|
+
for (const lesson of opts.lessons) {
|
|
319
|
+
lines.push(` - id: ${lesson.id}`);
|
|
320
|
+
lines.push(` title: ${yamlQuote2(lesson.title)}`);
|
|
321
|
+
lines.push(` type: spa`);
|
|
322
|
+
lines.push(` path: ${lesson.path}`);
|
|
323
|
+
}
|
|
324
|
+
if (opts.assessments.length) {
|
|
325
|
+
lines.push("assessments:");
|
|
326
|
+
for (const assessment of opts.assessments) {
|
|
327
|
+
lines.push(` - id: ${assessment.id}`);
|
|
328
|
+
lines.push(` file: ${assessment.file}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return `${lines.join("\n")}
|
|
332
|
+
`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/writeProject.ts
|
|
336
|
+
async function copyDir(src, dest) {
|
|
337
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
338
|
+
await cp(src, dest, { recursive: true });
|
|
339
|
+
}
|
|
340
|
+
async function writeLxpackProject(options) {
|
|
341
|
+
const validation = validateDescriptor(options.descriptor);
|
|
342
|
+
if (!validation.ok) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
validation.issues.map((i) => `${i.path}: ${i.message}`).join("; ")
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
const descriptor = validation.descriptor;
|
|
348
|
+
const outDir = resolve2(options.outDir);
|
|
349
|
+
await mkdir(outDir, { recursive: true });
|
|
350
|
+
const spaLessons = resolveSpaLessons(descriptor);
|
|
351
|
+
const runtime = descriptor.theme ? themeToLxpackRuntime(descriptor.theme) : void 0;
|
|
352
|
+
const assessments = (descriptor.assessments ?? []).map((a) => ({
|
|
353
|
+
id: a.checkId,
|
|
354
|
+
file: `assessments/${a.checkId}.yaml`
|
|
355
|
+
}));
|
|
356
|
+
if (descriptor.layout === "single-spa") {
|
|
357
|
+
const srcDist = resolve2(options.spaDistDir ?? descriptor.spaDistDir ?? "dist");
|
|
358
|
+
try {
|
|
359
|
+
await access(srcDist);
|
|
360
|
+
} catch {
|
|
361
|
+
throw new Error(`spaDistDir not found: ${srcDist}`);
|
|
362
|
+
}
|
|
363
|
+
const destDist = join(outDir, "dist");
|
|
364
|
+
await rm(destDist, { recursive: true, force: true });
|
|
365
|
+
await copyDir(srcDist, destDist);
|
|
366
|
+
} else {
|
|
367
|
+
const lessonDirs = options.lessonSpaDirs ?? {};
|
|
368
|
+
for (const lesson of descriptor.lessons) {
|
|
369
|
+
const src = lessonDirs[lesson.id];
|
|
370
|
+
if (!src) {
|
|
371
|
+
throw new Error(`lessonSpaDirs missing build output for lesson "${lesson.id}"`);
|
|
372
|
+
}
|
|
373
|
+
const dest = join(outDir, lesson.spaPath);
|
|
374
|
+
assertResolvedPathUnderRoot(outDir, dest);
|
|
375
|
+
await rm(dest, { recursive: true, force: true });
|
|
376
|
+
await copyDir(resolve2(src), dest);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (assessments.length) {
|
|
380
|
+
const assessmentsDir = join(outDir, "assessments");
|
|
381
|
+
await mkdir(assessmentsDir, { recursive: true });
|
|
382
|
+
for (const assessment of descriptor.assessments ?? []) {
|
|
383
|
+
await writeFile(
|
|
384
|
+
join(outDir, `assessments/${assessment.checkId}.yaml`),
|
|
385
|
+
emitAssessmentYaml(assessment),
|
|
386
|
+
"utf-8"
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const courseYamlPath = join(outDir, "course.yaml");
|
|
391
|
+
await writeFile(
|
|
392
|
+
courseYamlPath,
|
|
393
|
+
emitCourseYaml({
|
|
394
|
+
title: descriptor.title,
|
|
395
|
+
version: descriptor.version ?? "1.0.0",
|
|
396
|
+
runtime,
|
|
397
|
+
tracking: descriptor.tracking,
|
|
398
|
+
lessons: spaLessons.map((l) => ({
|
|
399
|
+
id: l.id,
|
|
400
|
+
title: l.title,
|
|
401
|
+
type: "spa",
|
|
402
|
+
path: l.path
|
|
403
|
+
})),
|
|
404
|
+
assessments
|
|
405
|
+
}),
|
|
406
|
+
"utf-8"
|
|
407
|
+
);
|
|
408
|
+
const lessonkitJsonPath = join(outDir, "lessonkit.json");
|
|
409
|
+
await writeFile(
|
|
410
|
+
lessonkitJsonPath,
|
|
411
|
+
`${JSON.stringify(descriptorToInterchange(descriptor), null, 2)}
|
|
412
|
+
`,
|
|
413
|
+
"utf-8"
|
|
414
|
+
);
|
|
415
|
+
return { outDir, courseYamlPath, lessonkitJsonPath };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/packageCourse.ts
|
|
419
|
+
import { mkdir as mkdir2, mkdtemp, rename, rm as rm2 } from "fs/promises";
|
|
420
|
+
import { dirname as dirname2, join as join2, resolve as resolve3 } from "path";
|
|
421
|
+
import { tmpdir } from "os";
|
|
422
|
+
import {
|
|
423
|
+
buildCourse,
|
|
424
|
+
validateCourse
|
|
425
|
+
} from "@lxpack/api";
|
|
426
|
+
async function validateLessonkitProject(options) {
|
|
427
|
+
return validateCourse({
|
|
428
|
+
courseDir: resolve3(options.courseDir),
|
|
429
|
+
target: options.target
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
async function buildLessonkitProject(options) {
|
|
433
|
+
return buildCourse({
|
|
434
|
+
courseDir: resolve3(options.courseDir),
|
|
435
|
+
target: options.target,
|
|
436
|
+
output: options.output,
|
|
437
|
+
dir: options.dir,
|
|
438
|
+
outputBaseDir: options.outputBaseDir,
|
|
439
|
+
assessments: options.assessments
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
async function packageLessonkitCourse(options) {
|
|
443
|
+
const { target, output, dir, outputBaseDir, ...writeOpts } = options;
|
|
444
|
+
const outDir = resolve3(writeOpts.outDir);
|
|
445
|
+
const descriptorValidation = validateDescriptor(writeOpts.descriptor);
|
|
446
|
+
if (!descriptorValidation.ok) {
|
|
447
|
+
return {
|
|
448
|
+
ok: false,
|
|
449
|
+
courseDir: outDir,
|
|
450
|
+
target,
|
|
451
|
+
issues: descriptorValidation.issues.map((i) => ({
|
|
452
|
+
path: i.path,
|
|
453
|
+
message: i.message
|
|
454
|
+
}))
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const descriptor = descriptorValidation.descriptor;
|
|
458
|
+
const stagingDir = await mkdtemp(join2(tmpdir(), "lessonkit-lxpack-"));
|
|
459
|
+
let promoted = false;
|
|
460
|
+
try {
|
|
461
|
+
const written = await writeLxpackProject({ ...writeOpts, descriptor, outDir: stagingDir });
|
|
462
|
+
const courseDir = written.outDir;
|
|
463
|
+
const assessments = extractAssessments(descriptor);
|
|
464
|
+
const validation = await validateLessonkitProject({ courseDir, target });
|
|
465
|
+
if (!validation.ok) {
|
|
466
|
+
return {
|
|
467
|
+
ok: false,
|
|
468
|
+
courseDir: outDir,
|
|
469
|
+
target,
|
|
470
|
+
validation,
|
|
471
|
+
issues: validation.issues.map((i) => ({
|
|
472
|
+
path: i.path,
|
|
473
|
+
message: i.message,
|
|
474
|
+
severity: i.severity
|
|
475
|
+
}))
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
479
|
+
await mkdir2(join2(courseDir, outputBase), { recursive: true });
|
|
480
|
+
const defaultOutput = output ?? (dir ? join2(outputBase, target) : join2(outputBase, `course-${target}.zip`));
|
|
481
|
+
const build = await buildLessonkitProject({
|
|
482
|
+
courseDir,
|
|
483
|
+
target,
|
|
484
|
+
output: defaultOutput.startsWith("/") ? defaultOutput : join2(courseDir, defaultOutput),
|
|
485
|
+
dir,
|
|
486
|
+
assessments: assessments.length ? assessments : void 0
|
|
487
|
+
});
|
|
488
|
+
if (!build.ok) {
|
|
489
|
+
return {
|
|
490
|
+
ok: false,
|
|
491
|
+
courseDir: outDir,
|
|
492
|
+
target,
|
|
493
|
+
validation,
|
|
494
|
+
build,
|
|
495
|
+
issues: build.issues.map((i) => ({
|
|
496
|
+
path: i.path,
|
|
497
|
+
message: i.message,
|
|
498
|
+
severity: i.severity
|
|
499
|
+
}))
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
await rm2(outDir, { recursive: true, force: true });
|
|
503
|
+
await mkdir2(dirname2(outDir), { recursive: true });
|
|
504
|
+
await rename(stagingDir, outDir);
|
|
505
|
+
promoted = true;
|
|
506
|
+
const remapArtifactPath = (artifactPath) => {
|
|
507
|
+
if (!artifactPath) return void 0;
|
|
508
|
+
const resolved = resolve3(artifactPath);
|
|
509
|
+
const stagingResolved = resolve3(stagingDir);
|
|
510
|
+
if (resolved === stagingResolved || resolved.startsWith(stagingResolved + "/")) {
|
|
511
|
+
return join2(outDir, resolved.slice(stagingResolved.length + 1));
|
|
512
|
+
}
|
|
513
|
+
return artifactPath;
|
|
514
|
+
};
|
|
515
|
+
return {
|
|
516
|
+
ok: true,
|
|
517
|
+
courseDir: outDir,
|
|
518
|
+
target,
|
|
519
|
+
outputPath: remapArtifactPath("outputPath" in build ? build.outputPath : void 0),
|
|
520
|
+
outputDir: remapArtifactPath("outputDir" in build ? build.outputDir : void 0),
|
|
521
|
+
fileCount: build.fileCount,
|
|
522
|
+
validation,
|
|
523
|
+
build
|
|
524
|
+
};
|
|
525
|
+
} finally {
|
|
526
|
+
if (!promoted) {
|
|
527
|
+
await rm2(stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
export {
|
|
532
|
+
assessmentDescriptorToLxpack,
|
|
533
|
+
buildLessonkitProject,
|
|
534
|
+
descriptorToInterchange,
|
|
535
|
+
extractAssessments,
|
|
536
|
+
mapLessonkitIds,
|
|
537
|
+
packageLessonkitCourse,
|
|
538
|
+
resolveSpaLessons,
|
|
539
|
+
themeToLxpackRuntime,
|
|
540
|
+
validateDescriptor,
|
|
541
|
+
validateLessonkitProject,
|
|
542
|
+
writeLxpackProject
|
|
543
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lessonkit/lxpack",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "LXPack export adapter for LessonKit courses (SCORM, standalone, xAPI, cmi5).",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/eddiethedean/lessonkit.git",
|
|
10
|
+
"directory": "packages/lxpack"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/eddiethedean/lessonkit",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/eddiethedean/lessonkit/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"lessonkit",
|
|
18
|
+
"lxpack",
|
|
19
|
+
"scorm",
|
|
20
|
+
"lms",
|
|
21
|
+
"packaging",
|
|
22
|
+
"elearning"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./dist/index.cjs",
|
|
29
|
+
"module": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js",
|
|
35
|
+
"require": "./dist/index.cjs"
|
|
36
|
+
},
|
|
37
|
+
"./bridge": {
|
|
38
|
+
"types": "./dist/bridge.d.ts",
|
|
39
|
+
"import": "./dist/bridge.js",
|
|
40
|
+
"require": "./dist/bridge.cjs"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup src/index.ts src/bridge.ts --format esm,cjs --dts",
|
|
48
|
+
"dev": "tsup src/index.ts src/bridge.ts --format esm,cjs --dts --watch",
|
|
49
|
+
"prepublishOnly": "npm run build",
|
|
50
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
51
|
+
"test": "vitest run --passWithNoTests",
|
|
52
|
+
"test:coverage": "vitest run --coverage --passWithNoTests=false",
|
|
53
|
+
"lint": "echo \"(no lint configured yet)\""
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@lessonkit/core": "0.6.0",
|
|
57
|
+
"@lessonkit/themes": "0.6.0",
|
|
58
|
+
"@lxpack/api": "^0.4.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "^22.13.10",
|
|
62
|
+
"tsup": "^8.5.0",
|
|
63
|
+
"typescript": "^5.8.3",
|
|
64
|
+
"vitest": "^3.2.4"
|
|
65
|
+
}
|
|
66
|
+
}
|