@lessonkit/lxpack 0.7.0 → 0.8.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 +9 -1
- package/dist/bridge.cjs +8 -3
- package/dist/bridge.d.cts +8 -2
- package/dist/bridge.d.ts +8 -2
- package/dist/bridge.js +8 -3
- package/dist/index.cjs +180 -59
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +158 -49
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# @lessonkit/lxpack
|
|
2
2
|
|
|
3
|
+
[](https://lessonkit.readthedocs.io/en/latest/)
|
|
4
|
+
[](https://www.npmjs.com/package/@lessonkit/lxpack)
|
|
5
|
+
[](https://github.com/eddiethedean/lessonkit/blob/main/LICENSE)
|
|
6
|
+
|
|
3
7
|
LXPack export adapter for LessonKit — write `lessonkit.json` + `course.yaml`, copy SPA builds, and package to SCORM / standalone / xAPI / cmi5 via [`@lxpack/api`](https://www.npmjs.com/package/@lxpack/api).
|
|
4
8
|
|
|
5
9
|
Requires **Node.js 20+**.
|
|
6
10
|
|
|
11
|
+
**Docs:** [Packaging reference](https://lessonkit.readthedocs.io/en/latest/reference/packaging.html) · [Packaging & CLI guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html) · [Live examples](https://lessonkit.readthedocs.io/en/latest/examples/index.html)
|
|
12
|
+
|
|
7
13
|
## Install
|
|
8
14
|
|
|
9
15
|
```bash
|
|
@@ -27,7 +33,7 @@ const result = await packageLessonkitCourse({
|
|
|
27
33
|
if (!result.ok) throw new Error("packaging failed");
|
|
28
34
|
```
|
|
29
35
|
|
|
30
|
-
See [
|
|
36
|
+
See the [packaging reference](https://lessonkit.readthedocs.io/en/latest/reference/packaging.html) and the [`examples/lxpack-golden`](https://github.com/eddiethedean/lessonkit/tree/main/examples/lxpack-golden) course.
|
|
31
37
|
|
|
32
38
|
## Browser bridge
|
|
33
39
|
|
|
@@ -38,3 +44,5 @@ import { notifyLxpackLessonComplete } from "@lessonkit/lxpack/bridge";
|
|
|
38
44
|
```
|
|
39
45
|
|
|
40
46
|
`@lessonkit/react` forwards `lesson_completed`, `course_completed`, and `quiz_completed` automatically when `window.parent.lxpackBridge.v1` is present (`config.lxpack.bridge: "off"` to disable).
|
|
47
|
+
|
|
48
|
+
For interoperability notes, see [LXPack upgrades](https://lessonkit.readthedocs.io/en/latest/reference/lxpack-upgrades.html). LXPack maintainers: [upgrade plan for maintainers](https://github.com/eddiethedean/lessonkit/blob/main/docs/LXPACK_UPGRADE_PLAN_FOR_MAINTAINERS.md).
|
package/dist/bridge.cjs
CHANGED
|
@@ -33,10 +33,15 @@ function normalizeAssessmentScore(opts) {
|
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
const maxScore = typeof opts.maxScore === "number" && opts.maxScore > 0 ? opts.maxScore : 1;
|
|
36
|
-
return opts.score / maxScore;
|
|
36
|
+
return Math.min(1, opts.score / maxScore);
|
|
37
37
|
}
|
|
38
|
-
function normalizeAssessmentPassingScore(
|
|
39
|
-
|
|
38
|
+
function normalizeAssessmentPassingScore(opts) {
|
|
39
|
+
const passingScore = opts?.passingScore;
|
|
40
|
+
if (typeof passingScore !== "number" || !Number.isFinite(passingScore) || passingScore <= 0) {
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
const maxScore = typeof opts?.maxScore === "number" && opts.maxScore > 0 ? opts.maxScore : 1;
|
|
44
|
+
return Math.min(1, passingScore / maxScore);
|
|
40
45
|
}
|
|
41
46
|
function getBridge() {
|
|
42
47
|
if (typeof window === "undefined") return null;
|
package/dist/bridge.d.cts
CHANGED
|
@@ -28,8 +28,14 @@ declare function normalizeAssessmentScore(opts: {
|
|
|
28
28
|
score?: number;
|
|
29
29
|
maxScore?: number;
|
|
30
30
|
}): number | null;
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Scale a raw passing threshold to 0–1 for the LXPack parent bridge.
|
|
33
|
+
* Uses the same `maxScore` denominator as `normalizeAssessmentScore`. Defaults to 1 when omitted or invalid.
|
|
34
|
+
*/
|
|
35
|
+
declare function normalizeAssessmentPassingScore(opts?: {
|
|
36
|
+
passingScore?: number;
|
|
37
|
+
maxScore?: number;
|
|
38
|
+
}): number;
|
|
33
39
|
declare function createLxpackBridge(): LxpackBridgeV1 | null;
|
|
34
40
|
declare function notifyLxpackLessonComplete(lessonId: LessonId): boolean;
|
|
35
41
|
declare function notifyLxpackCourseComplete(): boolean;
|
package/dist/bridge.d.ts
CHANGED
|
@@ -28,8 +28,14 @@ declare function normalizeAssessmentScore(opts: {
|
|
|
28
28
|
score?: number;
|
|
29
29
|
maxScore?: number;
|
|
30
30
|
}): number | null;
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Scale a raw passing threshold to 0–1 for the LXPack parent bridge.
|
|
33
|
+
* Uses the same `maxScore` denominator as `normalizeAssessmentScore`. Defaults to 1 when omitted or invalid.
|
|
34
|
+
*/
|
|
35
|
+
declare function normalizeAssessmentPassingScore(opts?: {
|
|
36
|
+
passingScore?: number;
|
|
37
|
+
maxScore?: number;
|
|
38
|
+
}): number;
|
|
33
39
|
declare function createLxpackBridge(): LxpackBridgeV1 | null;
|
|
34
40
|
declare function notifyLxpackLessonComplete(lessonId: LessonId): boolean;
|
|
35
41
|
declare function notifyLxpackCourseComplete(): boolean;
|
package/dist/bridge.js
CHANGED
|
@@ -4,10 +4,15 @@ function normalizeAssessmentScore(opts) {
|
|
|
4
4
|
return null;
|
|
5
5
|
}
|
|
6
6
|
const maxScore = typeof opts.maxScore === "number" && opts.maxScore > 0 ? opts.maxScore : 1;
|
|
7
|
-
return opts.score / maxScore;
|
|
7
|
+
return Math.min(1, opts.score / maxScore);
|
|
8
8
|
}
|
|
9
|
-
function normalizeAssessmentPassingScore(
|
|
10
|
-
|
|
9
|
+
function normalizeAssessmentPassingScore(opts) {
|
|
10
|
+
const passingScore = opts?.passingScore;
|
|
11
|
+
if (typeof passingScore !== "number" || !Number.isFinite(passingScore) || passingScore <= 0) {
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
14
|
+
const maxScore = typeof opts?.maxScore === "number" && opts.maxScore > 0 ? opts.maxScore : 1;
|
|
15
|
+
return Math.min(1, passingScore / maxScore);
|
|
11
16
|
}
|
|
12
17
|
function getBridge() {
|
|
13
18
|
if (typeof window === "undefined") return null;
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -26,10 +36,12 @@ __export(index_exports, {
|
|
|
26
36
|
extractAssessments: () => extractAssessments,
|
|
27
37
|
mapLessonkitIds: () => mapLessonkitIds,
|
|
28
38
|
packageLessonkitCourse: () => packageLessonkitCourse,
|
|
39
|
+
resolveSafePackageOutputOverride: () => resolveSafePackageOutputOverride,
|
|
29
40
|
resolveSpaLessons: () => resolveSpaLessons,
|
|
30
41
|
themeToLxpackRuntime: () => themeToLxpackRuntime,
|
|
31
42
|
validateDescriptor: () => validateDescriptor,
|
|
32
43
|
validateLessonkitProject: () => validateLessonkitProject,
|
|
44
|
+
validateProjectPaths: () => validateProjectPaths,
|
|
33
45
|
writeLxpackProject: () => writeLxpackProject
|
|
34
46
|
});
|
|
35
47
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -43,8 +55,8 @@ function isSafeRelativeSpaPath(spaPath) {
|
|
|
43
55
|
if (!spaPath.length || spaPath.includes("\0")) return false;
|
|
44
56
|
if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
|
|
45
57
|
if (/^[a-zA-Z]:[/\\]/.test(spaPath)) return false;
|
|
46
|
-
const segments = spaPath.split(/[/\\]/);
|
|
47
|
-
if (segments.some((s) => s === "..")) return false;
|
|
58
|
+
const segments = spaPath.split(/[/\\]/).filter((s) => s.length > 0);
|
|
59
|
+
if (segments.some((s) => s === ".." || s === ".")) return false;
|
|
48
60
|
return true;
|
|
49
61
|
}
|
|
50
62
|
function assertResolvedPathUnderRoot(root, target) {
|
|
@@ -56,6 +68,21 @@ function assertResolvedPathUnderRoot(root, target) {
|
|
|
56
68
|
}
|
|
57
69
|
}
|
|
58
70
|
|
|
71
|
+
// src/theme.ts
|
|
72
|
+
var import_themes = require("@lessonkit/themes");
|
|
73
|
+
function themeToLxpackRuntime(input) {
|
|
74
|
+
const theme = input.theme ?? (0, import_themes.getPresetTheme)(input.preset ?? "default");
|
|
75
|
+
const raw = (0, import_themes.themeToCssVariables)(theme);
|
|
76
|
+
const cssVariables = {};
|
|
77
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
78
|
+
cssVariables[key] = String(value);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
theme: theme.name,
|
|
82
|
+
cssVariables
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
59
86
|
// src/validateDescriptor.ts
|
|
60
87
|
var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
|
|
61
88
|
var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
|
|
@@ -117,6 +144,25 @@ function validateDescriptor(input) {
|
|
|
117
144
|
message: `unknown preset; use one of: ${VALID_THEME_PRESETS.join(", ")}`
|
|
118
145
|
});
|
|
119
146
|
}
|
|
147
|
+
if (input.theme?.theme) {
|
|
148
|
+
try {
|
|
149
|
+
themeToLxpackRuntime({ preset: themePreset, theme: input.theme.theme });
|
|
150
|
+
} catch (err) {
|
|
151
|
+
issues.push({
|
|
152
|
+
path: "theme.theme",
|
|
153
|
+
message: err instanceof Error ? err.message : "invalid custom theme"
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const completionThreshold = input.tracking?.completion?.threshold;
|
|
158
|
+
if (completionThreshold !== void 0) {
|
|
159
|
+
if (!Number.isFinite(completionThreshold) || completionThreshold < 0 || completionThreshold > 1) {
|
|
160
|
+
issues.push({
|
|
161
|
+
path: "tracking.completion.threshold",
|
|
162
|
+
message: "threshold must be a finite number between 0 and 1"
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
120
166
|
if (layout === "single-spa" && (input.lessons?.length ?? 0) > 1) {
|
|
121
167
|
issues.push({
|
|
122
168
|
path: "lessons",
|
|
@@ -196,24 +242,69 @@ function validateDescriptor(input) {
|
|
|
196
242
|
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
197
243
|
}
|
|
198
244
|
const passingScore = assessment.passingScore;
|
|
199
|
-
if (passingScore !== void 0) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
});
|
|
205
|
-
} else if (trimmedChoices.length && passingScore > trimmedChoices.length) {
|
|
206
|
-
issues.push({
|
|
207
|
-
path: `${path}.passingScore`,
|
|
208
|
-
message: "passingScore must not exceed the number of choices"
|
|
209
|
-
});
|
|
210
|
-
}
|
|
245
|
+
if (passingScore !== void 0 && !(passingScore > 0)) {
|
|
246
|
+
issues.push({
|
|
247
|
+
path: `${path}.passingScore`,
|
|
248
|
+
message: "passingScore must be greater than 0 (absolute point threshold)"
|
|
249
|
+
});
|
|
211
250
|
}
|
|
212
251
|
}
|
|
213
252
|
if (issues.length) return { ok: false, issues };
|
|
214
253
|
return { ok: true, descriptor: normalizeDescriptor(input) };
|
|
215
254
|
}
|
|
216
255
|
|
|
256
|
+
// src/validateProjectPaths.ts
|
|
257
|
+
var import_node_path2 = require("path");
|
|
258
|
+
function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
259
|
+
if (!isSafeRelativeSpaPath(value)) {
|
|
260
|
+
issues.push({
|
|
261
|
+
path: fieldPath,
|
|
262
|
+
message: "path must be relative without '..' segments or absolute prefixes"
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
assertResolvedPathUnderRoot(projectRoot, (0, import_node_path2.resolve)(projectRoot, value));
|
|
268
|
+
} catch {
|
|
269
|
+
issues.push({
|
|
270
|
+
path: fieldPath,
|
|
271
|
+
message: "path must resolve inside the project root"
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function validateProjectPaths(projectRoot, paths) {
|
|
276
|
+
const issues = [];
|
|
277
|
+
const root = (0, import_node_path2.resolve)(projectRoot);
|
|
278
|
+
if (paths.spaDistDir?.trim()) {
|
|
279
|
+
validatePathField(paths.spaDistDir.trim(), "paths.spaDistDir", root, issues);
|
|
280
|
+
}
|
|
281
|
+
if (paths.lxpackOutDir?.trim()) {
|
|
282
|
+
validatePathField(paths.lxpackOutDir.trim(), "paths.lxpackOutDir", root, issues);
|
|
283
|
+
}
|
|
284
|
+
if (paths.outputBaseDir?.trim()) {
|
|
285
|
+
validatePathField(paths.outputBaseDir.trim(), "paths.outputBaseDir", root, issues);
|
|
286
|
+
}
|
|
287
|
+
return issues;
|
|
288
|
+
}
|
|
289
|
+
function resolveSafePackageOutputOverride(projectRoot, override) {
|
|
290
|
+
const root = (0, import_node_path2.resolve)(projectRoot);
|
|
291
|
+
const trimmed = override.trim();
|
|
292
|
+
if (!trimmed) {
|
|
293
|
+
throw new Error("output override must be a non-empty path");
|
|
294
|
+
}
|
|
295
|
+
if ((0, import_node_path2.isAbsolute)(trimmed)) {
|
|
296
|
+
const resolved2 = (0, import_node_path2.resolve)(trimmed);
|
|
297
|
+
assertResolvedPathUnderRoot(root, resolved2);
|
|
298
|
+
return resolved2;
|
|
299
|
+
}
|
|
300
|
+
if (!isSafeRelativeSpaPath(trimmed)) {
|
|
301
|
+
throw new Error(`unsafe output path: ${override}`);
|
|
302
|
+
}
|
|
303
|
+
const resolved = (0, import_node_path2.resolve)(root, trimmed);
|
|
304
|
+
assertResolvedPathUnderRoot(root, resolved);
|
|
305
|
+
return resolved;
|
|
306
|
+
}
|
|
307
|
+
|
|
217
308
|
// src/mapIds.ts
|
|
218
309
|
var import_core2 = require("@lessonkit/core");
|
|
219
310
|
function mapLessonkitIds(descriptor) {
|
|
@@ -225,21 +316,6 @@ function mapLessonkitIds(descriptor) {
|
|
|
225
316
|
return { courseId, lessonIds, checkIds };
|
|
226
317
|
}
|
|
227
318
|
|
|
228
|
-
// src/theme.ts
|
|
229
|
-
var import_themes = require("@lessonkit/themes");
|
|
230
|
-
function themeToLxpackRuntime(input) {
|
|
231
|
-
const theme = input.theme ?? (0, import_themes.getPresetTheme)(input.preset ?? "default");
|
|
232
|
-
const raw = (0, import_themes.themeToCssVariables)(theme);
|
|
233
|
-
const cssVariables = {};
|
|
234
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
235
|
-
cssVariables[key] = String(value);
|
|
236
|
-
}
|
|
237
|
-
return {
|
|
238
|
-
theme: theme.name,
|
|
239
|
-
cssVariables
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
319
|
// src/interchange.ts
|
|
244
320
|
function resolveSpaLessons(descriptor) {
|
|
245
321
|
const mapped = mapLessonkitIds(descriptor);
|
|
@@ -313,7 +389,7 @@ function extractAssessments(descriptor) {
|
|
|
313
389
|
|
|
314
390
|
// src/writeProject.ts
|
|
315
391
|
var import_promises = require("fs/promises");
|
|
316
|
-
var
|
|
392
|
+
var import_node_path3 = require("path");
|
|
317
393
|
|
|
318
394
|
// src/assessmentYaml.ts
|
|
319
395
|
function yamlQuote(value) {
|
|
@@ -388,7 +464,7 @@ function emitCourseYaml(opts) {
|
|
|
388
464
|
|
|
389
465
|
// src/writeProject.ts
|
|
390
466
|
async function copyDir(src, dest) {
|
|
391
|
-
await (0, import_promises.mkdir)((0,
|
|
467
|
+
await (0, import_promises.mkdir)((0, import_node_path3.dirname)(dest), { recursive: true });
|
|
392
468
|
await (0, import_promises.cp)(src, dest, { recursive: true });
|
|
393
469
|
}
|
|
394
470
|
async function writeLxpackProject(options) {
|
|
@@ -399,7 +475,7 @@ async function writeLxpackProject(options) {
|
|
|
399
475
|
);
|
|
400
476
|
}
|
|
401
477
|
const descriptor = validation.descriptor;
|
|
402
|
-
const outDir = (0,
|
|
478
|
+
const outDir = (0, import_node_path3.resolve)(options.outDir);
|
|
403
479
|
await (0, import_promises.mkdir)(outDir, { recursive: true });
|
|
404
480
|
const spaLessons = resolveSpaLessons(descriptor);
|
|
405
481
|
const runtime = descriptor.theme ? themeToLxpackRuntime(descriptor.theme) : void 0;
|
|
@@ -408,13 +484,17 @@ async function writeLxpackProject(options) {
|
|
|
408
484
|
file: `assessments/${a.checkId}.yaml`
|
|
409
485
|
}));
|
|
410
486
|
if (descriptor.layout === "single-spa") {
|
|
411
|
-
const
|
|
487
|
+
const spaDistRelative = options.spaDistDir ?? descriptor.spaDistDir ?? "dist";
|
|
488
|
+
const srcDist = options.projectRoot ? (0, import_node_path3.resolve)(options.projectRoot, spaDistRelative) : (0, import_node_path3.resolve)(spaDistRelative);
|
|
489
|
+
if (options.projectRoot) {
|
|
490
|
+
assertResolvedPathUnderRoot((0, import_node_path3.resolve)(options.projectRoot), srcDist);
|
|
491
|
+
}
|
|
412
492
|
try {
|
|
413
493
|
await (0, import_promises.access)(srcDist);
|
|
414
494
|
} catch {
|
|
415
495
|
throw new Error(`spaDistDir not found: ${srcDist}`);
|
|
416
496
|
}
|
|
417
|
-
const destDist = (0,
|
|
497
|
+
const destDist = (0, import_node_path3.join)(outDir, "dist");
|
|
418
498
|
await (0, import_promises.rm)(destDist, { recursive: true, force: true });
|
|
419
499
|
await copyDir(srcDist, destDist);
|
|
420
500
|
} else {
|
|
@@ -424,24 +504,24 @@ async function writeLxpackProject(options) {
|
|
|
424
504
|
if (!src) {
|
|
425
505
|
throw new Error(`lessonSpaDirs missing build output for lesson "${lesson.id}"`);
|
|
426
506
|
}
|
|
427
|
-
const dest = (0,
|
|
507
|
+
const dest = (0, import_node_path3.join)(outDir, lesson.spaPath);
|
|
428
508
|
assertResolvedPathUnderRoot(outDir, dest);
|
|
429
509
|
await (0, import_promises.rm)(dest, { recursive: true, force: true });
|
|
430
|
-
await copyDir((0,
|
|
510
|
+
await copyDir((0, import_node_path3.resolve)(src), dest);
|
|
431
511
|
}
|
|
432
512
|
}
|
|
433
513
|
if (assessments.length) {
|
|
434
|
-
const assessmentsDir = (0,
|
|
514
|
+
const assessmentsDir = (0, import_node_path3.join)(outDir, "assessments");
|
|
435
515
|
await (0, import_promises.mkdir)(assessmentsDir, { recursive: true });
|
|
436
516
|
for (const assessment of descriptor.assessments ?? []) {
|
|
437
517
|
await (0, import_promises.writeFile)(
|
|
438
|
-
(0,
|
|
518
|
+
(0, import_node_path3.join)(outDir, `assessments/${assessment.checkId}.yaml`),
|
|
439
519
|
emitAssessmentYaml(assessment),
|
|
440
520
|
"utf-8"
|
|
441
521
|
);
|
|
442
522
|
}
|
|
443
523
|
}
|
|
444
|
-
const courseYamlPath = (0,
|
|
524
|
+
const courseYamlPath = (0, import_node_path3.join)(outDir, "course.yaml");
|
|
445
525
|
await (0, import_promises.writeFile)(
|
|
446
526
|
courseYamlPath,
|
|
447
527
|
emitCourseYaml({
|
|
@@ -459,7 +539,7 @@ async function writeLxpackProject(options) {
|
|
|
459
539
|
}),
|
|
460
540
|
"utf-8"
|
|
461
541
|
);
|
|
462
|
-
const lessonkitJsonPath = (0,
|
|
542
|
+
const lessonkitJsonPath = (0, import_node_path3.join)(outDir, "lessonkit.json");
|
|
463
543
|
await (0, import_promises.writeFile)(
|
|
464
544
|
lessonkitJsonPath,
|
|
465
545
|
`${JSON.stringify(descriptorToInterchange(descriptor), null, 2)}
|
|
@@ -470,19 +550,19 @@ async function writeLxpackProject(options) {
|
|
|
470
550
|
}
|
|
471
551
|
|
|
472
552
|
// src/packageCourse.ts
|
|
473
|
-
var
|
|
474
|
-
var
|
|
553
|
+
var fsp = __toESM(require("fs/promises"), 1);
|
|
554
|
+
var import_node_path4 = require("path");
|
|
475
555
|
var import_node_os = require("os");
|
|
476
556
|
var import_api = require("@lxpack/api");
|
|
477
557
|
async function validateLessonkitProject(options) {
|
|
478
558
|
return (0, import_api.validateCourse)({
|
|
479
|
-
courseDir: (0,
|
|
559
|
+
courseDir: (0, import_node_path4.resolve)(options.courseDir),
|
|
480
560
|
target: options.target
|
|
481
561
|
});
|
|
482
562
|
}
|
|
483
563
|
async function buildLessonkitProject(options) {
|
|
484
564
|
return (0, import_api.buildCourse)({
|
|
485
|
-
courseDir: (0,
|
|
565
|
+
courseDir: (0, import_node_path4.resolve)(options.courseDir),
|
|
486
566
|
target: options.target,
|
|
487
567
|
output: options.output,
|
|
488
568
|
dir: options.dir,
|
|
@@ -490,9 +570,38 @@ async function buildLessonkitProject(options) {
|
|
|
490
570
|
assessments: options.assessments
|
|
491
571
|
});
|
|
492
572
|
}
|
|
573
|
+
async function pathExists(path) {
|
|
574
|
+
try {
|
|
575
|
+
await fsp.access(path);
|
|
576
|
+
return true;
|
|
577
|
+
} catch {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
582
|
+
const tmpPromote = `${outDir}.tmp-promote`;
|
|
583
|
+
const backup = `${outDir}.bak`;
|
|
584
|
+
await fsp.rename(stagingDir, tmpPromote);
|
|
585
|
+
const hadOutDir = await pathExists(outDir);
|
|
586
|
+
if (hadOutDir) {
|
|
587
|
+
await fsp.rename(outDir, backup);
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
await fsp.rename(tmpPromote, outDir);
|
|
591
|
+
} catch (promoteError) {
|
|
592
|
+
if (hadOutDir) {
|
|
593
|
+
await fsp.rename(backup, outDir).catch(() => void 0);
|
|
594
|
+
}
|
|
595
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
596
|
+
throw promoteError;
|
|
597
|
+
}
|
|
598
|
+
if (hadOutDir) {
|
|
599
|
+
await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
493
602
|
async function packageLessonkitCourse(options) {
|
|
494
603
|
const { target, output, dir, outputBaseDir, ...writeOpts } = options;
|
|
495
|
-
const outDir = (0,
|
|
604
|
+
const outDir = (0, import_node_path4.resolve)(writeOpts.outDir);
|
|
496
605
|
const descriptorValidation = validateDescriptor(writeOpts.descriptor);
|
|
497
606
|
if (!descriptorValidation.ok) {
|
|
498
607
|
return {
|
|
@@ -506,7 +615,7 @@ async function packageLessonkitCourse(options) {
|
|
|
506
615
|
};
|
|
507
616
|
}
|
|
508
617
|
const descriptor = descriptorValidation.descriptor;
|
|
509
|
-
const stagingDir = await
|
|
618
|
+
const stagingDir = await fsp.mkdtemp((0, import_node_path4.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
|
|
510
619
|
let promoted = false;
|
|
511
620
|
try {
|
|
512
621
|
const written = await writeLxpackProject({ ...writeOpts, descriptor, outDir: stagingDir });
|
|
@@ -527,12 +636,12 @@ async function packageLessonkitCourse(options) {
|
|
|
527
636
|
};
|
|
528
637
|
}
|
|
529
638
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
530
|
-
await
|
|
531
|
-
const defaultOutput = output ?? (dir ? (0,
|
|
639
|
+
await fsp.mkdir((0, import_node_path4.join)(courseDir, outputBase), { recursive: true });
|
|
640
|
+
const defaultOutput = output ?? (dir ? (0, import_node_path4.join)(outputBase, target) : (0, import_node_path4.join)(outputBase, `course-${target}.zip`));
|
|
532
641
|
const build = await buildLessonkitProject({
|
|
533
642
|
courseDir,
|
|
534
643
|
target,
|
|
535
|
-
output: defaultOutput.startsWith("/") ? defaultOutput : (0,
|
|
644
|
+
output: defaultOutput.startsWith("/") ? defaultOutput : (0, import_node_path4.join)(courseDir, defaultOutput),
|
|
536
645
|
dir,
|
|
537
646
|
assessments: assessments.length ? assessments : void 0
|
|
538
647
|
});
|
|
@@ -550,32 +659,42 @@ async function packageLessonkitCourse(options) {
|
|
|
550
659
|
}))
|
|
551
660
|
};
|
|
552
661
|
}
|
|
553
|
-
await (0,
|
|
554
|
-
await (
|
|
555
|
-
await (0, import_promises2.rename)(stagingDir, outDir);
|
|
662
|
+
await fsp.mkdir((0, import_node_path4.dirname)(outDir), { recursive: true });
|
|
663
|
+
await promoteStagingToOutDir(stagingDir, outDir);
|
|
556
664
|
promoted = true;
|
|
557
665
|
const remapArtifactPath = (artifactPath) => {
|
|
558
666
|
if (!artifactPath) return void 0;
|
|
559
|
-
const resolved = (0,
|
|
560
|
-
const stagingResolved = (0,
|
|
667
|
+
const resolved = (0, import_node_path4.resolve)(artifactPath);
|
|
668
|
+
const stagingResolved = (0, import_node_path4.resolve)(stagingDir);
|
|
561
669
|
if (resolved === stagingResolved || resolved.startsWith(stagingResolved + "/")) {
|
|
562
|
-
return (0,
|
|
670
|
+
return (0, import_node_path4.join)(outDir, resolved.slice(stagingResolved.length + 1));
|
|
563
671
|
}
|
|
564
672
|
return artifactPath;
|
|
565
673
|
};
|
|
674
|
+
const remappedOutputPath = remapArtifactPath(
|
|
675
|
+
"outputPath" in build ? build.outputPath : void 0
|
|
676
|
+
);
|
|
677
|
+
const remappedOutputDir = remapArtifactPath("outputDir" in build ? build.outputDir : void 0);
|
|
678
|
+
const remappedBuild = { ...build };
|
|
679
|
+
if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
|
|
680
|
+
remappedBuild.outputPath = remappedOutputPath;
|
|
681
|
+
}
|
|
682
|
+
if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
|
|
683
|
+
remappedBuild.outputDir = remappedOutputDir;
|
|
684
|
+
}
|
|
566
685
|
return {
|
|
567
686
|
ok: true,
|
|
568
687
|
courseDir: outDir,
|
|
569
688
|
target,
|
|
570
|
-
outputPath:
|
|
571
|
-
outputDir:
|
|
689
|
+
outputPath: remappedOutputPath,
|
|
690
|
+
outputDir: remappedOutputDir,
|
|
572
691
|
fileCount: build.fileCount,
|
|
573
692
|
validation,
|
|
574
|
-
build
|
|
693
|
+
build: remappedBuild
|
|
575
694
|
};
|
|
576
695
|
} finally {
|
|
577
696
|
if (!promoted) {
|
|
578
|
-
await
|
|
697
|
+
await fsp.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
579
698
|
}
|
|
580
699
|
}
|
|
581
700
|
}
|
|
@@ -587,9 +706,11 @@ async function packageLessonkitCourse(options) {
|
|
|
587
706
|
extractAssessments,
|
|
588
707
|
mapLessonkitIds,
|
|
589
708
|
packageLessonkitCourse,
|
|
709
|
+
resolveSafePackageOutputOverride,
|
|
590
710
|
resolveSpaLessons,
|
|
591
711
|
themeToLxpackRuntime,
|
|
592
712
|
validateDescriptor,
|
|
593
713
|
validateLessonkitProject,
|
|
714
|
+
validateProjectPaths,
|
|
594
715
|
writeLxpackProject
|
|
595
716
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -76,6 +76,18 @@ type DescriptorValidationResult = {
|
|
|
76
76
|
};
|
|
77
77
|
declare function validateDescriptor(input: LessonkitCourseDescriptor): DescriptorValidationResult;
|
|
78
78
|
|
|
79
|
+
type ProjectPathsInput = {
|
|
80
|
+
spaDistDir?: string;
|
|
81
|
+
lxpackOutDir?: string;
|
|
82
|
+
outputBaseDir?: string;
|
|
83
|
+
};
|
|
84
|
+
/** Validate lessonkit.json paths.* entries stay under projectRoot. */
|
|
85
|
+
declare function validateProjectPaths(projectRoot: string, paths: ProjectPathsInput): DescriptorValidationIssue[];
|
|
86
|
+
/**
|
|
87
|
+
* Resolve a package --out override under projectRoot and ensure it stays inside the project.
|
|
88
|
+
*/
|
|
89
|
+
declare function resolveSafePackageOutputOverride(projectRoot: string, override: string): string;
|
|
90
|
+
|
|
79
91
|
/**
|
|
80
92
|
* Stable 1:1 mapping from LessonKit ids to LXPack manifest ids.
|
|
81
93
|
* LessonKit slug rules already match LXPack `activityIdSchema` (letter-first, alphanumeric + _ -).
|
|
@@ -129,6 +141,8 @@ type WriteLxpackProjectOptions = {
|
|
|
129
141
|
* For `per-lesson-spa`: map lesson id → absolute path to that lesson's built SPA folder.
|
|
130
142
|
*/
|
|
131
143
|
lessonSpaDirs?: Record<string, string>;
|
|
144
|
+
/** When set, relative `spaDistDir` is resolved under this directory instead of `process.cwd()`. */
|
|
145
|
+
projectRoot?: string;
|
|
132
146
|
};
|
|
133
147
|
type WriteLxpackProjectResult = {
|
|
134
148
|
outDir: string;
|
|
@@ -181,4 +195,4 @@ declare function validateLessonkitProject(options: ValidateLessonkitProjectOptio
|
|
|
181
195
|
declare function buildLessonkitProject(options: BuildLessonkitProjectOptions): Promise<BuildCourseResult>;
|
|
182
196
|
declare function packageLessonkitCourse(options: PackageLessonkitCourseOptions): Promise<PackageLessonkitCourseResult>;
|
|
183
197
|
|
|
184
|
-
export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitInterchangeV1, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, descriptorToInterchange, extractAssessments, mapLessonkitIds, packageLessonkitCourse, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateLessonkitProject, writeLxpackProject };
|
|
198
|
+
export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitInterchangeV1, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, descriptorToInterchange, extractAssessments, mapLessonkitIds, packageLessonkitCourse, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateLessonkitProject, validateProjectPaths, writeLxpackProject };
|
package/dist/index.d.ts
CHANGED
|
@@ -76,6 +76,18 @@ type DescriptorValidationResult = {
|
|
|
76
76
|
};
|
|
77
77
|
declare function validateDescriptor(input: LessonkitCourseDescriptor): DescriptorValidationResult;
|
|
78
78
|
|
|
79
|
+
type ProjectPathsInput = {
|
|
80
|
+
spaDistDir?: string;
|
|
81
|
+
lxpackOutDir?: string;
|
|
82
|
+
outputBaseDir?: string;
|
|
83
|
+
};
|
|
84
|
+
/** Validate lessonkit.json paths.* entries stay under projectRoot. */
|
|
85
|
+
declare function validateProjectPaths(projectRoot: string, paths: ProjectPathsInput): DescriptorValidationIssue[];
|
|
86
|
+
/**
|
|
87
|
+
* Resolve a package --out override under projectRoot and ensure it stays inside the project.
|
|
88
|
+
*/
|
|
89
|
+
declare function resolveSafePackageOutputOverride(projectRoot: string, override: string): string;
|
|
90
|
+
|
|
79
91
|
/**
|
|
80
92
|
* Stable 1:1 mapping from LessonKit ids to LXPack manifest ids.
|
|
81
93
|
* LessonKit slug rules already match LXPack `activityIdSchema` (letter-first, alphanumeric + _ -).
|
|
@@ -129,6 +141,8 @@ type WriteLxpackProjectOptions = {
|
|
|
129
141
|
* For `per-lesson-spa`: map lesson id → absolute path to that lesson's built SPA folder.
|
|
130
142
|
*/
|
|
131
143
|
lessonSpaDirs?: Record<string, string>;
|
|
144
|
+
/** When set, relative `spaDistDir` is resolved under this directory instead of `process.cwd()`. */
|
|
145
|
+
projectRoot?: string;
|
|
132
146
|
};
|
|
133
147
|
type WriteLxpackProjectResult = {
|
|
134
148
|
outDir: string;
|
|
@@ -181,4 +195,4 @@ declare function validateLessonkitProject(options: ValidateLessonkitProjectOptio
|
|
|
181
195
|
declare function buildLessonkitProject(options: BuildLessonkitProjectOptions): Promise<BuildCourseResult>;
|
|
182
196
|
declare function packageLessonkitCourse(options: PackageLessonkitCourseOptions): Promise<PackageLessonkitCourseResult>;
|
|
183
197
|
|
|
184
|
-
export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitInterchangeV1, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, descriptorToInterchange, extractAssessments, mapLessonkitIds, packageLessonkitCourse, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateLessonkitProject, writeLxpackProject };
|
|
198
|
+
export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitInterchangeV1, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, descriptorToInterchange, extractAssessments, mapLessonkitIds, packageLessonkitCourse, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateLessonkitProject, validateProjectPaths, writeLxpackProject };
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ function isSafeRelativeSpaPath(spaPath) {
|
|
|
7
7
|
if (!spaPath.length || spaPath.includes("\0")) return false;
|
|
8
8
|
if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
|
|
9
9
|
if (/^[a-zA-Z]:[/\\]/.test(spaPath)) return false;
|
|
10
|
-
const segments = spaPath.split(/[/\\]/);
|
|
11
|
-
if (segments.some((s) => s === "..")) return false;
|
|
10
|
+
const segments = spaPath.split(/[/\\]/).filter((s) => s.length > 0);
|
|
11
|
+
if (segments.some((s) => s === ".." || s === ".")) return false;
|
|
12
12
|
return true;
|
|
13
13
|
}
|
|
14
14
|
function assertResolvedPathUnderRoot(root, target) {
|
|
@@ -20,6 +20,21 @@ function assertResolvedPathUnderRoot(root, target) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// src/theme.ts
|
|
24
|
+
import { getPresetTheme, themeToCssVariables } from "@lessonkit/themes";
|
|
25
|
+
function themeToLxpackRuntime(input) {
|
|
26
|
+
const theme = input.theme ?? getPresetTheme(input.preset ?? "default");
|
|
27
|
+
const raw = themeToCssVariables(theme);
|
|
28
|
+
const cssVariables = {};
|
|
29
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
30
|
+
cssVariables[key] = String(value);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
theme: theme.name,
|
|
34
|
+
cssVariables
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
// src/validateDescriptor.ts
|
|
24
39
|
var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
|
|
25
40
|
var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
|
|
@@ -81,6 +96,25 @@ function validateDescriptor(input) {
|
|
|
81
96
|
message: `unknown preset; use one of: ${VALID_THEME_PRESETS.join(", ")}`
|
|
82
97
|
});
|
|
83
98
|
}
|
|
99
|
+
if (input.theme?.theme) {
|
|
100
|
+
try {
|
|
101
|
+
themeToLxpackRuntime({ preset: themePreset, theme: input.theme.theme });
|
|
102
|
+
} catch (err) {
|
|
103
|
+
issues.push({
|
|
104
|
+
path: "theme.theme",
|
|
105
|
+
message: err instanceof Error ? err.message : "invalid custom theme"
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const completionThreshold = input.tracking?.completion?.threshold;
|
|
110
|
+
if (completionThreshold !== void 0) {
|
|
111
|
+
if (!Number.isFinite(completionThreshold) || completionThreshold < 0 || completionThreshold > 1) {
|
|
112
|
+
issues.push({
|
|
113
|
+
path: "tracking.completion.threshold",
|
|
114
|
+
message: "threshold must be a finite number between 0 and 1"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
84
118
|
if (layout === "single-spa" && (input.lessons?.length ?? 0) > 1) {
|
|
85
119
|
issues.push({
|
|
86
120
|
path: "lessons",
|
|
@@ -160,24 +194,69 @@ function validateDescriptor(input) {
|
|
|
160
194
|
issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
|
|
161
195
|
}
|
|
162
196
|
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
|
-
}
|
|
197
|
+
if (passingScore !== void 0 && !(passingScore > 0)) {
|
|
198
|
+
issues.push({
|
|
199
|
+
path: `${path}.passingScore`,
|
|
200
|
+
message: "passingScore must be greater than 0 (absolute point threshold)"
|
|
201
|
+
});
|
|
175
202
|
}
|
|
176
203
|
}
|
|
177
204
|
if (issues.length) return { ok: false, issues };
|
|
178
205
|
return { ok: true, descriptor: normalizeDescriptor(input) };
|
|
179
206
|
}
|
|
180
207
|
|
|
208
|
+
// src/validateProjectPaths.ts
|
|
209
|
+
import { isAbsolute, resolve as resolve2 } from "path";
|
|
210
|
+
function validatePathField(value, fieldPath, projectRoot, issues) {
|
|
211
|
+
if (!isSafeRelativeSpaPath(value)) {
|
|
212
|
+
issues.push({
|
|
213
|
+
path: fieldPath,
|
|
214
|
+
message: "path must be relative without '..' segments or absolute prefixes"
|
|
215
|
+
});
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
assertResolvedPathUnderRoot(projectRoot, resolve2(projectRoot, value));
|
|
220
|
+
} catch {
|
|
221
|
+
issues.push({
|
|
222
|
+
path: fieldPath,
|
|
223
|
+
message: "path must resolve inside the project root"
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function validateProjectPaths(projectRoot, paths) {
|
|
228
|
+
const issues = [];
|
|
229
|
+
const root = resolve2(projectRoot);
|
|
230
|
+
if (paths.spaDistDir?.trim()) {
|
|
231
|
+
validatePathField(paths.spaDistDir.trim(), "paths.spaDistDir", root, issues);
|
|
232
|
+
}
|
|
233
|
+
if (paths.lxpackOutDir?.trim()) {
|
|
234
|
+
validatePathField(paths.lxpackOutDir.trim(), "paths.lxpackOutDir", root, issues);
|
|
235
|
+
}
|
|
236
|
+
if (paths.outputBaseDir?.trim()) {
|
|
237
|
+
validatePathField(paths.outputBaseDir.trim(), "paths.outputBaseDir", root, issues);
|
|
238
|
+
}
|
|
239
|
+
return issues;
|
|
240
|
+
}
|
|
241
|
+
function resolveSafePackageOutputOverride(projectRoot, override) {
|
|
242
|
+
const root = resolve2(projectRoot);
|
|
243
|
+
const trimmed = override.trim();
|
|
244
|
+
if (!trimmed) {
|
|
245
|
+
throw new Error("output override must be a non-empty path");
|
|
246
|
+
}
|
|
247
|
+
if (isAbsolute(trimmed)) {
|
|
248
|
+
const resolved2 = resolve2(trimmed);
|
|
249
|
+
assertResolvedPathUnderRoot(root, resolved2);
|
|
250
|
+
return resolved2;
|
|
251
|
+
}
|
|
252
|
+
if (!isSafeRelativeSpaPath(trimmed)) {
|
|
253
|
+
throw new Error(`unsafe output path: ${override}`);
|
|
254
|
+
}
|
|
255
|
+
const resolved = resolve2(root, trimmed);
|
|
256
|
+
assertResolvedPathUnderRoot(root, resolved);
|
|
257
|
+
return resolved;
|
|
258
|
+
}
|
|
259
|
+
|
|
181
260
|
// src/mapIds.ts
|
|
182
261
|
import { assertValidId } from "@lessonkit/core";
|
|
183
262
|
function mapLessonkitIds(descriptor) {
|
|
@@ -189,21 +268,6 @@ function mapLessonkitIds(descriptor) {
|
|
|
189
268
|
return { courseId, lessonIds, checkIds };
|
|
190
269
|
}
|
|
191
270
|
|
|
192
|
-
// src/theme.ts
|
|
193
|
-
import { getPresetTheme, themeToCssVariables } from "@lessonkit/themes";
|
|
194
|
-
function themeToLxpackRuntime(input) {
|
|
195
|
-
const theme = input.theme ?? getPresetTheme(input.preset ?? "default");
|
|
196
|
-
const raw = themeToCssVariables(theme);
|
|
197
|
-
const cssVariables = {};
|
|
198
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
199
|
-
cssVariables[key] = String(value);
|
|
200
|
-
}
|
|
201
|
-
return {
|
|
202
|
-
theme: theme.name,
|
|
203
|
-
cssVariables
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
271
|
// src/interchange.ts
|
|
208
272
|
function resolveSpaLessons(descriptor) {
|
|
209
273
|
const mapped = mapLessonkitIds(descriptor);
|
|
@@ -277,7 +341,7 @@ function extractAssessments(descriptor) {
|
|
|
277
341
|
|
|
278
342
|
// src/writeProject.ts
|
|
279
343
|
import { access, cp, mkdir, rm, writeFile } from "fs/promises";
|
|
280
|
-
import { dirname, join, resolve as
|
|
344
|
+
import { dirname, join, resolve as resolve3 } from "path";
|
|
281
345
|
|
|
282
346
|
// src/assessmentYaml.ts
|
|
283
347
|
function yamlQuote(value) {
|
|
@@ -363,7 +427,7 @@ async function writeLxpackProject(options) {
|
|
|
363
427
|
);
|
|
364
428
|
}
|
|
365
429
|
const descriptor = validation.descriptor;
|
|
366
|
-
const outDir =
|
|
430
|
+
const outDir = resolve3(options.outDir);
|
|
367
431
|
await mkdir(outDir, { recursive: true });
|
|
368
432
|
const spaLessons = resolveSpaLessons(descriptor);
|
|
369
433
|
const runtime = descriptor.theme ? themeToLxpackRuntime(descriptor.theme) : void 0;
|
|
@@ -372,7 +436,11 @@ async function writeLxpackProject(options) {
|
|
|
372
436
|
file: `assessments/${a.checkId}.yaml`
|
|
373
437
|
}));
|
|
374
438
|
if (descriptor.layout === "single-spa") {
|
|
375
|
-
const
|
|
439
|
+
const spaDistRelative = options.spaDistDir ?? descriptor.spaDistDir ?? "dist";
|
|
440
|
+
const srcDist = options.projectRoot ? resolve3(options.projectRoot, spaDistRelative) : resolve3(spaDistRelative);
|
|
441
|
+
if (options.projectRoot) {
|
|
442
|
+
assertResolvedPathUnderRoot(resolve3(options.projectRoot), srcDist);
|
|
443
|
+
}
|
|
376
444
|
try {
|
|
377
445
|
await access(srcDist);
|
|
378
446
|
} catch {
|
|
@@ -391,7 +459,7 @@ async function writeLxpackProject(options) {
|
|
|
391
459
|
const dest = join(outDir, lesson.spaPath);
|
|
392
460
|
assertResolvedPathUnderRoot(outDir, dest);
|
|
393
461
|
await rm(dest, { recursive: true, force: true });
|
|
394
|
-
await copyDir(
|
|
462
|
+
await copyDir(resolve3(src), dest);
|
|
395
463
|
}
|
|
396
464
|
}
|
|
397
465
|
if (assessments.length) {
|
|
@@ -434,8 +502,8 @@ async function writeLxpackProject(options) {
|
|
|
434
502
|
}
|
|
435
503
|
|
|
436
504
|
// src/packageCourse.ts
|
|
437
|
-
import
|
|
438
|
-
import { dirname as dirname2, join as join2, resolve as
|
|
505
|
+
import * as fsp from "fs/promises";
|
|
506
|
+
import { dirname as dirname2, join as join2, resolve as resolve4 } from "path";
|
|
439
507
|
import { tmpdir } from "os";
|
|
440
508
|
import {
|
|
441
509
|
buildCourse,
|
|
@@ -443,13 +511,13 @@ import {
|
|
|
443
511
|
} from "@lxpack/api";
|
|
444
512
|
async function validateLessonkitProject(options) {
|
|
445
513
|
return validateCourse({
|
|
446
|
-
courseDir:
|
|
514
|
+
courseDir: resolve4(options.courseDir),
|
|
447
515
|
target: options.target
|
|
448
516
|
});
|
|
449
517
|
}
|
|
450
518
|
async function buildLessonkitProject(options) {
|
|
451
519
|
return buildCourse({
|
|
452
|
-
courseDir:
|
|
520
|
+
courseDir: resolve4(options.courseDir),
|
|
453
521
|
target: options.target,
|
|
454
522
|
output: options.output,
|
|
455
523
|
dir: options.dir,
|
|
@@ -457,9 +525,38 @@ async function buildLessonkitProject(options) {
|
|
|
457
525
|
assessments: options.assessments
|
|
458
526
|
});
|
|
459
527
|
}
|
|
528
|
+
async function pathExists(path) {
|
|
529
|
+
try {
|
|
530
|
+
await fsp.access(path);
|
|
531
|
+
return true;
|
|
532
|
+
} catch {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async function promoteStagingToOutDir(stagingDir, outDir) {
|
|
537
|
+
const tmpPromote = `${outDir}.tmp-promote`;
|
|
538
|
+
const backup = `${outDir}.bak`;
|
|
539
|
+
await fsp.rename(stagingDir, tmpPromote);
|
|
540
|
+
const hadOutDir = await pathExists(outDir);
|
|
541
|
+
if (hadOutDir) {
|
|
542
|
+
await fsp.rename(outDir, backup);
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
await fsp.rename(tmpPromote, outDir);
|
|
546
|
+
} catch (promoteError) {
|
|
547
|
+
if (hadOutDir) {
|
|
548
|
+
await fsp.rename(backup, outDir).catch(() => void 0);
|
|
549
|
+
}
|
|
550
|
+
await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
|
|
551
|
+
throw promoteError;
|
|
552
|
+
}
|
|
553
|
+
if (hadOutDir) {
|
|
554
|
+
await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
460
557
|
async function packageLessonkitCourse(options) {
|
|
461
558
|
const { target, output, dir, outputBaseDir, ...writeOpts } = options;
|
|
462
|
-
const outDir =
|
|
559
|
+
const outDir = resolve4(writeOpts.outDir);
|
|
463
560
|
const descriptorValidation = validateDescriptor(writeOpts.descriptor);
|
|
464
561
|
if (!descriptorValidation.ok) {
|
|
465
562
|
return {
|
|
@@ -473,7 +570,7 @@ async function packageLessonkitCourse(options) {
|
|
|
473
570
|
};
|
|
474
571
|
}
|
|
475
572
|
const descriptor = descriptorValidation.descriptor;
|
|
476
|
-
const stagingDir = await mkdtemp(join2(tmpdir(), "lessonkit-lxpack-"));
|
|
573
|
+
const stagingDir = await fsp.mkdtemp(join2(tmpdir(), "lessonkit-lxpack-"));
|
|
477
574
|
let promoted = false;
|
|
478
575
|
try {
|
|
479
576
|
const written = await writeLxpackProject({ ...writeOpts, descriptor, outDir: stagingDir });
|
|
@@ -494,7 +591,7 @@ async function packageLessonkitCourse(options) {
|
|
|
494
591
|
};
|
|
495
592
|
}
|
|
496
593
|
const outputBase = outputBaseDir ?? ".lxpack/out";
|
|
497
|
-
await
|
|
594
|
+
await fsp.mkdir(join2(courseDir, outputBase), { recursive: true });
|
|
498
595
|
const defaultOutput = output ?? (dir ? join2(outputBase, target) : join2(outputBase, `course-${target}.zip`));
|
|
499
596
|
const build = await buildLessonkitProject({
|
|
500
597
|
courseDir,
|
|
@@ -517,32 +614,42 @@ async function packageLessonkitCourse(options) {
|
|
|
517
614
|
}))
|
|
518
615
|
};
|
|
519
616
|
}
|
|
520
|
-
await
|
|
521
|
-
await
|
|
522
|
-
await rename(stagingDir, outDir);
|
|
617
|
+
await fsp.mkdir(dirname2(outDir), { recursive: true });
|
|
618
|
+
await promoteStagingToOutDir(stagingDir, outDir);
|
|
523
619
|
promoted = true;
|
|
524
620
|
const remapArtifactPath = (artifactPath) => {
|
|
525
621
|
if (!artifactPath) return void 0;
|
|
526
|
-
const resolved =
|
|
527
|
-
const stagingResolved =
|
|
622
|
+
const resolved = resolve4(artifactPath);
|
|
623
|
+
const stagingResolved = resolve4(stagingDir);
|
|
528
624
|
if (resolved === stagingResolved || resolved.startsWith(stagingResolved + "/")) {
|
|
529
625
|
return join2(outDir, resolved.slice(stagingResolved.length + 1));
|
|
530
626
|
}
|
|
531
627
|
return artifactPath;
|
|
532
628
|
};
|
|
629
|
+
const remappedOutputPath = remapArtifactPath(
|
|
630
|
+
"outputPath" in build ? build.outputPath : void 0
|
|
631
|
+
);
|
|
632
|
+
const remappedOutputDir = remapArtifactPath("outputDir" in build ? build.outputDir : void 0);
|
|
633
|
+
const remappedBuild = { ...build };
|
|
634
|
+
if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
|
|
635
|
+
remappedBuild.outputPath = remappedOutputPath;
|
|
636
|
+
}
|
|
637
|
+
if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
|
|
638
|
+
remappedBuild.outputDir = remappedOutputDir;
|
|
639
|
+
}
|
|
533
640
|
return {
|
|
534
641
|
ok: true,
|
|
535
642
|
courseDir: outDir,
|
|
536
643
|
target,
|
|
537
|
-
outputPath:
|
|
538
|
-
outputDir:
|
|
644
|
+
outputPath: remappedOutputPath,
|
|
645
|
+
outputDir: remappedOutputDir,
|
|
539
646
|
fileCount: build.fileCount,
|
|
540
647
|
validation,
|
|
541
|
-
build
|
|
648
|
+
build: remappedBuild
|
|
542
649
|
};
|
|
543
650
|
} finally {
|
|
544
651
|
if (!promoted) {
|
|
545
|
-
await
|
|
652
|
+
await fsp.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
546
653
|
}
|
|
547
654
|
}
|
|
548
655
|
}
|
|
@@ -553,9 +660,11 @@ export {
|
|
|
553
660
|
extractAssessments,
|
|
554
661
|
mapLessonkitIds,
|
|
555
662
|
packageLessonkitCourse,
|
|
663
|
+
resolveSafePackageOutputOverride,
|
|
556
664
|
resolveSpaLessons,
|
|
557
665
|
themeToLxpackRuntime,
|
|
558
666
|
validateDescriptor,
|
|
559
667
|
validateLessonkitProject,
|
|
668
|
+
validateProjectPaths,
|
|
560
669
|
writeLxpackProject
|
|
561
670
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/lxpack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "LXPack export adapter for LessonKit courses (SCORM, standalone, xAPI, cmi5).",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"lint": "echo \"(no lint configured yet)\""
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@lessonkit/core": "0.
|
|
57
|
-
"@lessonkit/themes": "0.
|
|
56
|
+
"@lessonkit/core": "0.8.1",
|
|
57
|
+
"@lessonkit/themes": "0.8.1",
|
|
58
58
|
"@lxpack/api": "^0.4.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|