@learnpack/learnpack 5.0.347 → 5.0.349

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.
@@ -3271,6 +3271,7 @@ class ServeCommand extends SessionCommand_1.default {
3271
3271
  const addedLessons = [];
3272
3272
  let repairedTranslationsInLessons = 0;
3273
3273
  let repairedTranslationEntries = 0;
3274
+ let fixedLessons = 0;
3274
3275
  console.log(`📋 Checking ${syllabus.lessons.length} lessons in syllabus...`);
3275
3276
  // First pass: Check each lesson to see if it exists in the bucket and count files.
3276
3277
  // We try two possible folder names because they can differ by source:
@@ -3468,15 +3469,39 @@ class ServeCommand extends SessionCommand_1.default {
3468
3469
  repairedTranslationsInLessons += 1;
3469
3470
  }
3470
3471
  }
3472
+ // Fifth pass: fix lessons stuck in GENERATING or ERROR when the file exists in the bucket
3473
+ const primaryLanguage = syllabus.courseInfo.language || "en";
3474
+ for (const lesson of syllabus.lessons) {
3475
+ if (lesson.generated !== false)
3476
+ continue;
3477
+ if (lesson.status !== "GENERATING" && lesson.status !== "ERROR")
3478
+ continue;
3479
+ const candidateSlugs = [
3480
+ (0, creatorUtilities_2.slugify)(lesson.id + "-" + lesson.title),
3481
+ lesson.uid,
3482
+ ].filter(Boolean);
3483
+ const matchedSlug = candidateSlugs.find(s => translationsBySlug.has(s));
3484
+ if (!matchedSlug)
3485
+ continue;
3486
+ const langs = translationsBySlug.get(matchedSlug) || [];
3487
+ if (!langs.includes(primaryLanguage))
3488
+ continue;
3489
+ const prevStatus = lesson.status;
3490
+ lesson.generated = true;
3491
+ lesson.status = "DONE";
3492
+ fixedLessons += 1;
3493
+ console.log(`🔧 Fixed lesson: ${lesson.id} - "${lesson.title}" (was generated:false status:${prevStatus}, primary language "${primaryLanguage}" exists in bucket)`);
3494
+ }
3471
3495
  }
3472
3496
  catch (error) {
3473
3497
  console.error("⚠️ Could not reconcile lesson translations during syllabus sync:", error);
3474
3498
  }
3475
3499
  if (totalRemoved > 0 ||
3476
3500
  addedLessons.length > 0 ||
3477
- repairedTranslationsInLessons > 0) {
3501
+ repairedTranslationsInLessons > 0 ||
3502
+ fixedLessons > 0) {
3478
3503
  await saveSyllabus(courseSlug, syllabus, bucket);
3479
- console.log(`✅ Syllabus synchronized. Removed ${removedLessons.length} non-existent, ${duplicatesRemoved.length} duplicate(s); added ${addedLessons.length} from bucket; repaired translations in ${repairedTranslationsInLessons} lesson(s).`);
3504
+ console.log(`✅ Syllabus synchronized. Removed ${removedLessons.length} non-existent, ${duplicatesRemoved.length} duplicate(s); added ${addedLessons.length} from bucket; repaired translations in ${repairedTranslationsInLessons} lesson(s); fixed ${fixedLessons} stuck lesson(s).`);
3480
3505
  }
3481
3506
  else {
3482
3507
  console.log(`✅ Syllabus is already in sync. No changes.`);
@@ -3489,6 +3514,7 @@ class ServeCommand extends SessionCommand_1.default {
3489
3514
  removedLessons: removedLessons.length,
3490
3515
  duplicatesResolved: duplicatesRemoved.length,
3491
3516
  addedLessons: addedLessons.length,
3517
+ fixedLessons,
3492
3518
  repairedTranslationsInLessons,
3493
3519
  repairedTranslationEntries,
3494
3520
  removed: removedLessons,
@@ -9,7 +9,7 @@ const rimraf = require("rimraf");
9
9
  const uuid_1 = require("uuid");
10
10
  const shared_1 = require("./shared");
11
11
  async function exportToScorm(options) {
12
- var _a, _b;
12
+ var _a;
13
13
  const { courseSlug, bucket, outDir } = options;
14
14
  // 1. Create temporary folder
15
15
  const tmpName = (0, uuid_1.v4)();
@@ -22,23 +22,24 @@ async function exportToScorm(options) {
22
22
  // Paths
23
23
  const appDir = path.resolve(__dirname, "../../ui/_app");
24
24
  const configDir = path.join(scormOutDir, "config");
25
- // Check if _app exists and has content
26
- if (fs.existsSync(appDir) && fs.readdirSync(appDir).length > 0) {
27
- // Copy app.css and app.js to config/
28
- for (const file of ["app.css", "app.js"]) {
29
- const src = path.join(appDir, file);
30
- const dest = path.join(configDir, file);
31
- if (fs.existsSync(src)) {
32
- fs.copyFileSync(src, dest);
33
- }
25
+ // Copy the IDE bundle. app.js/app.css are declared as required files in the
26
+ // manifest, so their absence must be a hard error (otherwise we would ship a
27
+ // broken, blank package that references non-existent files).
28
+ for (const file of ["app.css", "app.js"]) {
29
+ const src = path.join(appDir, file);
30
+ const dest = path.join(configDir, file);
31
+ if (!fs.existsSync(src)) {
32
+ throw new Error(`SCORM export failed: IDE bundle (${file}) not found in ${appDir}. ` +
33
+ "Ensure the editor was downloaded/extracted before exporting.");
34
34
  }
35
- // Copy logo-192.png and logo-512.png to SCORM build root
36
- for (const file of ["logo-192.png", "logo-512.png"]) {
37
- const src = path.join(appDir, file);
38
- const dest = path.join(scormOutDir, file);
39
- if (fs.existsSync(src)) {
40
- fs.copyFileSync(src, dest);
41
- }
35
+ fs.copyFileSync(src, dest);
36
+ }
37
+ // Copy logo-192.png and logo-512.png to SCORM build root (optional assets)
38
+ for (const file of ["logo-192.png", "logo-512.png"]) {
39
+ const src = path.join(appDir, file);
40
+ const dest = path.join(scormOutDir, file);
41
+ if (fs.existsSync(src)) {
42
+ fs.copyFileSync(src, dest);
42
43
  }
43
44
  }
44
45
  // 3. Download exercises and .learn from bucket
@@ -46,26 +47,27 @@ async function exportToScorm(options) {
46
47
  await (0, shared_1.downloadS3Folder)(bucket, `courses/${courseSlug}/.learn/`, path.join(scormOutDir, ".learn"));
47
48
  // 4. Read learn.json for course info
48
49
  const learnJson = await (0, shared_1.getCourseMetadata)(bucket, courseSlug);
50
+ // Resolve the course title once, escaped for safe XML/HTML embedding
51
+ const courseTitle = (0, shared_1.escapeXml)(((_a = learnJson.title) === null || _a === void 0 ? void 0 : _a.en) || learnJson.title || courseSlug);
49
52
  // 5. Replace imsmanifest.xml
50
53
  const manifestPath = path.join(scormOutDir, "imsmanifest.xml");
51
54
  let manifest = fs.readFileSync(manifestPath, "utf-8");
52
- manifest = manifest.replace(/{{organization_name}}/g, "FourGeeksAcademy");
53
- manifest = manifest.replace(/{{course_title}}/g, ((_a = learnJson.title) === null || _a === void 0 ? void 0 : _a.en) || learnJson.title || courseSlug);
55
+ manifest = manifest.replace(/{{organization_name}}/g, (0, shared_1.escapeXml)("FourGeeksAcademy"));
56
+ manifest = manifest.replace(/{{course_title}}/g, courseTitle);
54
57
  // Generate exercises list
55
58
  const exercisesList = (0, shared_1.getFilesList)(path.join(scormOutDir, "exercises"), "exercises").join("\n ");
56
59
  manifest = manifest.replace(/{{exercises_list}}/g, exercisesList);
57
- // Generate images list (optional, if you have assets)
58
- let imagesList = "";
59
- const assetsDir = path.join(scormOutDir, ".learn", "assets");
60
- if (fs.existsSync(assetsDir)) {
61
- imagesList = (0, shared_1.getFilesList)(assetsDir, ".learn/assets").join("\n ");
62
- }
63
- manifest = manifest.replace(/{{images_list}}/g, imagesList);
60
+ // Declare all .learn/ files (config.json + assets) that ship in the package
61
+ const learnDir = path.join(scormOutDir, ".learn");
62
+ const learnFilesList = fs.existsSync(learnDir)
63
+ ? (0, shared_1.getFilesList)(learnDir, ".learn").join("\n ")
64
+ : "";
65
+ manifest = manifest.replace(/{{learn_files_list}}/g, learnFilesList);
64
66
  fs.writeFileSync(manifestPath, manifest, "utf-8");
65
67
  // 6. Replace title in config/index.html
66
68
  const configIndexPath = path.join(scormOutDir, "config", "index.html");
67
69
  let indexHtml = fs.readFileSync(configIndexPath, "utf-8");
68
- indexHtml = indexHtml.replace(/{{title}}/g, ((_b = learnJson.title) === null || _b === void 0 ? void 0 : _b.en) || learnJson.title || courseSlug);
70
+ indexHtml = indexHtml.replace(/{{title}}/g, courseTitle);
69
71
  fs.writeFileSync(configIndexPath, indexHtml, "utf-8");
70
72
  // 7. Compress as ZIP
71
73
  const zipPath = `${scormOutDir}.zip`;
@@ -1,3 +1,4 @@
1
+ export declare function escapeXml(value: string): string;
1
2
  export declare function copyDir(src: string, dest: string): void;
2
3
  export declare function downloadS3Folder(bucket: any, prefix: string, localPath: string): Promise<void>;
3
4
  export declare function getFilesList(folder: string, base: string): string[];
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.escapeXml = escapeXml;
3
4
  exports.copyDir = copyDir;
4
5
  exports.downloadS3Folder = downloadS3Folder;
5
6
  exports.getFilesList = getFilesList;
@@ -7,6 +8,15 @@ exports.getCourseMetadata = getCourseMetadata;
7
8
  const path = require("path");
8
9
  const fs = require("fs");
9
10
  const mkdirp = require("mkdirp");
11
+ // Escape a string so it can be safely embedded inside XML text or attributes
12
+ function escapeXml(value) {
13
+ return String(value)
14
+ .replace(/&/g, "&amp;")
15
+ .replace(/</g, "&lt;")
16
+ .replace(/>/g, "&gt;")
17
+ .replace(/"/g, "&quot;")
18
+ .replace(/'/g, "&apos;");
19
+ }
10
20
  // Utility function to copy directory recursively
11
21
  function copyDir(src, dest) {
12
22
  if (!fs.existsSync(dest))
@@ -47,7 +57,7 @@ function getFilesList(folder, base) {
47
57
  list = list.concat(getFilesList(filePath, path.join(base, file.name)));
48
58
  }
49
59
  else {
50
- list.push(`<file href="${path.join(base, file.name).replace(/\\/g, "/")}" />`);
60
+ list.push(`<file href="${escapeXml(path.join(base, file.name).replace(/\\/g, "/"))}" />`);
51
61
  }
52
62
  }
53
63
  return list;
@@ -109,6 +109,35 @@ function ScormProcessFinish() {
109
109
  }
110
110
  }
111
111
 
112
+ function ScormProcessCommit() {
113
+ var result;
114
+
115
+ if (initialized == false || finishCalled == true) {
116
+ return;
117
+ }
118
+
119
+ result = API.LMSCommit("");
120
+
121
+ if (result == SCORM_FALSE) {
122
+ var errorNumber = API.LMSGetLastError();
123
+ var errorString = API.LMSGetErrorString(errorNumber);
124
+ var diagnostic = API.LMSGetDiagnostic(errorNumber);
125
+
126
+ var errorDescription =
127
+ "Number: " +
128
+ errorNumber +
129
+ "\nDescription: " +
130
+ errorString +
131
+ "\nDiagnostic: " +
132
+ diagnostic;
133
+
134
+ alert(
135
+ "Error - Could not persist data to the LMS.\n\nYour results may not be recorded.\n\n" +
136
+ errorDescription
137
+ );
138
+ }
139
+ }
140
+
112
141
  function ScormProcessGetValue(element) {
113
142
  var result;
114
143
 
@@ -25,13 +25,12 @@
25
25
 
26
26
  {{exercises_list}}
27
27
 
28
- {{images_list}}
28
+ {{learn_files_list}}
29
29
 
30
30
  <file href="config/index.html" />
31
31
  <file href="config/api.js" />
32
32
  <file href="config/app.js" />
33
33
  <file href="config/app.css" />
34
- <file href="config/config.json" />
35
34
 
36
35
  </resource>
37
36
  </resources>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@learnpack/learnpack",
3
3
  "description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
4
- "version": "5.0.347",
4
+ "version": "5.0.349",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -4741,6 +4741,7 @@ class ServeCommand extends SessionCommand {
4741
4741
  }> = []
4742
4742
  let repairedTranslationsInLessons = 0
4743
4743
  let repairedTranslationEntries = 0
4744
+ let fixedLessons = 0
4744
4745
 
4745
4746
  console.log(
4746
4747
  `📋 Checking ${syllabus.lessons.length} lessons in syllabus...`
@@ -4983,6 +4984,35 @@ class ServeCommand extends SessionCommand {
4983
4984
  repairedTranslationsInLessons += 1
4984
4985
  }
4985
4986
  }
4987
+
4988
+ // Fifth pass: fix lessons stuck in GENERATING or ERROR when the file exists in the bucket
4989
+ const primaryLanguage = syllabus.courseInfo.language || "en"
4990
+ for (const lesson of syllabus.lessons) {
4991
+ if (lesson.generated !== false) continue
4992
+ if (lesson.status !== "GENERATING" && lesson.status !== "ERROR")
4993
+ continue
4994
+
4995
+ const candidateSlugs = [
4996
+ slugify(lesson.id + "-" + lesson.title),
4997
+ lesson.uid,
4998
+ ].filter(Boolean) as string[]
4999
+
5000
+ const matchedSlug = candidateSlugs.find(s =>
5001
+ translationsBySlug.has(s)
5002
+ )
5003
+ if (!matchedSlug) continue
5004
+
5005
+ const langs = translationsBySlug.get(matchedSlug) || []
5006
+ if (!langs.includes(primaryLanguage)) continue
5007
+
5008
+ const prevStatus = lesson.status
5009
+ lesson.generated = true
5010
+ lesson.status = "DONE"
5011
+ fixedLessons += 1
5012
+ console.log(
5013
+ `🔧 Fixed lesson: ${lesson.id} - "${lesson.title}" (was generated:false status:${prevStatus}, primary language "${primaryLanguage}" exists in bucket)`
5014
+ )
5015
+ }
4986
5016
  } catch (error) {
4987
5017
  console.error(
4988
5018
  "⚠️ Could not reconcile lesson translations during syllabus sync:",
@@ -4993,11 +5023,12 @@ class ServeCommand extends SessionCommand {
4993
5023
  if (
4994
5024
  totalRemoved > 0 ||
4995
5025
  addedLessons.length > 0 ||
4996
- repairedTranslationsInLessons > 0
5026
+ repairedTranslationsInLessons > 0 ||
5027
+ fixedLessons > 0
4997
5028
  ) {
4998
5029
  await saveSyllabus(courseSlug, syllabus, bucket)
4999
5030
  console.log(
5000
- `✅ Syllabus synchronized. Removed ${removedLessons.length} non-existent, ${duplicatesRemoved.length} duplicate(s); added ${addedLessons.length} from bucket; repaired translations in ${repairedTranslationsInLessons} lesson(s).`
5031
+ `✅ Syllabus synchronized. Removed ${removedLessons.length} non-existent, ${duplicatesRemoved.length} duplicate(s); added ${addedLessons.length} from bucket; repaired translations in ${repairedTranslationsInLessons} lesson(s); fixed ${fixedLessons} stuck lesson(s).`
5001
5032
  )
5002
5033
  } else {
5003
5034
  console.log(`✅ Syllabus is already in sync. No changes.`)
@@ -5011,6 +5042,7 @@ class ServeCommand extends SessionCommand {
5011
5042
  removedLessons: removedLessons.length,
5012
5043
  duplicatesResolved: duplicatesRemoved.length,
5013
5044
  addedLessons: addedLessons.length,
5045
+ fixedLessons,
5014
5046
  repairedTranslationsInLessons,
5015
5047
  repairedTranslationEntries,
5016
5048
  removed: removedLessons,