@lessonkit/lxpack 0.8.0 → 0.9.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 +1 -1
- package/dist/bridge.cjs +69 -9
- package/dist/bridge.d.cts +15 -27
- package/dist/bridge.d.ts +15 -27
- package/dist/bridge.js +46 -8
- package/dist/chunk-PSUSESH3.js +32 -0
- package/dist/index.cjs +325 -230
- package/dist/index.d.cts +23 -20
- package/dist/index.d.ts +23 -20
- package/dist/index.js +287 -229
- package/dist/telemetry-gCxlwc7I.d.cts +9 -0
- package/dist/telemetry-gCxlwc7I.d.ts +9 -0
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
telemetryEventToLessonkit
|
|
3
|
+
} from "./chunk-PSUSESH3.js";
|
|
4
|
+
|
|
1
5
|
// src/validateDescriptor.ts
|
|
2
6
|
import { validateId } from "@lessonkit/core";
|
|
3
7
|
|
|
@@ -7,8 +11,8 @@ function isSafeRelativeSpaPath(spaPath) {
|
|
|
7
11
|
if (!spaPath.length || spaPath.includes("\0")) return false;
|
|
8
12
|
if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
|
|
9
13
|
if (/^[a-zA-Z]:[/\\]/.test(spaPath)) return false;
|
|
10
|
-
const segments = spaPath.split(/[/\\]/);
|
|
11
|
-
if (segments.some((s) => s === "..")) return false;
|
|
14
|
+
const segments = spaPath.split(/[/\\]/).filter((s) => s.length > 0);
|
|
15
|
+
if (segments.some((s) => s === ".." || s === ".")) return false;
|
|
12
16
|
return true;
|
|
13
17
|
}
|
|
14
18
|
function assertResolvedPathUnderRoot(root, target) {
|
|
@@ -20,6 +24,21 @@ function assertResolvedPathUnderRoot(root, target) {
|
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
// src/theme.ts
|
|
28
|
+
import { getPresetTheme, themeToCssVariables } from "@lessonkit/themes";
|
|
29
|
+
function themeToLxpackRuntime(input) {
|
|
30
|
+
const theme = input.theme ?? getPresetTheme(input.preset ?? "default");
|
|
31
|
+
const raw = themeToCssVariables(theme);
|
|
32
|
+
const cssVariables = {};
|
|
33
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
34
|
+
cssVariables[key] = String(value);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
theme: theme.name,
|
|
38
|
+
cssVariables
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
// src/validateDescriptor.ts
|
|
24
43
|
var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
|
|
25
44
|
var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
|
|
@@ -81,6 +100,25 @@ function validateDescriptor(input) {
|
|
|
81
100
|
message: `unknown preset; use one of: ${VALID_THEME_PRESETS.join(", ")}`
|
|
82
101
|
});
|
|
83
102
|
}
|
|
103
|
+
if (input.theme?.theme) {
|
|
104
|
+
try {
|
|
105
|
+
themeToLxpackRuntime({ preset: themePreset, theme: input.theme.theme });
|
|
106
|
+
} catch (err) {
|
|
107
|
+
issues.push({
|
|
108
|
+
path: "theme.theme",
|
|
109
|
+
message: err instanceof Error ? err.message : "invalid custom theme"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const completionThreshold = input.tracking?.completion?.threshold;
|
|
114
|
+
if (completionThreshold !== void 0) {
|
|
115
|
+
if (!Number.isFinite(completionThreshold) || completionThreshold < 0 || completionThreshold > 1) {
|
|
116
|
+
issues.push({
|
|
117
|
+
path: "tracking.completion.threshold",
|
|
118
|
+
message: "threshold must be a finite number between 0 and 1"
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
84
122
|
if (layout === "single-spa" && (input.lessons?.length ?? 0) > 1) {
|
|
85
123
|
issues.push({
|
|
86
124
|
path: "lessons",
|
|
@@ -160,24 +198,69 @@ function validateDescriptor(input) {
|
|
|
160
198
|
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
161
199
|
}
|
|
162
200
|
const passingScore = assessment.passingScore;
|
|
163
|
-
if (passingScore !== void 0) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
});
|
|
169
|
-
} else if (trimmedChoices.length && passingScore > trimmedChoices.length) {
|
|
170
|
-
issues.push({
|
|
171
|
-
path: `${path}.passingScore`,
|
|
172
|
-
message: "passingScore must not exceed the number of choices"
|
|
173
|
-
});
|
|
174
|
-
}
|
|
201
|
+
if (passingScore !== void 0 && !(passingScore > 0)) {
|
|
202
|
+
issues.push({
|
|
203
|
+
path: `${path}.passingScore`,
|
|
204
|
+
message: "passingScore must be greater than 0 (absolute point threshold)"
|
|
205
|
+
});
|
|
175
206
|
}
|
|
176
207
|
}
|
|
177
208
|
if (issues.length) return { ok: false, issues };
|
|
178
209
|
return { ok: true, descriptor: normalizeDescriptor(input) };
|
|
179
210
|
}
|
|
180
211
|
|
|
212
|
+
// src/validateProjectPaths.ts
|
|
213
|
+
import { isAbsolute, resolve as resolve2 } from "path";
|
|
214
|
+
function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
215
|
+
if (!isSafeRelativeSpaPath(value)) {
|
|
216
|
+
issues.push({
|
|
217
|
+
path: fieldPath,
|
|
218
|
+
message: "path must be relative without '..' segments or absolute prefixes"
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
assertResolvedPathUnderRoot(projectRoot, resolve2(projectRoot, value));
|
|
224
|
+
} catch {
|
|
225
|
+
issues.push({
|
|
226
|
+
path: fieldPath,
|
|
227
|
+
message: "path must resolve inside the project root"
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function validateProjectPaths(projectRoot, paths) {
|
|
232
|
+
const issues = [];
|
|
233
|
+
const root = resolve2(projectRoot);
|
|
234
|
+
if (paths.spaDistDir?.trim()) {
|
|
235
|
+
validatePathField(paths.spaDistDir.trim(), "paths.spaDistDir", root, issues);
|
|
236
|
+
}
|
|
237
|
+
if (paths.lxpackOutDir?.trim()) {
|
|
238
|
+
validatePathField(paths.lxpackOutDir.trim(), "paths.lxpackOutDir", root, issues);
|
|
239
|
+
}
|
|
240
|
+
if (paths.outputBaseDir?.trim()) {
|
|
241
|
+
validatePathField(paths.outputBaseDir.trim(), "paths.outputBaseDir", root, issues);
|
|
242
|
+
}
|
|
243
|
+
return issues;
|
|
244
|
+
}
|
|
245
|
+
function resolveSafePackageOutputOverride(projectRoot, override) {
|
|
246
|
+
const root = resolve2(projectRoot);
|
|
247
|
+
const trimmed = override.trim();
|
|
248
|
+
if (!trimmed) {
|
|
249
|
+
throw new Error("output override must be a non-empty path");
|
|
250
|
+
}
|
|
251
|
+
if (isAbsolute(trimmed)) {
|
|
252
|
+
const resolved2 = resolve2(trimmed);
|
|
253
|
+
assertResolvedPathUnderRoot(root, resolved2);
|
|
254
|
+
return resolved2;
|
|
255
|
+
}
|
|
256
|
+
if (!isSafeRelativeSpaPath(trimmed)) {
|
|
257
|
+
throw new Error(`unsafe output path: ${override}`);
|
|
258
|
+
}
|
|
259
|
+
const resolved = resolve2(root, trimmed);
|
|
260
|
+
assertResolvedPathUnderRoot(root, resolved);
|
|
261
|
+
return resolved;
|
|
262
|
+
}
|
|
263
|
+
|
|
181
264
|
// src/mapIds.ts
|
|
182
265
|
import { assertValidId } from "@lessonkit/core";
|
|
183
266
|
function mapLessonkitIds(descriptor) {
|
|
@@ -189,20 +272,36 @@ function mapLessonkitIds(descriptor) {
|
|
|
189
272
|
return { courseId, lessonIds, checkIds };
|
|
190
273
|
}
|
|
191
274
|
|
|
192
|
-
// src/
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
275
|
+
// src/assessments.ts
|
|
276
|
+
function slugChoiceId(text, index) {
|
|
277
|
+
const base = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
|
|
278
|
+
const stem = base.length ? base : "choice";
|
|
279
|
+
return `${stem}-${index + 1}`;
|
|
280
|
+
}
|
|
281
|
+
function assessmentDescriptorToLxpack(assessment) {
|
|
282
|
+
const choices = assessment.choices.map((text, index) => {
|
|
283
|
+
const id = slugChoiceId(text, index);
|
|
284
|
+
return {
|
|
285
|
+
id,
|
|
286
|
+
text,
|
|
287
|
+
correct: text === assessment.answer
|
|
288
|
+
};
|
|
289
|
+
});
|
|
201
290
|
return {
|
|
202
|
-
|
|
203
|
-
|
|
291
|
+
id: assessment.checkId,
|
|
292
|
+
passingScore: assessment.passingScore ?? 1,
|
|
293
|
+
questions: [
|
|
294
|
+
{
|
|
295
|
+
id: "q1",
|
|
296
|
+
prompt: assessment.question,
|
|
297
|
+
choices
|
|
298
|
+
}
|
|
299
|
+
]
|
|
204
300
|
};
|
|
205
301
|
}
|
|
302
|
+
function extractAssessments(descriptor) {
|
|
303
|
+
return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack);
|
|
304
|
+
}
|
|
206
305
|
|
|
207
306
|
// src/interchange.ts
|
|
208
307
|
function resolveSpaLessons(descriptor) {
|
|
@@ -227,6 +326,8 @@ function resolveSpaLessons(descriptor) {
|
|
|
227
326
|
function descriptorToInterchange(descriptor) {
|
|
228
327
|
const mapped = mapLessonkitIds(descriptor);
|
|
229
328
|
const spaLessons = resolveSpaLessons(descriptor);
|
|
329
|
+
const runtime = descriptor.theme ? themeToLxpackRuntime(descriptor.theme) : void 0;
|
|
330
|
+
const assessments = extractAssessments(descriptor);
|
|
230
331
|
return {
|
|
231
332
|
format: "lessonkit",
|
|
232
333
|
version: "1",
|
|
@@ -240,121 +341,52 @@ function descriptorToInterchange(descriptor) {
|
|
|
240
341
|
type: "spa",
|
|
241
342
|
path: l.path
|
|
242
343
|
})),
|
|
243
|
-
tracking: descriptor.tracking
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const base = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
|
|
250
|
-
const stem = base.length ? base : "choice";
|
|
251
|
-
return `${stem}-${index + 1}`;
|
|
252
|
-
}
|
|
253
|
-
function assessmentDescriptorToLxpack(assessment) {
|
|
254
|
-
const choices = assessment.choices.map((text, index) => {
|
|
255
|
-
const id = slugChoiceId(text, index);
|
|
256
|
-
return {
|
|
257
|
-
id,
|
|
258
|
-
text,
|
|
259
|
-
correct: text === assessment.answer
|
|
260
|
-
};
|
|
261
|
-
});
|
|
262
|
-
return {
|
|
263
|
-
id: assessment.checkId,
|
|
264
|
-
passingScore: assessment.passingScore ?? 1,
|
|
265
|
-
questions: [
|
|
266
|
-
{
|
|
267
|
-
id: "q1",
|
|
268
|
-
prompt: assessment.question,
|
|
269
|
-
choices
|
|
270
|
-
}
|
|
271
|
-
]
|
|
344
|
+
tracking: descriptor.tracking,
|
|
345
|
+
runtime: runtime ? {
|
|
346
|
+
theme: runtime.theme,
|
|
347
|
+
cssVariables: runtime.cssVariables
|
|
348
|
+
} : void 0,
|
|
349
|
+
assessments: assessments.length ? assessments : void 0
|
|
272
350
|
};
|
|
273
351
|
}
|
|
274
|
-
function extractAssessments(descriptor) {
|
|
275
|
-
return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack);
|
|
276
|
-
}
|
|
277
352
|
|
|
278
353
|
// src/writeProject.ts
|
|
279
|
-
import {
|
|
280
|
-
import {
|
|
354
|
+
import { join, resolve as resolve4 } from "path";
|
|
355
|
+
import { materializeLessonkitProject } from "@lxpack/validators";
|
|
281
356
|
|
|
282
|
-
// src/
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
lines.push(` - id: ${question.id}`);
|
|
294
|
-
lines.push(` prompt: ${yamlQuote(question.prompt)}`);
|
|
295
|
-
lines.push(" choices:");
|
|
296
|
-
for (const choice of question.choices) {
|
|
297
|
-
lines.push(` - id: ${choice.id}`);
|
|
298
|
-
lines.push(` text: ${yamlQuote(choice.text)}`);
|
|
299
|
-
if (choice.correct) lines.push(" correct: true");
|
|
357
|
+
// src/spaDirs.ts
|
|
358
|
+
import { access } from "fs/promises";
|
|
359
|
+
import { resolve as resolve3 } from "path";
|
|
360
|
+
async function resolveSpaDirs(options) {
|
|
361
|
+
const { descriptor, spaDistDir, lessonSpaDirs, projectRoot } = options;
|
|
362
|
+
const spaLessons = resolveSpaLessons(descriptor);
|
|
363
|
+
if (descriptor.layout === "single-spa") {
|
|
364
|
+
const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? "dist";
|
|
365
|
+
const srcDist = projectRoot ? resolve3(projectRoot, spaDistRelative) : resolve3(spaDistRelative);
|
|
366
|
+
if (projectRoot) {
|
|
367
|
+
assertResolvedPathUnderRoot(resolve3(projectRoot), srcDist);
|
|
300
368
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// src/yaml.ts
|
|
307
|
-
function yamlQuote2(value) {
|
|
308
|
-
if (/[:#\n\r]/.test(value) || value.startsWith(" ") || value.endsWith(" ")) {
|
|
309
|
-
return JSON.stringify(value);
|
|
310
|
-
}
|
|
311
|
-
return value;
|
|
312
|
-
}
|
|
313
|
-
function emitCourseYaml(opts) {
|
|
314
|
-
const lines = [];
|
|
315
|
-
lines.push(`title: ${yamlQuote2(opts.title)}`);
|
|
316
|
-
lines.push(`version: ${yamlQuote2(opts.version)}`);
|
|
317
|
-
if (opts.description) lines.push(`description: ${yamlQuote2(opts.description)}`);
|
|
318
|
-
if (opts.runtime) {
|
|
319
|
-
lines.push("runtime:");
|
|
320
|
-
lines.push(` theme: ${yamlQuote2(opts.runtime.theme)}`);
|
|
321
|
-
if (opts.runtime.cssVariables && Object.keys(opts.runtime.cssVariables).length) {
|
|
322
|
-
lines.push(" cssVariables:");
|
|
323
|
-
for (const [key, value] of Object.entries(opts.runtime.cssVariables).sort(
|
|
324
|
-
([a], [b]) => a.localeCompare(b)
|
|
325
|
-
)) {
|
|
326
|
-
lines.push(` ${key}: ${JSON.stringify(String(value))}`);
|
|
327
|
-
}
|
|
369
|
+
try {
|
|
370
|
+
await access(srcDist);
|
|
371
|
+
} catch {
|
|
372
|
+
throw new Error(`spaDistDir not found: ${srcDist}`);
|
|
328
373
|
}
|
|
374
|
+
const lessonId = spaLessons[0]?.id ?? "main";
|
|
375
|
+
return { [lessonId]: srcDist };
|
|
329
376
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
for (const lesson of opts.lessons) {
|
|
337
|
-
lines.push(` - id: ${lesson.id}`);
|
|
338
|
-
lines.push(` title: ${yamlQuote2(lesson.title)}`);
|
|
339
|
-
lines.push(` type: spa`);
|
|
340
|
-
lines.push(` path: ${lesson.path}`);
|
|
341
|
-
}
|
|
342
|
-
if (opts.assessments.length) {
|
|
343
|
-
lines.push("assessments:");
|
|
344
|
-
for (const assessment of opts.assessments) {
|
|
345
|
-
lines.push(` - id: ${assessment.id}`);
|
|
346
|
-
lines.push(` file: ${assessment.file}`);
|
|
377
|
+
const dirs = {};
|
|
378
|
+
const lessonDirs = lessonSpaDirs ?? {};
|
|
379
|
+
for (const lesson of descriptor.lessons) {
|
|
380
|
+
const src = lessonDirs[lesson.id];
|
|
381
|
+
if (!src) {
|
|
382
|
+
throw new Error(`lessonSpaDirs missing build output for lesson "${lesson.id}"`);
|
|
347
383
|
}
|
|
384
|
+
dirs[lesson.id] = resolve3(src);
|
|
348
385
|
}
|
|
349
|
-
return
|
|
350
|
-
`;
|
|
386
|
+
return dirs;
|
|
351
387
|
}
|
|
352
388
|
|
|
353
389
|
// src/writeProject.ts
|
|
354
|
-
async function copyDir(src, dest) {
|
|
355
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
356
|
-
await cp(src, dest, { recursive: true });
|
|
357
|
-
}
|
|
358
390
|
async function writeLxpackProject(options) {
|
|
359
391
|
const validation = validateDescriptor(options.descriptor);
|
|
360
392
|
if (!validation.ok) {
|
|
@@ -363,93 +395,46 @@ async function writeLxpackProject(options) {
|
|
|
363
395
|
);
|
|
364
396
|
}
|
|
365
397
|
const descriptor = validation.descriptor;
|
|
366
|
-
const outDir =
|
|
367
|
-
await
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
throw new Error(`spaDistDir not found: ${srcDist}`);
|
|
380
|
-
}
|
|
381
|
-
const destDist = join(outDir, "dist");
|
|
382
|
-
await rm(destDist, { recursive: true, force: true });
|
|
383
|
-
await copyDir(srcDist, destDist);
|
|
384
|
-
} else {
|
|
385
|
-
const lessonDirs = options.lessonSpaDirs ?? {};
|
|
386
|
-
for (const lesson of descriptor.lessons) {
|
|
387
|
-
const src = lessonDirs[lesson.id];
|
|
388
|
-
if (!src) {
|
|
389
|
-
throw new Error(`lessonSpaDirs missing build output for lesson "${lesson.id}"`);
|
|
390
|
-
}
|
|
391
|
-
const dest = join(outDir, lesson.spaPath);
|
|
392
|
-
assertResolvedPathUnderRoot(outDir, dest);
|
|
393
|
-
await rm(dest, { recursive: true, force: true });
|
|
394
|
-
await copyDir(resolve2(src), dest);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
if (assessments.length) {
|
|
398
|
-
const assessmentsDir = join(outDir, "assessments");
|
|
399
|
-
await mkdir(assessmentsDir, { recursive: true });
|
|
400
|
-
for (const assessment of descriptor.assessments ?? []) {
|
|
401
|
-
await writeFile(
|
|
402
|
-
join(outDir, `assessments/${assessment.checkId}.yaml`),
|
|
403
|
-
emitAssessmentYaml(assessment),
|
|
404
|
-
"utf-8"
|
|
405
|
-
);
|
|
406
|
-
}
|
|
398
|
+
const outDir = resolve4(options.outDir);
|
|
399
|
+
const spaDirs = await resolveSpaDirs({ ...options, descriptor });
|
|
400
|
+
const interchange = descriptorToInterchange(descriptor);
|
|
401
|
+
const materialized = await materializeLessonkitProject({
|
|
402
|
+
interchange,
|
|
403
|
+
spaDirs,
|
|
404
|
+
courseDir: outDir,
|
|
405
|
+
writeAuthoringFiles: true
|
|
406
|
+
});
|
|
407
|
+
if (!materialized.ok) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
materialized.issues.map((i) => `${i.path ?? ""}: ${i.message}`.trim()).join("; ")
|
|
410
|
+
);
|
|
407
411
|
}
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
runtime,
|
|
415
|
-
tracking: descriptor.tracking,
|
|
416
|
-
lessons: spaLessons.map((l) => ({
|
|
417
|
-
id: l.id,
|
|
418
|
-
title: l.title,
|
|
419
|
-
type: "spa",
|
|
420
|
-
path: l.path
|
|
421
|
-
})),
|
|
422
|
-
assessments
|
|
423
|
-
}),
|
|
424
|
-
"utf-8"
|
|
425
|
-
);
|
|
426
|
-
const lessonkitJsonPath = join(outDir, "lessonkit.json");
|
|
427
|
-
await writeFile(
|
|
428
|
-
lessonkitJsonPath,
|
|
429
|
-
`${JSON.stringify(descriptorToInterchange(descriptor), null, 2)}
|
|
430
|
-
`,
|
|
431
|
-
"utf-8"
|
|
432
|
-
);
|
|
433
|
-
return { outDir, courseYamlPath, lessonkitJsonPath };
|
|
412
|
+
const courseDir = materialized.courseDir;
|
|
413
|
+
return {
|
|
414
|
+
outDir: courseDir,
|
|
415
|
+
courseYamlPath: join(courseDir, "course.yaml"),
|
|
416
|
+
lessonkitJsonPath: join(courseDir, "lessonkit.json")
|
|
417
|
+
};
|
|
434
418
|
}
|
|
435
419
|
|
|
436
420
|
// src/packageCourse.ts
|
|
437
|
-
import
|
|
438
|
-
import { dirname
|
|
421
|
+
import * as fsp from "fs/promises";
|
|
422
|
+
import { dirname, join as join2, resolve as resolve5 } from "path";
|
|
439
423
|
import { tmpdir } from "os";
|
|
440
424
|
import {
|
|
441
425
|
buildCourse,
|
|
426
|
+
packageLessonkit,
|
|
442
427
|
validateCourse
|
|
443
428
|
} from "@lxpack/api";
|
|
444
429
|
async function validateLessonkitProject(options) {
|
|
445
430
|
return validateCourse({
|
|
446
|
-
courseDir:
|
|
431
|
+
courseDir: resolve5(options.courseDir),
|
|
447
432
|
target: options.target
|
|
448
433
|
});
|
|
449
434
|
}
|
|
450
435
|
async function buildLessonkitProject(options) {
|
|
451
436
|
return buildCourse({
|
|
452
|
-
courseDir:
|
|
437
|
+
courseDir: resolve5(options.courseDir),
|
|
453
438
|
target: options.target,
|
|
454
439
|
output: options.output,
|
|
455
440
|
dir: options.dir,
|
|
@@ -457,9 +442,38 @@ async function buildLessonkitProject(options) {
|
|
|
457
442
|
assessments: options.assessments
|
|
458
443
|
});
|
|
459
444
|
}
|
|
445
|
+
async function pathExists(path) {
|
|
446
|
+
try {
|
|
447
|
+
await fsp.access(path);
|
|
448
|
+
return true;
|
|
449
|
+
} catch {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
454
|
+
const tmpPromote = `${outDir}.tmp-promote`;
|
|
455
|
+
const backup = `${outDir}.bak`;
|
|
456
|
+
await fsp.rename(stagingDir, tmpPromote);
|
|
457
|
+
const hadOutDir = await pathExists(outDir);
|
|
458
|
+
if (hadOutDir) {
|
|
459
|
+
await fsp.rename(outDir, backup);
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
await fsp.rename(tmpPromote, outDir);
|
|
463
|
+
} catch (promoteError) {
|
|
464
|
+
if (hadOutDir) {
|
|
465
|
+
await fsp.rename(backup, outDir).catch(() => void 0);
|
|
466
|
+
}
|
|
467
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
468
|
+
throw promoteError;
|
|
469
|
+
}
|
|
470
|
+
if (hadOutDir) {
|
|
471
|
+
await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
460
474
|
async function packageLessonkitCourse(options) {
|
|
461
475
|
const { target, output, dir, outputBaseDir, ...writeOpts } = options;
|
|
462
|
-
const outDir =
|
|
476
|
+
const outDir = resolve5(writeOpts.outDir);
|
|
463
477
|
const descriptorValidation = validateDescriptor(writeOpts.descriptor);
|
|
464
478
|
if (!descriptorValidation.ok) {
|
|
465
479
|
return {
|
|
@@ -473,42 +487,50 @@ async function packageLessonkitCourse(options) {
|
|
|
473
487
|
};
|
|
474
488
|
}
|
|
475
489
|
const descriptor = descriptorValidation.descriptor;
|
|
476
|
-
const stagingDir = await mkdtemp(join2(tmpdir(), "lessonkit-lxpack-"));
|
|
490
|
+
const stagingDir = await fsp.mkdtemp(join2(tmpdir(), "lessonkit-lxpack-"));
|
|
477
491
|
let promoted = false;
|
|
478
492
|
try {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (!validation.ok) {
|
|
493
|
+
let spaDirs;
|
|
494
|
+
try {
|
|
495
|
+
spaDirs = await resolveSpaDirs({ ...writeOpts, descriptor });
|
|
496
|
+
} catch (err) {
|
|
484
497
|
return {
|
|
485
498
|
ok: false,
|
|
486
499
|
courseDir: outDir,
|
|
487
500
|
target,
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
501
|
+
issues: [
|
|
502
|
+
{
|
|
503
|
+
path: "spaDirs",
|
|
504
|
+
message: err instanceof Error ? err.message : String(err)
|
|
505
|
+
}
|
|
506
|
+
]
|
|
494
507
|
};
|
|
495
508
|
}
|
|
509
|
+
const interchange = descriptorToInterchange(descriptor);
|
|
496
510
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
497
|
-
await
|
|
511
|
+
await fsp.mkdir(join2(stagingDir, outputBase), { recursive: true });
|
|
498
512
|
const defaultOutput = output ?? (dir ? join2(outputBase, target) : join2(outputBase, `course-${target}.zip`));
|
|
499
|
-
const build = await
|
|
500
|
-
|
|
513
|
+
const build = await packageLessonkit({
|
|
514
|
+
interchange,
|
|
515
|
+
spaDirs,
|
|
501
516
|
target,
|
|
502
|
-
|
|
517
|
+
courseDir: stagingDir,
|
|
518
|
+
output: defaultOutput,
|
|
503
519
|
dir,
|
|
504
|
-
|
|
520
|
+
outputBaseDir,
|
|
521
|
+
outputAnchorDir: stagingDir,
|
|
522
|
+
writeAuthoringFiles: true
|
|
505
523
|
});
|
|
506
524
|
if (!build.ok) {
|
|
525
|
+
const validation2 = {
|
|
526
|
+
ok: false,
|
|
527
|
+
issues: build.issues
|
|
528
|
+
};
|
|
507
529
|
return {
|
|
508
530
|
ok: false,
|
|
509
531
|
courseDir: outDir,
|
|
510
532
|
target,
|
|
511
|
-
validation,
|
|
533
|
+
validation: validation2,
|
|
512
534
|
build,
|
|
513
535
|
issues: build.issues.map((i) => ({
|
|
514
536
|
path: i.path,
|
|
@@ -517,45 +539,81 @@ async function packageLessonkitCourse(options) {
|
|
|
517
539
|
}))
|
|
518
540
|
};
|
|
519
541
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
542
|
+
const validation = {
|
|
543
|
+
ok: true,
|
|
544
|
+
manifest: build.manifest,
|
|
545
|
+
issues: build.issues
|
|
546
|
+
};
|
|
547
|
+
const stagingRoot = await fsp.realpath(stagingDir);
|
|
524
548
|
const remapArtifactPath = (artifactPath) => {
|
|
525
549
|
if (!artifactPath) return void 0;
|
|
526
|
-
const resolved =
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
return join2(outDir, resolved.slice(stagingResolved.length + 1));
|
|
550
|
+
const resolved = resolve5(artifactPath);
|
|
551
|
+
if (resolved === stagingRoot || resolved.startsWith(`${stagingRoot}/`)) {
|
|
552
|
+
return join2(outDir, resolved.slice(stagingRoot.length + 1));
|
|
530
553
|
}
|
|
531
554
|
return artifactPath;
|
|
532
555
|
};
|
|
556
|
+
const remappedOutputPath = remapArtifactPath(
|
|
557
|
+
"outputPath" in build ? build.outputPath : void 0
|
|
558
|
+
);
|
|
559
|
+
const remappedOutputDir = remapArtifactPath("outputDir" in build ? build.outputDir : void 0);
|
|
560
|
+
await fsp.mkdir(dirname(outDir), { recursive: true });
|
|
561
|
+
await promoteStagingToOutDir(stagingDir, outDir);
|
|
562
|
+
promoted = true;
|
|
563
|
+
const remappedBuild = { ...build };
|
|
564
|
+
if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
|
|
565
|
+
remappedBuild.outputPath = remappedOutputPath;
|
|
566
|
+
}
|
|
567
|
+
if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
|
|
568
|
+
remappedBuild.outputDir = remappedOutputDir;
|
|
569
|
+
}
|
|
533
570
|
return {
|
|
534
571
|
ok: true,
|
|
535
572
|
courseDir: outDir,
|
|
536
573
|
target,
|
|
537
|
-
outputPath:
|
|
538
|
-
outputDir:
|
|
574
|
+
outputPath: remappedOutputPath,
|
|
575
|
+
outputDir: remappedOutputDir,
|
|
539
576
|
fileCount: build.fileCount,
|
|
540
577
|
validation,
|
|
541
|
-
build
|
|
578
|
+
build: remappedBuild
|
|
542
579
|
};
|
|
543
580
|
} finally {
|
|
544
581
|
if (!promoted) {
|
|
545
|
-
await
|
|
582
|
+
await fsp.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
546
583
|
}
|
|
547
584
|
}
|
|
548
585
|
}
|
|
586
|
+
|
|
587
|
+
// src/index.ts
|
|
588
|
+
import {
|
|
589
|
+
LESSONKIT_TELEMETRY_EVENTS,
|
|
590
|
+
mapLessonkitTelemetryToBridgeAction,
|
|
591
|
+
mapLessonkitTelemetryToLxpack
|
|
592
|
+
} from "@lxpack/tracking-schema";
|
|
593
|
+
import {
|
|
594
|
+
lessonkitInterchangeSchema,
|
|
595
|
+
materializeLessonkitProject as materializeLessonkitProject2,
|
|
596
|
+
parseLessonkitInterchange
|
|
597
|
+
} from "@lxpack/validators";
|
|
549
598
|
export {
|
|
599
|
+
LESSONKIT_TELEMETRY_EVENTS,
|
|
550
600
|
assessmentDescriptorToLxpack,
|
|
551
601
|
buildLessonkitProject,
|
|
552
602
|
descriptorToInterchange,
|
|
553
603
|
extractAssessments,
|
|
604
|
+
lessonkitInterchangeSchema,
|
|
554
605
|
mapLessonkitIds,
|
|
606
|
+
mapLessonkitTelemetryToBridgeAction,
|
|
607
|
+
mapLessonkitTelemetryToLxpack,
|
|
608
|
+
materializeLessonkitProject2 as materializeLessonkitProject,
|
|
555
609
|
packageLessonkitCourse,
|
|
610
|
+
parseLessonkitInterchange,
|
|
611
|
+
resolveSafePackageOutputOverride,
|
|
556
612
|
resolveSpaLessons,
|
|
613
|
+
telemetryEventToLessonkit,
|
|
557
614
|
themeToLxpackRuntime,
|
|
558
615
|
validateDescriptor,
|
|
559
616
|
validateLessonkitProject,
|
|
617
|
+
validateProjectPaths,
|
|
560
618
|
writeLxpackProject
|
|
561
619
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TelemetryEvent } from '@lessonkit/core';
|
|
2
|
+
import { LessonkitTelemetryEvent } from '@lxpack/tracking-schema';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Map a `@lessonkit/core` telemetry event to the LXPack LessonKit telemetry shape.
|
|
6
|
+
*/
|
|
7
|
+
declare function telemetryEventToLessonkit(event: TelemetryEvent): LessonkitTelemetryEvent | null;
|
|
8
|
+
|
|
9
|
+
export { telemetryEventToLessonkit as t };
|