@lessonkit/cli 1.4.0 → 1.5.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 +26 -5
- package/dist/bin.js +97 -38
- package/dist/index.d.ts +7 -0
- package/dist/index.js +97 -38
- package/package.json +5 -5
- package/template/vite-react/.env.example +6 -3
- package/template/vite-react/README.md +27 -2
- package/template/vite-react/package.json +7 -7
- package/template/vite-react/src/App.tsx +2 -2
- package/template/vite-react/src/courseConfig.test.ts +2 -0
- package/template/vite-react/src/courseConfig.ts +11 -1
- package/template/vite-react/src/vite-env.d.ts +3 -0
package/README.md
CHANGED
|
@@ -4,25 +4,37 @@
|
|
|
4
4
|
[](https://lessonkit.readthedocs.io/en/latest/reference/cli.html)
|
|
5
5
|
[](https://github.com/eddiethedean/lessonkit/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
Scaffold, develop, build, and package LessonKit courses.
|
|
7
|
+
Scaffold, develop, build, and package LessonKit courses.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npm install -g @lessonkit/cli
|
|
13
|
-
# or one-shot:
|
|
13
|
+
# or one-shot (recommended for new courses):
|
|
14
14
|
npx @lessonkit/cli init my-course
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
**Node.js:** **20.19+** recommended for `init` (Vite 8 scaffold). **18+** minimum for `dev`, `build`, and `package`.
|
|
18
|
+
|
|
17
19
|
## Commands
|
|
18
20
|
|
|
19
21
|
```bash
|
|
20
|
-
lessonkit init my-course # scaffold Vite + React project
|
|
22
|
+
lessonkit init my-course # scaffold Vite + React project (runs npm install)
|
|
21
23
|
lessonkit dev # Vite dev server
|
|
22
24
|
lessonkit build # production build → dist/
|
|
23
25
|
lessonkit package --target scorm12 # LMS artifact
|
|
24
26
|
```
|
|
25
27
|
|
|
28
|
+
### Init flags
|
|
29
|
+
|
|
30
|
+
| Flag | Purpose |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `--here` | Scaffold in the current directory |
|
|
33
|
+
| `--force` | Overwrite existing files in the target directory |
|
|
34
|
+
| `--skip-install` | Skip `npm install` after copying the template |
|
|
35
|
+
|
|
36
|
+
### Package targets
|
|
37
|
+
|
|
26
38
|
| Target | Output |
|
|
27
39
|
| --- | --- |
|
|
28
40
|
| `react-vite` | Vite build only |
|
|
@@ -32,11 +44,20 @@ lessonkit package --target scorm12 # LMS artifact
|
|
|
32
44
|
|
|
33
45
|
Every project includes a root `lessonkit.json` manifest (`schemaVersion: 1`).
|
|
34
46
|
|
|
35
|
-
Subprocess timeout defaults to **30 minutes** (`LESSONKIT_CMD_TIMEOUT_MS`).
|
|
47
|
+
Subprocess timeout defaults to **30 minutes** (`LESSONKIT_CMD_TIMEOUT_MS`).
|
|
48
|
+
|
|
49
|
+
## Common issues
|
|
50
|
+
|
|
51
|
+
| Symptom | Fix |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `lessonkit: command not found` | Use `npx @lessonkit/cli` or `npm run dev` in a scaffolded project |
|
|
54
|
+
| `init` fails on Node version | Use Node **20.19+** for Vite 8 scaffold |
|
|
55
|
+
| SCORM zip not at project root | Default path: `.lxpack/course/.lxpack/out/course-scorm12.zip` — trust CLI stdout |
|
|
56
|
+
| ID parity errors on `package` | Align `courseId`, `lessonId`, `checkId` between React and `lessonkit.json` |
|
|
36
57
|
|
|
37
58
|
## Docs
|
|
38
59
|
|
|
39
|
-
[CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) · [Packaging guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html) · [
|
|
60
|
+
[5-minute guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/getting-started-in-5-minutes.html) · [CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) · [Ship to LMS checklist](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/ship-to-lms.html) · [Packaging guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html) · [Published template](https://github.com/eddiethedean/lessonkit/tree/main/packages/cli/template/vite-react) (monorepo source: [`templates/vite-react`](https://github.com/eddiethedean/lessonkit/tree/main/templates/vite-react))
|
|
40
61
|
|
|
41
62
|
## License
|
|
42
63
|
|
package/dist/bin.js
CHANGED
|
@@ -6,8 +6,9 @@ import { Command } from "commander";
|
|
|
6
6
|
|
|
7
7
|
// src/commands/init.ts
|
|
8
8
|
import { slugifyId } from "@lessonkit/core";
|
|
9
|
-
import { cp, mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
9
|
+
import { cp, mkdir, readdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
10
10
|
import { existsSync } from "fs";
|
|
11
|
+
import { randomUUID } from "crypto";
|
|
11
12
|
import { basename, dirname, join, resolve } from "path";
|
|
12
13
|
import { fileURLToPath } from "url";
|
|
13
14
|
|
|
@@ -124,7 +125,7 @@ async function runNpmInstall(cwd) {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
// src/commands/init.ts
|
|
127
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".lxpack", ".git"]);
|
|
128
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".lxpack", ".git", "coverage", ".nyc_output"]);
|
|
128
129
|
var SKIP_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
|
|
129
130
|
function getTemplateDir() {
|
|
130
131
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -137,18 +138,13 @@ function getTemplateDir() {
|
|
|
137
138
|
}
|
|
138
139
|
return candidates[0];
|
|
139
140
|
}
|
|
140
|
-
async function isDirEmpty(dir) {
|
|
141
|
-
if (!existsSync(dir)) return true;
|
|
142
|
-
const entries = await readdir(dir);
|
|
143
|
-
return entries.length === 0;
|
|
144
|
-
}
|
|
145
141
|
async function isDirEmptyOrDotfilesOnly(dir) {
|
|
146
142
|
if (!existsSync(dir)) return true;
|
|
147
143
|
const entries = await readdir(dir);
|
|
148
144
|
return entries.every((name) => name.startsWith("."));
|
|
149
145
|
}
|
|
150
146
|
function escapeJsxString(value) {
|
|
151
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/</g, "\\u003c").replace(/\r\n|\n|\r/g, "\\n");
|
|
147
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029").replace(/\r\n|\n|\r/g, "\\n");
|
|
152
148
|
}
|
|
153
149
|
async function copyTemplate(src, dest) {
|
|
154
150
|
await mkdir(dest, { recursive: true });
|
|
@@ -169,7 +165,7 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
|
|
|
169
165
|
const pkgPath = join(projectDir, "package.json");
|
|
170
166
|
const lessonkitPath = join(projectDir, "lessonkit.json");
|
|
171
167
|
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
172
|
-
pkg.name =
|
|
168
|
+
pkg.name = slug;
|
|
173
169
|
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
174
170
|
`, "utf8");
|
|
175
171
|
const lessonkit = JSON.parse(await readFile(lessonkitPath, "utf8"));
|
|
@@ -177,11 +173,11 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
|
|
|
177
173
|
const course = lessonkit.course;
|
|
178
174
|
course.courseId = slug;
|
|
179
175
|
course.title = projectName;
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
176
|
+
const courseTracking = course.tracking ?? {};
|
|
177
|
+
const courseXapi = courseTracking.xapi ?? {};
|
|
178
|
+
courseXapi.activityIri = `https://example.com/courses/${slug}`;
|
|
179
|
+
courseTracking.xapi = courseXapi;
|
|
180
|
+
course.tracking = courseTracking;
|
|
185
181
|
await writeFile(lessonkitPath, `${JSON.stringify(lessonkit, null, 2)}
|
|
186
182
|
`, "utf8");
|
|
187
183
|
const courseConfigPath = join(projectDir, "src", "courseConfig.ts");
|
|
@@ -194,6 +190,20 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
|
|
|
194
190
|
appSource = appSource.replace(/\{\{courseTitle\}\}/g, escapeJsxString(projectName));
|
|
195
191
|
await writeFile(appPath, appSource, "utf8");
|
|
196
192
|
}
|
|
193
|
+
async function promoteStagingToProjectDir(stagingDir, projectDir) {
|
|
194
|
+
await mkdir(projectDir, { recursive: true });
|
|
195
|
+
const entries = await readdir(stagingDir, { withFileTypes: true });
|
|
196
|
+
for (const entry of entries) {
|
|
197
|
+
const srcPath = join(stagingDir, entry.name);
|
|
198
|
+
const destPath = join(projectDir, entry.name);
|
|
199
|
+
if (entry.isDirectory()) {
|
|
200
|
+
await cp(srcPath, destPath, { recursive: true });
|
|
201
|
+
} else if (entry.isFile()) {
|
|
202
|
+
await cp(srcPath, destPath);
|
|
203
|
+
} else {
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
197
207
|
async function runInit(opts, logger) {
|
|
198
208
|
const cwd = process.cwd();
|
|
199
209
|
const rawName = opts.name ?? (opts.here ? slugifyId(basename(process.cwd()) || "my-course") : void 0);
|
|
@@ -221,7 +231,7 @@ async function runInit(opts, logger) {
|
|
|
221
231
|
}
|
|
222
232
|
);
|
|
223
233
|
}
|
|
224
|
-
if (opts.here && !await
|
|
234
|
+
if (opts.here && !await isDirEmptyOrDotfilesOnly(projectDir) && !opts.force) {
|
|
225
235
|
throw new CliError(`Directory is not empty: ${projectDir}. Use --force to initialize anyway.`, {
|
|
226
236
|
code: "INVALID_PROJECT",
|
|
227
237
|
exitCode: EXIT_INVALID_PROJECT
|
|
@@ -243,11 +253,26 @@ async function runInit(opts, logger) {
|
|
|
243
253
|
exitCode: EXIT_INVALID_PROJECT
|
|
244
254
|
});
|
|
245
255
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
256
|
+
const stagingDir = opts.here ? join(cwd, `.lessonkit-init-${randomUUID()}`) : join(cwd, `.${slug}-init-${randomUUID()}`);
|
|
257
|
+
try {
|
|
258
|
+
await copyTemplate(templateDir, stagingDir);
|
|
259
|
+
await applyTemplateSubstitutions(stagingDir, projectName, slug);
|
|
260
|
+
if (!opts.skipInstall) {
|
|
261
|
+
if (!opts.json) logger.log(`Installing dependencies in ${stagingDir}\u2026`);
|
|
262
|
+
await runNpmInstall(stagingDir);
|
|
263
|
+
}
|
|
264
|
+
if (opts.here) {
|
|
265
|
+
await promoteStagingToProjectDir(stagingDir, projectDir);
|
|
266
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
267
|
+
} else {
|
|
268
|
+
await rename(stagingDir, projectDir);
|
|
269
|
+
}
|
|
270
|
+
} catch (err) {
|
|
271
|
+
await rm(stagingDir, { recursive: true, force: true }).catch(
|
|
272
|
+
/* v8 ignore next */
|
|
273
|
+
() => void 0
|
|
274
|
+
);
|
|
275
|
+
throw err;
|
|
251
276
|
}
|
|
252
277
|
if (!opts.json) {
|
|
253
278
|
logger.log(`Created LessonKit project at ${projectDir}`);
|
|
@@ -256,6 +281,10 @@ async function runInit(opts, logger) {
|
|
|
256
281
|
return { ok: true, command: "init", projectRoot: projectDir };
|
|
257
282
|
}
|
|
258
283
|
|
|
284
|
+
// src/commands/dev.ts
|
|
285
|
+
import { existsSync as existsSync3 } from "fs";
|
|
286
|
+
import { join as join3 } from "path";
|
|
287
|
+
|
|
259
288
|
// src/lib/project.ts
|
|
260
289
|
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
261
290
|
import { readFile as readFile2 } from "fs/promises";
|
|
@@ -451,13 +480,24 @@ function resolvePackageOutput(project, target, override) {
|
|
|
451
480
|
}
|
|
452
481
|
return { output: `${outputBaseDir}/course-${target}.zip`, dir: false, outputBaseDir };
|
|
453
482
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
483
|
+
function stripOutDirFromViteArgs(viteArgs) {
|
|
484
|
+
const stripped = [];
|
|
485
|
+
for (let i = 0; i < viteArgs.length; i++) {
|
|
486
|
+
const arg = viteArgs[i];
|
|
487
|
+
if (arg === "--outDir" || arg === "-o") {
|
|
488
|
+
i++;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (arg.startsWith("--outDir=")) {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
stripped.push(arg);
|
|
459
495
|
}
|
|
460
|
-
return
|
|
496
|
+
return stripped;
|
|
497
|
+
}
|
|
498
|
+
function resolveViteBuildArgv(project, viteArgs = []) {
|
|
499
|
+
const passthrough = stripOutDirFromViteArgs(viteArgs);
|
|
500
|
+
return ["build", ...passthrough, "--outDir", project.paths.spaDistDir];
|
|
461
501
|
}
|
|
462
502
|
function parsePackageTarget(value) {
|
|
463
503
|
if (!value) {
|
|
@@ -486,15 +526,24 @@ async function runBuild(opts) {
|
|
|
486
526
|
const pkg = await readPackageJson(project.root);
|
|
487
527
|
assertViteProject(pkg, project.root);
|
|
488
528
|
const viteJs = resolveViteJs(project.root);
|
|
489
|
-
const buildArgs =
|
|
490
|
-
await runCommand(process.execPath, [viteJs, ...buildArgs
|
|
529
|
+
const buildArgs = resolveViteBuildArgv(project, opts.viteArgs);
|
|
530
|
+
await runCommand(process.execPath, [viteJs, ...buildArgs], {
|
|
491
531
|
cwd: project.root
|
|
492
532
|
});
|
|
533
|
+
const distDir = resolveDistDir(project);
|
|
534
|
+
const indexHtml = join3(distDir, "index.html");
|
|
535
|
+
if (!existsSync3(indexHtml)) {
|
|
536
|
+
throw new CliError(
|
|
537
|
+
`Build did not produce index.html at ${indexHtml}. Check paths.spaDistDir in lessonkit.json.`,
|
|
538
|
+
{ code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
|
|
539
|
+
);
|
|
540
|
+
}
|
|
493
541
|
return { ok: true, command: "build", projectRoot: project.root };
|
|
494
542
|
}
|
|
495
543
|
|
|
496
544
|
// src/commands/package.ts
|
|
497
|
-
import { existsSync as
|
|
545
|
+
import { existsSync as existsSync4 } from "fs";
|
|
546
|
+
import { isAbsolute } from "path";
|
|
498
547
|
import { packageLessonkitCourse } from "@lessonkit/lxpack";
|
|
499
548
|
async function runPackage(opts) {
|
|
500
549
|
let target;
|
|
@@ -512,7 +561,7 @@ async function runPackage(opts) {
|
|
|
512
561
|
}
|
|
513
562
|
const project = await loadProject(opts.cwd ?? process.cwd());
|
|
514
563
|
const distDir = resolveDistDir(project);
|
|
515
|
-
if (opts.noBuild && !
|
|
564
|
+
if (opts.noBuild && !existsSync4(distDir)) {
|
|
516
565
|
throw new CliError(
|
|
517
566
|
`dist directory not found at ${distDir}. Run lessonkit build before packaging with --no-build.`,
|
|
518
567
|
{
|
|
@@ -525,7 +574,7 @@ async function runPackage(opts) {
|
|
|
525
574
|
if (!opts.noBuild) {
|
|
526
575
|
await runBuild({ cwd: project.root, json: opts.json });
|
|
527
576
|
}
|
|
528
|
-
if (!
|
|
577
|
+
if (!existsSync4(distDir)) {
|
|
529
578
|
throw new CliError(`Build completed but dist directory not found at ${distDir}.`, {
|
|
530
579
|
code: "INVALID_PROJECT",
|
|
531
580
|
exitCode: EXIT_INVALID_PROJECT
|
|
@@ -537,14 +586,20 @@ async function runPackage(opts) {
|
|
|
537
586
|
if (!opts.noBuild) {
|
|
538
587
|
await runBuild({ cwd: project.root, json: opts.json });
|
|
539
588
|
}
|
|
540
|
-
if (!
|
|
589
|
+
if (!existsSync4(distDir)) {
|
|
541
590
|
throw new CliError(`dist directory not found at ${distDir}. Run lessonkit build first.`, {
|
|
542
591
|
code: "INVALID_PROJECT",
|
|
543
592
|
exitCode: EXIT_INVALID_PROJECT
|
|
544
593
|
});
|
|
545
594
|
}
|
|
546
595
|
const outDir = resolveLxpackOutDir(project);
|
|
547
|
-
const { output, dir, outputBaseDir } = resolvePackageOutput(
|
|
596
|
+
const { output: resolvedOutput, dir, outputBaseDir } = resolvePackageOutput(
|
|
597
|
+
project,
|
|
598
|
+
target,
|
|
599
|
+
opts.out
|
|
600
|
+
);
|
|
601
|
+
const trimmedOut = opts.out?.trim();
|
|
602
|
+
const output = trimmedOut && !isAbsolute(trimmedOut) ? trimmedOut : resolvedOutput;
|
|
548
603
|
const result = await packageLessonkitCourse({
|
|
549
604
|
descriptor: project.course,
|
|
550
605
|
outDir,
|
|
@@ -553,7 +608,8 @@ async function runPackage(opts) {
|
|
|
553
608
|
target,
|
|
554
609
|
output,
|
|
555
610
|
dir,
|
|
556
|
-
outputBaseDir
|
|
611
|
+
outputBaseDir,
|
|
612
|
+
strictParity: opts.strictParity
|
|
557
613
|
});
|
|
558
614
|
if (!result.ok) {
|
|
559
615
|
throw new CliError("Packaging failed.", {
|
|
@@ -656,7 +712,7 @@ function createProgram(baseLogger = console) {
|
|
|
656
712
|
);
|
|
657
713
|
}
|
|
658
714
|
);
|
|
659
|
-
program.command("package").description("Build or package for web / LMS delivery").requiredOption("--target <target>", `Export target (${PACKAGE_TARGETS.join(", ")})`).option("--cwd <dir>", "Project root directory").option("--no-build", "Skip implicit Vite build for LMS targets").option("--out <path>", "Override output artifact path").option("--json", "Emit structured JSON result").action(async (opts) => {
|
|
715
|
+
program.command("package").description("Build or package for web / LMS delivery").requiredOption("--target <target>", `Export target (${PACKAGE_TARGETS.join(", ")})`).option("--cwd <dir>", "Project root directory").option("--no-build", "Skip implicit Vite build for LMS targets").option("--out <path>", "Override output artifact path").option("--strict-parity", "Treat React ID parity warnings as packaging errors").option("--json", "Emit structured JSON result").action(async (opts) => {
|
|
660
716
|
const logger = createLogger({ json: opts.json });
|
|
661
717
|
await handleCommand(
|
|
662
718
|
async () => {
|
|
@@ -665,7 +721,8 @@ function createProgram(baseLogger = console) {
|
|
|
665
721
|
cwd: opts.cwd,
|
|
666
722
|
noBuild: opts.build === false,
|
|
667
723
|
out: opts.out,
|
|
668
|
-
json: opts.json
|
|
724
|
+
json: opts.json,
|
|
725
|
+
strictParity: opts.strictParity
|
|
669
726
|
});
|
|
670
727
|
if (!opts.json && result.ok && result.command === "package") {
|
|
671
728
|
if (result.target === "react-vite") {
|
|
@@ -683,8 +740,10 @@ function createProgram(baseLogger = console) {
|
|
|
683
740
|
Boolean(opts.json)
|
|
684
741
|
);
|
|
685
742
|
});
|
|
686
|
-
program.command("publish").description("
|
|
687
|
-
baseLogger.log(
|
|
743
|
+
program.command("publish").description("[maintainers] Not implemented \u2014 use Changesets (see RELEASING.md)").action(() => {
|
|
744
|
+
baseLogger.log(
|
|
745
|
+
"lessonkit publish is not implemented. Monorepo releases use Changesets: npm run changeset && npm run version-packages && npm run release. See RELEASING.md."
|
|
746
|
+
);
|
|
688
747
|
});
|
|
689
748
|
return program;
|
|
690
749
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,14 @@ type CliLogger = {
|
|
|
5
5
|
error: (...args: unknown[]) => void;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Build the Commander program used by the `lessonkit` CLI binary.
|
|
10
|
+
* Useful for embedding init/build/package in Node scripts and tests.
|
|
11
|
+
*/
|
|
8
12
|
declare function createProgram(baseLogger?: CliLogger): Command;
|
|
13
|
+
/**
|
|
14
|
+
* Parse argv and run the LessonKit CLI (same as the `lessonkit` binary entrypoint).
|
|
15
|
+
*/
|
|
9
16
|
declare function run(argv?: string[], logger?: CliLogger): Promise<void>;
|
|
10
17
|
|
|
11
18
|
export { type CliLogger, createProgram, run };
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,9 @@ import { Command } from "commander";
|
|
|
4
4
|
|
|
5
5
|
// src/commands/init.ts
|
|
6
6
|
import { slugifyId } from "@lessonkit/core";
|
|
7
|
-
import { cp, mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
7
|
+
import { cp, mkdir, readdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
8
8
|
import { existsSync } from "fs";
|
|
9
|
+
import { randomUUID } from "crypto";
|
|
9
10
|
import { basename, dirname, join, resolve } from "path";
|
|
10
11
|
import { fileURLToPath } from "url";
|
|
11
12
|
|
|
@@ -122,7 +123,7 @@ async function runNpmInstall(cwd) {
|
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
// src/commands/init.ts
|
|
125
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".lxpack", ".git"]);
|
|
126
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".lxpack", ".git", "coverage", ".nyc_output"]);
|
|
126
127
|
var SKIP_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
|
|
127
128
|
function getTemplateDir() {
|
|
128
129
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -135,18 +136,13 @@ function getTemplateDir() {
|
|
|
135
136
|
}
|
|
136
137
|
return candidates[0];
|
|
137
138
|
}
|
|
138
|
-
async function isDirEmpty(dir) {
|
|
139
|
-
if (!existsSync(dir)) return true;
|
|
140
|
-
const entries = await readdir(dir);
|
|
141
|
-
return entries.length === 0;
|
|
142
|
-
}
|
|
143
139
|
async function isDirEmptyOrDotfilesOnly(dir) {
|
|
144
140
|
if (!existsSync(dir)) return true;
|
|
145
141
|
const entries = await readdir(dir);
|
|
146
142
|
return entries.every((name) => name.startsWith("."));
|
|
147
143
|
}
|
|
148
144
|
function escapeJsxString(value) {
|
|
149
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/</g, "\\u003c").replace(/\r\n|\n|\r/g, "\\n");
|
|
145
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029").replace(/\r\n|\n|\r/g, "\\n");
|
|
150
146
|
}
|
|
151
147
|
async function copyTemplate(src, dest) {
|
|
152
148
|
await mkdir(dest, { recursive: true });
|
|
@@ -167,7 +163,7 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
|
|
|
167
163
|
const pkgPath = join(projectDir, "package.json");
|
|
168
164
|
const lessonkitPath = join(projectDir, "lessonkit.json");
|
|
169
165
|
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
170
|
-
pkg.name =
|
|
166
|
+
pkg.name = slug;
|
|
171
167
|
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
172
168
|
`, "utf8");
|
|
173
169
|
const lessonkit = JSON.parse(await readFile(lessonkitPath, "utf8"));
|
|
@@ -175,11 +171,11 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
|
|
|
175
171
|
const course = lessonkit.course;
|
|
176
172
|
course.courseId = slug;
|
|
177
173
|
course.title = projectName;
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
174
|
+
const courseTracking = course.tracking ?? {};
|
|
175
|
+
const courseXapi = courseTracking.xapi ?? {};
|
|
176
|
+
courseXapi.activityIri = `https://example.com/courses/${slug}`;
|
|
177
|
+
courseTracking.xapi = courseXapi;
|
|
178
|
+
course.tracking = courseTracking;
|
|
183
179
|
await writeFile(lessonkitPath, `${JSON.stringify(lessonkit, null, 2)}
|
|
184
180
|
`, "utf8");
|
|
185
181
|
const courseConfigPath = join(projectDir, "src", "courseConfig.ts");
|
|
@@ -192,6 +188,20 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
|
|
|
192
188
|
appSource = appSource.replace(/\{\{courseTitle\}\}/g, escapeJsxString(projectName));
|
|
193
189
|
await writeFile(appPath, appSource, "utf8");
|
|
194
190
|
}
|
|
191
|
+
async function promoteStagingToProjectDir(stagingDir, projectDir) {
|
|
192
|
+
await mkdir(projectDir, { recursive: true });
|
|
193
|
+
const entries = await readdir(stagingDir, { withFileTypes: true });
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
const srcPath = join(stagingDir, entry.name);
|
|
196
|
+
const destPath = join(projectDir, entry.name);
|
|
197
|
+
if (entry.isDirectory()) {
|
|
198
|
+
await cp(srcPath, destPath, { recursive: true });
|
|
199
|
+
} else if (entry.isFile()) {
|
|
200
|
+
await cp(srcPath, destPath);
|
|
201
|
+
} else {
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
195
205
|
async function runInit(opts, logger) {
|
|
196
206
|
const cwd = process.cwd();
|
|
197
207
|
const rawName = opts.name ?? (opts.here ? slugifyId(basename(process.cwd()) || "my-course") : void 0);
|
|
@@ -219,7 +229,7 @@ async function runInit(opts, logger) {
|
|
|
219
229
|
}
|
|
220
230
|
);
|
|
221
231
|
}
|
|
222
|
-
if (opts.here && !await
|
|
232
|
+
if (opts.here && !await isDirEmptyOrDotfilesOnly(projectDir) && !opts.force) {
|
|
223
233
|
throw new CliError(`Directory is not empty: ${projectDir}. Use --force to initialize anyway.`, {
|
|
224
234
|
code: "INVALID_PROJECT",
|
|
225
235
|
exitCode: EXIT_INVALID_PROJECT
|
|
@@ -241,11 +251,26 @@ async function runInit(opts, logger) {
|
|
|
241
251
|
exitCode: EXIT_INVALID_PROJECT
|
|
242
252
|
});
|
|
243
253
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
254
|
+
const stagingDir = opts.here ? join(cwd, `.lessonkit-init-${randomUUID()}`) : join(cwd, `.${slug}-init-${randomUUID()}`);
|
|
255
|
+
try {
|
|
256
|
+
await copyTemplate(templateDir, stagingDir);
|
|
257
|
+
await applyTemplateSubstitutions(stagingDir, projectName, slug);
|
|
258
|
+
if (!opts.skipInstall) {
|
|
259
|
+
if (!opts.json) logger.log(`Installing dependencies in ${stagingDir}\u2026`);
|
|
260
|
+
await runNpmInstall(stagingDir);
|
|
261
|
+
}
|
|
262
|
+
if (opts.here) {
|
|
263
|
+
await promoteStagingToProjectDir(stagingDir, projectDir);
|
|
264
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
265
|
+
} else {
|
|
266
|
+
await rename(stagingDir, projectDir);
|
|
267
|
+
}
|
|
268
|
+
} catch (err) {
|
|
269
|
+
await rm(stagingDir, { recursive: true, force: true }).catch(
|
|
270
|
+
/* v8 ignore next */
|
|
271
|
+
() => void 0
|
|
272
|
+
);
|
|
273
|
+
throw err;
|
|
249
274
|
}
|
|
250
275
|
if (!opts.json) {
|
|
251
276
|
logger.log(`Created LessonKit project at ${projectDir}`);
|
|
@@ -254,6 +279,10 @@ async function runInit(opts, logger) {
|
|
|
254
279
|
return { ok: true, command: "init", projectRoot: projectDir };
|
|
255
280
|
}
|
|
256
281
|
|
|
282
|
+
// src/commands/dev.ts
|
|
283
|
+
import { existsSync as existsSync3 } from "fs";
|
|
284
|
+
import { join as join3 } from "path";
|
|
285
|
+
|
|
257
286
|
// src/lib/project.ts
|
|
258
287
|
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
259
288
|
import { readFile as readFile2 } from "fs/promises";
|
|
@@ -449,13 +478,24 @@ function resolvePackageOutput(project, target, override) {
|
|
|
449
478
|
}
|
|
450
479
|
return { output: `${outputBaseDir}/course-${target}.zip`, dir: false, outputBaseDir };
|
|
451
480
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
481
|
+
function stripOutDirFromViteArgs(viteArgs) {
|
|
482
|
+
const stripped = [];
|
|
483
|
+
for (let i = 0; i < viteArgs.length; i++) {
|
|
484
|
+
const arg = viteArgs[i];
|
|
485
|
+
if (arg === "--outDir" || arg === "-o") {
|
|
486
|
+
i++;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (arg.startsWith("--outDir=")) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
stripped.push(arg);
|
|
457
493
|
}
|
|
458
|
-
return
|
|
494
|
+
return stripped;
|
|
495
|
+
}
|
|
496
|
+
function resolveViteBuildArgv(project, viteArgs = []) {
|
|
497
|
+
const passthrough = stripOutDirFromViteArgs(viteArgs);
|
|
498
|
+
return ["build", ...passthrough, "--outDir", project.paths.spaDistDir];
|
|
459
499
|
}
|
|
460
500
|
function parsePackageTarget(value) {
|
|
461
501
|
if (!value) {
|
|
@@ -484,15 +524,24 @@ async function runBuild(opts) {
|
|
|
484
524
|
const pkg = await readPackageJson(project.root);
|
|
485
525
|
assertViteProject(pkg, project.root);
|
|
486
526
|
const viteJs = resolveViteJs(project.root);
|
|
487
|
-
const buildArgs =
|
|
488
|
-
await runCommand(process.execPath, [viteJs, ...buildArgs
|
|
527
|
+
const buildArgs = resolveViteBuildArgv(project, opts.viteArgs);
|
|
528
|
+
await runCommand(process.execPath, [viteJs, ...buildArgs], {
|
|
489
529
|
cwd: project.root
|
|
490
530
|
});
|
|
531
|
+
const distDir = resolveDistDir(project);
|
|
532
|
+
const indexHtml = join3(distDir, "index.html");
|
|
533
|
+
if (!existsSync3(indexHtml)) {
|
|
534
|
+
throw new CliError(
|
|
535
|
+
`Build did not produce index.html at ${indexHtml}. Check paths.spaDistDir in lessonkit.json.`,
|
|
536
|
+
{ code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
|
|
537
|
+
);
|
|
538
|
+
}
|
|
491
539
|
return { ok: true, command: "build", projectRoot: project.root };
|
|
492
540
|
}
|
|
493
541
|
|
|
494
542
|
// src/commands/package.ts
|
|
495
|
-
import { existsSync as
|
|
543
|
+
import { existsSync as existsSync4 } from "fs";
|
|
544
|
+
import { isAbsolute } from "path";
|
|
496
545
|
import { packageLessonkitCourse } from "@lessonkit/lxpack";
|
|
497
546
|
async function runPackage(opts) {
|
|
498
547
|
let target;
|
|
@@ -510,7 +559,7 @@ async function runPackage(opts) {
|
|
|
510
559
|
}
|
|
511
560
|
const project = await loadProject(opts.cwd ?? process.cwd());
|
|
512
561
|
const distDir = resolveDistDir(project);
|
|
513
|
-
if (opts.noBuild && !
|
|
562
|
+
if (opts.noBuild && !existsSync4(distDir)) {
|
|
514
563
|
throw new CliError(
|
|
515
564
|
`dist directory not found at ${distDir}. Run lessonkit build before packaging with --no-build.`,
|
|
516
565
|
{
|
|
@@ -523,7 +572,7 @@ async function runPackage(opts) {
|
|
|
523
572
|
if (!opts.noBuild) {
|
|
524
573
|
await runBuild({ cwd: project.root, json: opts.json });
|
|
525
574
|
}
|
|
526
|
-
if (!
|
|
575
|
+
if (!existsSync4(distDir)) {
|
|
527
576
|
throw new CliError(`Build completed but dist directory not found at ${distDir}.`, {
|
|
528
577
|
code: "INVALID_PROJECT",
|
|
529
578
|
exitCode: EXIT_INVALID_PROJECT
|
|
@@ -535,14 +584,20 @@ async function runPackage(opts) {
|
|
|
535
584
|
if (!opts.noBuild) {
|
|
536
585
|
await runBuild({ cwd: project.root, json: opts.json });
|
|
537
586
|
}
|
|
538
|
-
if (!
|
|
587
|
+
if (!existsSync4(distDir)) {
|
|
539
588
|
throw new CliError(`dist directory not found at ${distDir}. Run lessonkit build first.`, {
|
|
540
589
|
code: "INVALID_PROJECT",
|
|
541
590
|
exitCode: EXIT_INVALID_PROJECT
|
|
542
591
|
});
|
|
543
592
|
}
|
|
544
593
|
const outDir = resolveLxpackOutDir(project);
|
|
545
|
-
const { output, dir, outputBaseDir } = resolvePackageOutput(
|
|
594
|
+
const { output: resolvedOutput, dir, outputBaseDir } = resolvePackageOutput(
|
|
595
|
+
project,
|
|
596
|
+
target,
|
|
597
|
+
opts.out
|
|
598
|
+
);
|
|
599
|
+
const trimmedOut = opts.out?.trim();
|
|
600
|
+
const output = trimmedOut && !isAbsolute(trimmedOut) ? trimmedOut : resolvedOutput;
|
|
546
601
|
const result = await packageLessonkitCourse({
|
|
547
602
|
descriptor: project.course,
|
|
548
603
|
outDir,
|
|
@@ -551,7 +606,8 @@ async function runPackage(opts) {
|
|
|
551
606
|
target,
|
|
552
607
|
output,
|
|
553
608
|
dir,
|
|
554
|
-
outputBaseDir
|
|
609
|
+
outputBaseDir,
|
|
610
|
+
strictParity: opts.strictParity
|
|
555
611
|
});
|
|
556
612
|
if (!result.ok) {
|
|
557
613
|
throw new CliError("Packaging failed.", {
|
|
@@ -654,7 +710,7 @@ function createProgram(baseLogger = console) {
|
|
|
654
710
|
);
|
|
655
711
|
}
|
|
656
712
|
);
|
|
657
|
-
program.command("package").description("Build or package for web / LMS delivery").requiredOption("--target <target>", `Export target (${PACKAGE_TARGETS.join(", ")})`).option("--cwd <dir>", "Project root directory").option("--no-build", "Skip implicit Vite build for LMS targets").option("--out <path>", "Override output artifact path").option("--json", "Emit structured JSON result").action(async (opts) => {
|
|
713
|
+
program.command("package").description("Build or package for web / LMS delivery").requiredOption("--target <target>", `Export target (${PACKAGE_TARGETS.join(", ")})`).option("--cwd <dir>", "Project root directory").option("--no-build", "Skip implicit Vite build for LMS targets").option("--out <path>", "Override output artifact path").option("--strict-parity", "Treat React ID parity warnings as packaging errors").option("--json", "Emit structured JSON result").action(async (opts) => {
|
|
658
714
|
const logger = createLogger({ json: opts.json });
|
|
659
715
|
await handleCommand(
|
|
660
716
|
async () => {
|
|
@@ -663,7 +719,8 @@ function createProgram(baseLogger = console) {
|
|
|
663
719
|
cwd: opts.cwd,
|
|
664
720
|
noBuild: opts.build === false,
|
|
665
721
|
out: opts.out,
|
|
666
|
-
json: opts.json
|
|
722
|
+
json: opts.json,
|
|
723
|
+
strictParity: opts.strictParity
|
|
667
724
|
});
|
|
668
725
|
if (!opts.json && result.ok && result.command === "package") {
|
|
669
726
|
if (result.target === "react-vite") {
|
|
@@ -681,8 +738,10 @@ function createProgram(baseLogger = console) {
|
|
|
681
738
|
Boolean(opts.json)
|
|
682
739
|
);
|
|
683
740
|
});
|
|
684
|
-
program.command("publish").description("
|
|
685
|
-
baseLogger.log(
|
|
741
|
+
program.command("publish").description("[maintainers] Not implemented \u2014 use Changesets (see RELEASING.md)").action(() => {
|
|
742
|
+
baseLogger.log(
|
|
743
|
+
"lessonkit publish is not implemented. Monorepo releases use Changesets: npm run changeset && npm run version-packages && npm run release. See RELEASING.md."
|
|
744
|
+
);
|
|
686
745
|
});
|
|
687
746
|
return program;
|
|
688
747
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "LessonKit CLI — init, dev, build, and package learning experiences.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
"typecheck": "tsc -p tsconfig.json",
|
|
43
43
|
"test": "vitest run --passWithNoTests",
|
|
44
44
|
"test:coverage": "vitest run --coverage --passWithNoTests=false",
|
|
45
|
-
"lint": "
|
|
45
|
+
"lint": "eslint --max-warnings 0 \"src/**/*.{ts,tsx}\" \"test/**/*.{ts,tsx}\""
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@lessonkit/core": "1.
|
|
49
|
-
"@lessonkit/lxpack": "1.
|
|
48
|
+
"@lessonkit/core": "1.5.0",
|
|
49
|
+
"@lessonkit/lxpack": "1.5.0",
|
|
50
50
|
"commander": "^15.0.0"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/node": "^25.9.2",
|
|
57
57
|
"tsup": "^8.5.0",
|
|
58
|
-
"typescript": "^
|
|
58
|
+
"typescript": "^6.0.3",
|
|
59
59
|
"vitest": "^4.1.8"
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
# Copy to
|
|
2
|
-
#
|
|
1
|
+
# Copy to `.env` for production LMS export (see courseConfig.ts).
|
|
2
|
+
# Never commit real secrets — use short-lived tokens from your backend.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
# Analytics batch ingest proxy (createFetchBatchSink)
|
|
5
5
|
VITE_ANALYTICS_URL=
|
|
6
|
+
|
|
7
|
+
# xAPI statement proxy (createFetchTransport)
|
|
8
|
+
VITE_XAPI_PROXY_URL=
|
|
@@ -18,10 +18,35 @@ npm run package:scorm12
|
|
|
18
18
|
- `.env.example` — `VITE_XAPI_PROXY_URL` and `VITE_ANALYTICS_URL` for production builds
|
|
19
19
|
- `lessonkit.json` — manifest for CLI and LXPack packaging
|
|
20
20
|
|
|
21
|
+
## Before LMS packaging
|
|
22
|
+
|
|
23
|
+
1. **LMS bridge** — In `src/courseConfig.ts`, enable the bridge for SCORM/xAPI/cmi5 export (template defaults to `"off"` for local preview):
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
lxpack: {
|
|
27
|
+
bridge: "auto",
|
|
28
|
+
allowedParentOrigins: ["https://your-lms.example"], // required in production builds
|
|
29
|
+
},
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Development builds allow `bridge: "auto"` without an allowlist; **production builds do not**. Discover your LMS origin from the SCORM preview URL or browser devtools (`document.referrer`).
|
|
33
|
+
|
|
34
|
+
2. **Production runtime** — Copy `.env.example` to `.env`, set `VITE_ANALYTICS_URL` and `VITE_XAPI_PROXY_URL`, and wire the observability hooks in `courseConfig.ts` (see comments there). Rebuild with `npm run build` before packaging. Alternatively, disable `tracking` and `xapi` for a first test export only.
|
|
35
|
+
|
|
36
|
+
3. **Activity IRI** — Replace the `example.com` placeholder in `lessonkit.json` → `course.tracking.xapi.activityIri` before xAPI/cmi5 export (must be HTTPS).
|
|
37
|
+
|
|
38
|
+
## SCORM output path
|
|
39
|
+
|
|
40
|
+
After `npm run package:scorm12`, the CLI prints the resolved ZIP path. Default:
|
|
41
|
+
|
|
42
|
+
**`.lxpack/course/.lxpack/out/course-scorm12.zip`**
|
|
43
|
+
|
|
44
|
+
(`paths.outputBaseDir` is resolved inside `paths.lxpackOutDir`, not at the project root.)
|
|
45
|
+
|
|
21
46
|
## Production
|
|
22
47
|
|
|
23
|
-
Copy `.env.example` to `.env
|
|
48
|
+
Copy `.env.example` to `.env` and set your LRS/analytics proxy URLs before `npm run build`. See the [production checklist](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/production-checklist.html) and [Ship to LMS checklist](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/ship-to-lms.html).
|
|
24
49
|
|
|
25
50
|
## Docs
|
|
26
51
|
|
|
27
|
-
[
|
|
52
|
+
[5-minute guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/getting-started-in-5-minutes.html) · [First LMS export](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/first-lms-export.html) · [CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) · [Packaging guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html)
|
|
@@ -16,23 +16,23 @@
|
|
|
16
16
|
"test:coverage": "vitest run --coverage --passWithNoTests=false"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@lessonkit/core": "^1.
|
|
20
|
-
"@lessonkit/react": "^1.
|
|
21
|
-
"@lessonkit/themes": "^1.
|
|
22
|
-
"@lessonkit/xapi": "^1.
|
|
19
|
+
"@lessonkit/core": "^1.5.0",
|
|
20
|
+
"@lessonkit/react": "^1.5.0",
|
|
21
|
+
"@lessonkit/themes": "^1.5.0",
|
|
22
|
+
"@lessonkit/xapi": "^1.5.0",
|
|
23
23
|
"react": "^19.2.7",
|
|
24
24
|
"react-dom": "^19.2.7"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@lessonkit/cli": "^1.
|
|
28
|
-
"@lessonkit/lxpack": "^1.
|
|
27
|
+
"@lessonkit/cli": "^1.5.0",
|
|
28
|
+
"@lessonkit/lxpack": "^1.5.0",
|
|
29
29
|
"@testing-library/react": "^16.3.0",
|
|
30
30
|
"@testing-library/dom": "^10.4.1",
|
|
31
31
|
"@types/react": "^19.2.17",
|
|
32
32
|
"@types/react-dom": "^19.2.3",
|
|
33
33
|
"@vitejs/plugin-react": "^6.0.2",
|
|
34
34
|
"jsdom": "^29.1.1",
|
|
35
|
-
"typescript": "^
|
|
35
|
+
"typescript": "^6.0.3",
|
|
36
36
|
"vite": "^8.0.11",
|
|
37
37
|
"vitest": "^4.1.8"
|
|
38
38
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Course, Lesson, Quiz, Scenario, ThemeProvider } from "@lessonkit/react";
|
|
3
|
-
import { createCourseConfig } from "./courseConfig";
|
|
3
|
+
import { createCourseConfig, COURSE_THEME_PRESET } from "./courseConfig";
|
|
4
4
|
|
|
5
5
|
const courseConfig = createCourseConfig();
|
|
6
6
|
|
|
7
7
|
export default function App() {
|
|
8
8
|
return (
|
|
9
|
-
<ThemeProvider preset=
|
|
9
|
+
<ThemeProvider preset={COURSE_THEME_PRESET} mode="light">
|
|
10
10
|
<div className="app-shell">
|
|
11
11
|
<Course title="{{courseTitle}}" courseId="my-course" config={courseConfig}>
|
|
12
12
|
<Lesson title="My first lesson" lessonId="lesson-1">
|
|
@@ -44,6 +44,8 @@ describe("createCourseConfig", () => {
|
|
|
44
44
|
courseId: "my-course",
|
|
45
45
|
} as TelemetryEvent);
|
|
46
46
|
config.observability?.onXapiTransportError?.(new Error("transport"));
|
|
47
|
+
config.observability?.onXapiMappingError?.(new Error("mapping"));
|
|
48
|
+
config.observability?.onLxpackBridgeError?.(new Error("bridge"));
|
|
47
49
|
|
|
48
50
|
expect(warn).toHaveBeenCalled();
|
|
49
51
|
warn.mockRestore();
|
|
@@ -31,7 +31,9 @@ function createObservability(): NonNullable<LessonkitConfig["observability"]> {
|
|
|
31
31
|
},
|
|
32
32
|
onXapiQueueCap: () => report("xapi-queue-cap", {}),
|
|
33
33
|
onLxpackBridgeMiss: (event) => report("lxpack-bridge-miss", { event: event.name }),
|
|
34
|
+
onLxpackBridgeError: (err) => report("lxpack-bridge-error", { err }),
|
|
34
35
|
onXapiTransportError: (err) => report("xapi-transport", { err }),
|
|
36
|
+
onXapiMappingError: (err) => report("xapi-mapping", { err }),
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -98,13 +100,21 @@ function productionXapi(xapiProxyUrl: string | undefined): LessonkitConfig["xapi
|
|
|
98
100
|
};
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
/** Theme preset — keep in sync with lessonkit.json `course.theme.preset`. */
|
|
104
|
+
export const COURSE_THEME_PRESET = "default" as const;
|
|
105
|
+
|
|
101
106
|
export function createCourseConfig(): LessonkitConfig {
|
|
102
107
|
const { xapiProxyUrl, analyticsUrl } = readProxyUrls();
|
|
103
108
|
const useProductionTransports = import.meta.env.PROD || (xapiProxyUrl && analyticsUrl);
|
|
104
109
|
|
|
105
110
|
const config: LessonkitConfig = {
|
|
106
111
|
courseId: "my-course",
|
|
107
|
-
lxpack: {
|
|
112
|
+
lxpack: {
|
|
113
|
+
// Set bridge: "auto" when packaging for LMS (SCORM/xAPI/cmi5). In production, allowedParentOrigins
|
|
114
|
+
// is required — see https://lessonkit.readthedocs.io/en/latest/guides/react-developers/production-checklist.html
|
|
115
|
+
bridge: "off",
|
|
116
|
+
// allowedParentOrigins: ["https://your-lms.example"],
|
|
117
|
+
},
|
|
108
118
|
observability: createObservability(),
|
|
109
119
|
tracking: useProductionTransports ? productionTracking(analyticsUrl) : devConsoleTracking(),
|
|
110
120
|
xapi: useProductionTransports ? productionXapi(xapiProxyUrl) : devConsoleXapi(),
|