@lessonkit/cli 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -5
- package/dist/bin.js +343 -42
- package/dist/index.d.ts +7 -0
- package/dist/index.js +343 -42
- package/package.json +6 -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/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { createRequire as
|
|
2
|
+
import { createRequire as createRequire3 } from "module";
|
|
3
3
|
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, stat, 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));
|
|
@@ -146,7 +147,7 @@ async function isDirEmptyOrDotfilesOnly(dir) {
|
|
|
146
147
|
return entries.every((name) => name.startsWith("."));
|
|
147
148
|
}
|
|
148
149
|
function escapeJsxString(value) {
|
|
149
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/</g, "\\u003c").replace(/\r\n|\n|\r/g, "\\n");
|
|
150
|
+
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
151
|
}
|
|
151
152
|
async function copyTemplate(src, dest) {
|
|
152
153
|
await mkdir(dest, { recursive: true });
|
|
@@ -167,7 +168,7 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
|
|
|
167
168
|
const pkgPath = join(projectDir, "package.json");
|
|
168
169
|
const lessonkitPath = join(projectDir, "lessonkit.json");
|
|
169
170
|
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
170
|
-
pkg.name =
|
|
171
|
+
pkg.name = slug;
|
|
171
172
|
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
172
173
|
`, "utf8");
|
|
173
174
|
const lessonkit = JSON.parse(await readFile(lessonkitPath, "utf8"));
|
|
@@ -175,11 +176,11 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
|
|
|
175
176
|
const course = lessonkit.course;
|
|
176
177
|
course.courseId = slug;
|
|
177
178
|
course.title = projectName;
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
const courseTracking = course.tracking ?? {};
|
|
180
|
+
const courseXapi = courseTracking.xapi ?? {};
|
|
181
|
+
courseXapi.activityIri = `https://example.com/courses/${slug}`;
|
|
182
|
+
courseTracking.xapi = courseXapi;
|
|
183
|
+
course.tracking = courseTracking;
|
|
183
184
|
await writeFile(lessonkitPath, `${JSON.stringify(lessonkit, null, 2)}
|
|
184
185
|
`, "utf8");
|
|
185
186
|
const courseConfigPath = join(projectDir, "src", "courseConfig.ts");
|
|
@@ -192,6 +193,75 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
|
|
|
192
193
|
appSource = appSource.replace(/\{\{courseTitle\}\}/g, escapeJsxString(projectName));
|
|
193
194
|
await writeFile(appPath, appSource, "utf8");
|
|
194
195
|
}
|
|
196
|
+
async function backupConflictingFiles(stagingDir, projectDir) {
|
|
197
|
+
const backups = /* @__PURE__ */ new Map();
|
|
198
|
+
const stagingEntries = await readdir(stagingDir, { withFileTypes: true });
|
|
199
|
+
for (const entry of stagingEntries) {
|
|
200
|
+
const destPath = join(projectDir, entry.name);
|
|
201
|
+
if (!existsSync(destPath)) continue;
|
|
202
|
+
const destStat = await stat(destPath);
|
|
203
|
+
if (destStat.isFile()) {
|
|
204
|
+
backups.set(entry.name, await readFile(destPath));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return backups;
|
|
208
|
+
}
|
|
209
|
+
async function rollbackPromotedFiles(projectDir, stagingDir, preExisting, backups) {
|
|
210
|
+
const failures = [];
|
|
211
|
+
let stagingEntries;
|
|
212
|
+
try {
|
|
213
|
+
stagingEntries = await readdir(stagingDir, { withFileTypes: true });
|
|
214
|
+
} catch {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
for (const entry of stagingEntries) {
|
|
218
|
+
if (preExisting.has(entry.name)) continue;
|
|
219
|
+
try {
|
|
220
|
+
await rm(join(projectDir, entry.name), { recursive: true, force: true });
|
|
221
|
+
} catch (err) {
|
|
222
|
+
failures.push(
|
|
223
|
+
`remove ${entry.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const [name, content] of backups) {
|
|
228
|
+
try {
|
|
229
|
+
await writeFile(join(projectDir, name), content);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
failures.push(`restore ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (failures.length > 0) {
|
|
235
|
+
throw new CliError(`Init rollback failed: ${failures.join("; ")}`, {
|
|
236
|
+
code: "RUNTIME",
|
|
237
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function promoteStagingToProjectDir(stagingDir, projectDir) {
|
|
242
|
+
await mkdir(projectDir, { recursive: true });
|
|
243
|
+
const entries = await readdir(stagingDir, { withFileTypes: true });
|
|
244
|
+
for (const entry of entries) {
|
|
245
|
+
const srcPath = join(stagingDir, entry.name);
|
|
246
|
+
const destPath = join(projectDir, entry.name);
|
|
247
|
+
if (entry.isDirectory()) {
|
|
248
|
+
await cp(srcPath, destPath, { recursive: true });
|
|
249
|
+
} else if (entry.isFile()) {
|
|
250
|
+
await cp(srcPath, destPath);
|
|
251
|
+
} else {
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
var __testInitHelpers = {
|
|
256
|
+
getTemplateDir,
|
|
257
|
+
isDirEmpty,
|
|
258
|
+
isDirEmptyOrDotfilesOnly,
|
|
259
|
+
escapeJsxString,
|
|
260
|
+
copyTemplate,
|
|
261
|
+
promoteStagingToProjectDir,
|
|
262
|
+
rollbackPromotedFiles,
|
|
263
|
+
backupConflictingFiles
|
|
264
|
+
};
|
|
195
265
|
async function runInit(opts, logger) {
|
|
196
266
|
const cwd = process.cwd();
|
|
197
267
|
const rawName = opts.name ?? (opts.here ? slugifyId(basename(process.cwd()) || "my-course") : void 0);
|
|
@@ -219,11 +289,14 @@ async function runInit(opts, logger) {
|
|
|
219
289
|
}
|
|
220
290
|
);
|
|
221
291
|
}
|
|
222
|
-
if (opts.here && !await
|
|
223
|
-
throw new CliError(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
292
|
+
if (opts.here && !await isDirEmptyOrDotfilesOnly(projectDir) && !opts.force) {
|
|
293
|
+
throw new CliError(
|
|
294
|
+
`Directory is not empty: ${projectDir}. Use --here --force only when the directory is empty or contains dotfiles only (e.g. .git).`,
|
|
295
|
+
{
|
|
296
|
+
code: "INVALID_PROJECT",
|
|
297
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
298
|
+
}
|
|
299
|
+
);
|
|
227
300
|
}
|
|
228
301
|
if (opts.here && opts.force && !await isDirEmptyOrDotfilesOnly(projectDir)) {
|
|
229
302
|
throw new CliError(
|
|
@@ -241,11 +314,42 @@ async function runInit(opts, logger) {
|
|
|
241
314
|
exitCode: EXIT_INVALID_PROJECT
|
|
242
315
|
});
|
|
243
316
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
317
|
+
const stagingDir = opts.here ? join(cwd, `.lessonkit-init-${randomUUID()}`) : join(cwd, `.${slug}-init-${randomUUID()}`);
|
|
318
|
+
try {
|
|
319
|
+
await copyTemplate(templateDir, stagingDir);
|
|
320
|
+
await applyTemplateSubstitutions(stagingDir, projectName, slug);
|
|
321
|
+
if (!opts.skipInstall) {
|
|
322
|
+
if (!opts.json) logger.log(`Installing dependencies in ${stagingDir}\u2026`);
|
|
323
|
+
await runNpmInstall(stagingDir);
|
|
324
|
+
}
|
|
325
|
+
if (opts.here) {
|
|
326
|
+
const preExisting = new Set(await readdir(projectDir));
|
|
327
|
+
const backups = await backupConflictingFiles(stagingDir, projectDir);
|
|
328
|
+
try {
|
|
329
|
+
await __testInitHelpers.promoteStagingToProjectDir(stagingDir, projectDir);
|
|
330
|
+
} catch (promoteErr) {
|
|
331
|
+
try {
|
|
332
|
+
await rollbackPromotedFiles(projectDir, stagingDir, preExisting, backups);
|
|
333
|
+
} catch (rollbackErr) {
|
|
334
|
+
const promoteMessage = promoteErr instanceof Error ? promoteErr.message : String(promoteErr);
|
|
335
|
+
const rollbackMessage = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
|
|
336
|
+
throw new CliError(`${promoteMessage}; ${rollbackMessage}`, {
|
|
337
|
+
code: "RUNTIME",
|
|
338
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
throw promoteErr;
|
|
342
|
+
}
|
|
343
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
344
|
+
} else {
|
|
345
|
+
await rename(stagingDir, projectDir);
|
|
346
|
+
}
|
|
347
|
+
} catch (err) {
|
|
348
|
+
await rm(stagingDir, { recursive: true, force: true }).catch(
|
|
349
|
+
/* v8 ignore next */
|
|
350
|
+
() => void 0
|
|
351
|
+
);
|
|
352
|
+
throw err;
|
|
249
353
|
}
|
|
250
354
|
if (!opts.json) {
|
|
251
355
|
logger.log(`Created LessonKit project at ${projectDir}`);
|
|
@@ -254,6 +358,12 @@ async function runInit(opts, logger) {
|
|
|
254
358
|
return { ok: true, command: "init", projectRoot: projectDir };
|
|
255
359
|
}
|
|
256
360
|
|
|
361
|
+
// src/commands/dev.ts
|
|
362
|
+
import { existsSync as existsSync3 } from "fs";
|
|
363
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
364
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
365
|
+
import { assertSpaDistContentsSafe } from "@lessonkit/lxpack";
|
|
366
|
+
|
|
257
367
|
// src/lib/project.ts
|
|
258
368
|
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
259
369
|
import { readFile as readFile2 } from "fs/promises";
|
|
@@ -262,10 +372,13 @@ import { dirname as dirname2, join as join2, parse, resolve as resolve2 } from "
|
|
|
262
372
|
import { parseLessonkitManifest } from "@lessonkit/lxpack";
|
|
263
373
|
var LESSONKIT_JSON = "lessonkit.json";
|
|
264
374
|
var PACKAGE_JSON = "package.json";
|
|
375
|
+
function isSchemaVersionOne(value) {
|
|
376
|
+
return value === 1 || value === "1";
|
|
377
|
+
}
|
|
265
378
|
function isProjectManifest(configPath) {
|
|
266
379
|
try {
|
|
267
380
|
const raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
268
|
-
return raw.schemaVersion
|
|
381
|
+
return isSchemaVersionOne(raw.schemaVersion) && typeof raw.name === "string" && raw.course !== null && typeof raw.course === "object" && !Array.isArray(raw.course);
|
|
269
382
|
} catch {
|
|
270
383
|
return false;
|
|
271
384
|
}
|
|
@@ -438,7 +551,12 @@ function resolvePackageOutput(project, target, override) {
|
|
|
438
551
|
if (override) {
|
|
439
552
|
try {
|
|
440
553
|
const resolved = resolveSafePackageOutputOverride(project.root, override);
|
|
441
|
-
|
|
554
|
+
const isZipOutput = override.trim().toLowerCase().endsWith(".zip");
|
|
555
|
+
return {
|
|
556
|
+
output: resolved,
|
|
557
|
+
dir: target === "standalone" && !isZipOutput,
|
|
558
|
+
outputBaseDir
|
|
559
|
+
};
|
|
442
560
|
} catch (err) {
|
|
443
561
|
const message = err instanceof Error ? err.message : String(err);
|
|
444
562
|
throw new CliError(message, { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT });
|
|
@@ -449,13 +567,24 @@ function resolvePackageOutput(project, target, override) {
|
|
|
449
567
|
}
|
|
450
568
|
return { output: `${outputBaseDir}/course-${target}.zip`, dir: false, outputBaseDir };
|
|
451
569
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
570
|
+
function stripOutDirFromViteArgs(viteArgs) {
|
|
571
|
+
const stripped = [];
|
|
572
|
+
for (let i = 0; i < viteArgs.length; i++) {
|
|
573
|
+
const arg = viteArgs[i];
|
|
574
|
+
if (arg === "--outDir" || arg === "-o") {
|
|
575
|
+
i++;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (arg.startsWith("--outDir=")) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
stripped.push(arg);
|
|
457
582
|
}
|
|
458
|
-
return
|
|
583
|
+
return stripped;
|
|
584
|
+
}
|
|
585
|
+
function resolveViteBuildArgv(project, viteArgs = []) {
|
|
586
|
+
const passthrough = stripOutDirFromViteArgs(viteArgs);
|
|
587
|
+
return ["build", ...passthrough, "--outDir", project.paths.spaDistDir];
|
|
459
588
|
}
|
|
460
589
|
function parsePackageTarget(value) {
|
|
461
590
|
if (!value) {
|
|
@@ -473,7 +602,8 @@ async function runDev(opts) {
|
|
|
473
602
|
const pkg = await readPackageJson(project.root);
|
|
474
603
|
assertViteProject(pkg, project.root);
|
|
475
604
|
const viteJs = resolveViteJs(project.root);
|
|
476
|
-
|
|
605
|
+
const devArgs = stripOutDirFromViteArgs(opts.viteArgs ?? []);
|
|
606
|
+
await runCommand(process.execPath, [viteJs, ...devArgs], {
|
|
477
607
|
cwd: project.root,
|
|
478
608
|
timeoutMs: 0
|
|
479
609
|
});
|
|
@@ -484,15 +614,28 @@ async function runBuild(opts) {
|
|
|
484
614
|
const pkg = await readPackageJson(project.root);
|
|
485
615
|
assertViteProject(pkg, project.root);
|
|
486
616
|
const viteJs = resolveViteJs(project.root);
|
|
487
|
-
const
|
|
488
|
-
await
|
|
617
|
+
const distDir = resolveDistDir(project);
|
|
618
|
+
await mkdir2(dirname3(distDir), { recursive: true });
|
|
619
|
+
if (existsSync3(distDir)) {
|
|
620
|
+
await assertSpaDistContentsSafe({ main: distDir }, project.root);
|
|
621
|
+
}
|
|
622
|
+
const buildArgs = resolveViteBuildArgv(project, opts.viteArgs);
|
|
623
|
+
await runCommand(process.execPath, [viteJs, ...buildArgs], {
|
|
489
624
|
cwd: project.root
|
|
490
625
|
});
|
|
626
|
+
const indexHtml = join3(distDir, "index.html");
|
|
627
|
+
if (!existsSync3(indexHtml)) {
|
|
628
|
+
throw new CliError(
|
|
629
|
+
`Build did not produce index.html at ${indexHtml}. Check paths.spaDistDir in lessonkit.json.`,
|
|
630
|
+
{ code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
|
|
631
|
+
);
|
|
632
|
+
}
|
|
491
633
|
return { ok: true, command: "build", projectRoot: project.root };
|
|
492
634
|
}
|
|
493
635
|
|
|
494
636
|
// src/commands/package.ts
|
|
495
|
-
import { existsSync as
|
|
637
|
+
import { existsSync as existsSync4 } from "fs";
|
|
638
|
+
import { isAbsolute } from "path";
|
|
496
639
|
import { packageLessonkitCourse } from "@lessonkit/lxpack";
|
|
497
640
|
async function runPackage(opts) {
|
|
498
641
|
let target;
|
|
@@ -510,7 +653,7 @@ async function runPackage(opts) {
|
|
|
510
653
|
}
|
|
511
654
|
const project = await loadProject(opts.cwd ?? process.cwd());
|
|
512
655
|
const distDir = resolveDistDir(project);
|
|
513
|
-
if (opts.noBuild && !
|
|
656
|
+
if (opts.noBuild && !existsSync4(distDir)) {
|
|
514
657
|
throw new CliError(
|
|
515
658
|
`dist directory not found at ${distDir}. Run lessonkit build before packaging with --no-build.`,
|
|
516
659
|
{
|
|
@@ -523,7 +666,7 @@ async function runPackage(opts) {
|
|
|
523
666
|
if (!opts.noBuild) {
|
|
524
667
|
await runBuild({ cwd: project.root, json: opts.json });
|
|
525
668
|
}
|
|
526
|
-
if (!
|
|
669
|
+
if (!existsSync4(distDir)) {
|
|
527
670
|
throw new CliError(`Build completed but dist directory not found at ${distDir}.`, {
|
|
528
671
|
code: "INVALID_PROJECT",
|
|
529
672
|
exitCode: EXIT_INVALID_PROJECT
|
|
@@ -535,14 +678,20 @@ async function runPackage(opts) {
|
|
|
535
678
|
if (!opts.noBuild) {
|
|
536
679
|
await runBuild({ cwd: project.root, json: opts.json });
|
|
537
680
|
}
|
|
538
|
-
if (!
|
|
681
|
+
if (!existsSync4(distDir)) {
|
|
539
682
|
throw new CliError(`dist directory not found at ${distDir}. Run lessonkit build first.`, {
|
|
540
683
|
code: "INVALID_PROJECT",
|
|
541
684
|
exitCode: EXIT_INVALID_PROJECT
|
|
542
685
|
});
|
|
543
686
|
}
|
|
544
687
|
const outDir = resolveLxpackOutDir(project);
|
|
545
|
-
const { output, dir, outputBaseDir } = resolvePackageOutput(
|
|
688
|
+
const { output: resolvedOutput, dir, outputBaseDir } = resolvePackageOutput(
|
|
689
|
+
project,
|
|
690
|
+
target,
|
|
691
|
+
opts.out
|
|
692
|
+
);
|
|
693
|
+
const trimmedOut = opts.out?.trim();
|
|
694
|
+
const output = trimmedOut && !isAbsolute(trimmedOut) ? trimmedOut : resolvedOutput;
|
|
546
695
|
const result = await packageLessonkitCourse({
|
|
547
696
|
descriptor: project.course,
|
|
548
697
|
outDir,
|
|
@@ -551,7 +700,9 @@ async function runPackage(opts) {
|
|
|
551
700
|
target,
|
|
552
701
|
output,
|
|
553
702
|
dir,
|
|
554
|
-
outputBaseDir
|
|
703
|
+
outputBaseDir,
|
|
704
|
+
strictParity: opts.strictParity,
|
|
705
|
+
strictBuild: opts.strict
|
|
555
706
|
});
|
|
556
707
|
if (!result.ok) {
|
|
557
708
|
throw new CliError("Packaging failed.", {
|
|
@@ -583,6 +734,112 @@ async function runPackage(opts) {
|
|
|
583
734
|
};
|
|
584
735
|
}
|
|
585
736
|
|
|
737
|
+
// src/commands/export.ts
|
|
738
|
+
import { existsSync as existsSync5 } from "fs";
|
|
739
|
+
import { relative, resolve as resolve4 } from "path";
|
|
740
|
+
import { exportLkcourse, resolveSafePackageOutputOverride as resolveSafePackageOutputOverride2 } from "@lessonkit/lxpack";
|
|
741
|
+
function resolveExportOutput(projectRoot, override, defaultName) {
|
|
742
|
+
if (override) {
|
|
743
|
+
try {
|
|
744
|
+
return resolveSafePackageOutputOverride2(projectRoot, override);
|
|
745
|
+
} catch (err) {
|
|
746
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
747
|
+
throw new CliError(message, { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT });
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return resolve4(projectRoot, `${defaultName ?? "course"}.lkcourse`);
|
|
751
|
+
}
|
|
752
|
+
async function runExport(opts) {
|
|
753
|
+
const project = await loadProject(opts.cwd ?? process.cwd());
|
|
754
|
+
const distDir = resolve4(project.root, project.paths.spaDistDir);
|
|
755
|
+
if (opts.noBuild && !existsSync5(distDir)) {
|
|
756
|
+
throw new CliError(
|
|
757
|
+
`dist directory not found at ${distDir}. Run lessonkit build before export with --no-build.`,
|
|
758
|
+
{
|
|
759
|
+
code: "INVALID_PROJECT",
|
|
760
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
761
|
+
}
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
if (!opts.noBuild) {
|
|
765
|
+
await runBuild({ cwd: project.root, json: opts.json });
|
|
766
|
+
}
|
|
767
|
+
if (!existsSync5(distDir)) {
|
|
768
|
+
throw new CliError(`dist directory not found at ${distDir}. Run lessonkit build first.`, {
|
|
769
|
+
code: "INVALID_PROJECT",
|
|
770
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
const resolvedOut = resolveExportOutput(project.root, opts.out, project.name);
|
|
774
|
+
const outRelative = relative(project.root, resolvedOut).replace(/\\/g, "/");
|
|
775
|
+
const result = await exportLkcourse({
|
|
776
|
+
projectRoot: project.root,
|
|
777
|
+
manifest: project,
|
|
778
|
+
outPath: outRelative,
|
|
779
|
+
includeBlockTree: Boolean(opts.withBlockTree)
|
|
780
|
+
});
|
|
781
|
+
if (!result.ok) {
|
|
782
|
+
throw new CliError(
|
|
783
|
+
result.issues.map((i) => `${i.path}: ${i.message}`).join("; "),
|
|
784
|
+
{ code: "EXPORT_FAILED", exitCode: EXIT_PACKAGING }
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
ok: true,
|
|
789
|
+
command: "export",
|
|
790
|
+
projectRoot: project.root,
|
|
791
|
+
archivePath: result.archivePath,
|
|
792
|
+
fileCount: result.fileCount,
|
|
793
|
+
includeBlockTree: result.includeBlockTree
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// src/commands/blocks.ts
|
|
798
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
799
|
+
import { createRequire as createRequire2 } from "module";
|
|
800
|
+
function loadBlockCatalog() {
|
|
801
|
+
const require3 = createRequire2(import.meta.url);
|
|
802
|
+
const catalogPath = require3.resolve("@lessonkit/react/block-catalog.v3.json");
|
|
803
|
+
return JSON.parse(readFileSync2(catalogPath, "utf8"));
|
|
804
|
+
}
|
|
805
|
+
function filterEntries(entries, opts) {
|
|
806
|
+
return entries.filter((entry) => {
|
|
807
|
+
if (opts.category && entry.category !== opts.category) return false;
|
|
808
|
+
if (opts.tier && entry.tier !== opts.tier) return false;
|
|
809
|
+
return true;
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
async function runBlocksList(opts) {
|
|
813
|
+
const catalog = loadBlockCatalog();
|
|
814
|
+
const entries = filterEntries(catalog.entries, opts);
|
|
815
|
+
if (!opts.json) {
|
|
816
|
+
const lines = [
|
|
817
|
+
"type category h5pMachineName",
|
|
818
|
+
...entries.map(
|
|
819
|
+
(entry) => [
|
|
820
|
+
entry.type,
|
|
821
|
+
entry.category ?? "\u2014",
|
|
822
|
+
entry.h5pMachineName ?? "\u2014"
|
|
823
|
+
].join(" ")
|
|
824
|
+
)
|
|
825
|
+
];
|
|
826
|
+
return {
|
|
827
|
+
ok: true,
|
|
828
|
+
command: "blocks list",
|
|
829
|
+
schemaVersion: catalog.schemaVersion,
|
|
830
|
+
count: entries.length,
|
|
831
|
+
text: lines.join("\n")
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
ok: true,
|
|
836
|
+
command: "blocks list",
|
|
837
|
+
schemaVersion: catalog.schemaVersion,
|
|
838
|
+
count: entries.length,
|
|
839
|
+
entries
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
586
843
|
// src/lib/logger.ts
|
|
587
844
|
function createLogger(opts) {
|
|
588
845
|
if (opts?.json) {
|
|
@@ -597,7 +854,7 @@ function createLogger(opts) {
|
|
|
597
854
|
}
|
|
598
855
|
|
|
599
856
|
// src/index.ts
|
|
600
|
-
var require2 =
|
|
857
|
+
var require2 = createRequire3(import.meta.url);
|
|
601
858
|
var { version } = require2("../package.json");
|
|
602
859
|
async function handleCommand(fn, logger, json) {
|
|
603
860
|
try {
|
|
@@ -654,7 +911,7 @@ function createProgram(baseLogger = console) {
|
|
|
654
911
|
);
|
|
655
912
|
}
|
|
656
913
|
);
|
|
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) => {
|
|
914
|
+
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("--strict", "Treat Vite build warnings as packaging failures").option("--json", "Emit structured JSON result").action(async (opts) => {
|
|
658
915
|
const logger = createLogger({ json: opts.json });
|
|
659
916
|
await handleCommand(
|
|
660
917
|
async () => {
|
|
@@ -663,7 +920,9 @@ function createProgram(baseLogger = console) {
|
|
|
663
920
|
cwd: opts.cwd,
|
|
664
921
|
noBuild: opts.build === false,
|
|
665
922
|
out: opts.out,
|
|
666
|
-
json: opts.json
|
|
923
|
+
json: opts.json,
|
|
924
|
+
strictParity: opts.strictParity,
|
|
925
|
+
strict: opts.strict
|
|
667
926
|
});
|
|
668
927
|
if (!opts.json && result.ok && result.command === "package") {
|
|
669
928
|
if (result.target === "react-vite") {
|
|
@@ -681,8 +940,50 @@ function createProgram(baseLogger = console) {
|
|
|
681
940
|
Boolean(opts.json)
|
|
682
941
|
);
|
|
683
942
|
});
|
|
684
|
-
|
|
685
|
-
|
|
943
|
+
addCwdAndJson(
|
|
944
|
+
program.command("export").description("Export a portable .lkcourse archive (manifest + interchange + dist)").option("--out <path>", "Output .lkcourse path (relative to project root)").option("--no-build", "Skip implicit Vite build").option("--with-block-tree", "Include optional block-tree.json from src scan")
|
|
945
|
+
).action(async (opts) => {
|
|
946
|
+
const logger = createLogger({ json: opts.json });
|
|
947
|
+
await handleCommand(
|
|
948
|
+
async () => {
|
|
949
|
+
const result = await runExport({
|
|
950
|
+
cwd: opts.cwd,
|
|
951
|
+
out: opts.out,
|
|
952
|
+
noBuild: opts.build === false,
|
|
953
|
+
withBlockTree: opts.withBlockTree,
|
|
954
|
+
json: opts.json
|
|
955
|
+
});
|
|
956
|
+
if (!opts.json && result.ok && result.command === "export") {
|
|
957
|
+
logger.log(`Exported .lkcourse \u2192 ${result.archivePath} (${result.fileCount} files)`);
|
|
958
|
+
}
|
|
959
|
+
return result;
|
|
960
|
+
},
|
|
961
|
+
logger,
|
|
962
|
+
Boolean(opts.json)
|
|
963
|
+
);
|
|
964
|
+
});
|
|
965
|
+
program.command("blocks").description("Block registry commands").command("list").description("List runtime blocks from block-catalog.v3.json").option("--json", "Emit structured JSON result").option("--category <category>", "Filter by category (container, assessment, content, compound)").option("--tier <tier>", "Filter by tier (A, B, C, D, E)").action(async (opts) => {
|
|
966
|
+
const logger = createLogger({ json: opts.json });
|
|
967
|
+
await handleCommand(
|
|
968
|
+
async () => {
|
|
969
|
+
const result = await runBlocksList({
|
|
970
|
+
json: opts.json,
|
|
971
|
+
category: opts.category,
|
|
972
|
+
tier: opts.tier
|
|
973
|
+
});
|
|
974
|
+
if (!opts.json && result.ok && "text" in result && typeof result.text === "string") {
|
|
975
|
+
logger.log(result.text);
|
|
976
|
+
}
|
|
977
|
+
return result;
|
|
978
|
+
},
|
|
979
|
+
logger,
|
|
980
|
+
Boolean(opts.json)
|
|
981
|
+
);
|
|
982
|
+
});
|
|
983
|
+
program.command("publish").description("[maintainers] Not implemented \u2014 use Changesets (see RELEASING.md)").action(() => {
|
|
984
|
+
baseLogger.log(
|
|
985
|
+
"lessonkit publish is not implemented. Monorepo releases use Changesets: npm run changeset && npm run version-packages && npm run release. See RELEASING.md."
|
|
986
|
+
);
|
|
686
987
|
});
|
|
687
988
|
return program;
|
|
688
989
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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,12 @@
|
|
|
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.6.0",
|
|
49
|
+
"@lessonkit/lxpack": "1.6.0",
|
|
50
|
+
"@lessonkit/react": "1.6.0",
|
|
50
51
|
"commander": "^15.0.0"
|
|
51
52
|
},
|
|
52
53
|
"engines": {
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
"devDependencies": {
|
|
56
57
|
"@types/node": "^25.9.2",
|
|
57
58
|
"tsup": "^8.5.0",
|
|
58
|
-
"typescript": "^
|
|
59
|
+
"typescript": "^6.0.3",
|
|
59
60
|
"vitest": "^4.1.8"
|
|
60
61
|
}
|
|
61
62
|
}
|
|
@@ -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.6.0",
|
|
20
|
+
"@lessonkit/react": "^1.6.0",
|
|
21
|
+
"@lessonkit/themes": "^1.6.0",
|
|
22
|
+
"@lessonkit/xapi": "^1.6.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.6.0",
|
|
28
|
+
"@lessonkit/lxpack": "^1.6.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
|
}
|