@learnpack/learnpack 5.0.335 → 5.0.339

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.
Files changed (48) hide show
  1. package/bin/run +17 -17
  2. package/lib/commands/init.js +41 -41
  3. package/lib/commands/serve.js +589 -126
  4. package/lib/creatorDist/assets/index-BhqDgBS9.js +8448 -78631
  5. package/lib/creatorDist/assets/index-CjddKHB_.css +1 -1688
  6. package/lib/managers/config/exercise.js +2 -14
  7. package/lib/managers/readmeHistoryService.js +3 -1
  8. package/lib/managers/server/routes.js +2 -1
  9. package/lib/utils/configBuilder.js +2 -1
  10. package/lib/utils/creatorUtilities.js +14 -14
  11. package/lib/utils/exerciseFileOrder.d.ts +20 -0
  12. package/lib/utils/exerciseFileOrder.js +49 -0
  13. package/lib/utils/export/epub.js +26 -26
  14. package/lib/utils/readmeSanitizer.d.ts +8 -0
  15. package/lib/utils/readmeSanitizer.js +13 -0
  16. package/lib/utils/templates/epub/epub.css +146 -146
  17. package/lib/utils/templates/scorm/config/api.js +175 -175
  18. package/package.json +1 -1
  19. package/src/commands/init.ts +655 -655
  20. package/src/commands/publish.ts +670 -670
  21. package/src/commands/serve.ts +5853 -5216
  22. package/src/creator/eslint.config.js +28 -28
  23. package/src/creator/src/index.css +227 -227
  24. package/src/creator/src/utils/lib.ts +471 -471
  25. package/src/creatorDist/assets/index-BhqDgBS9.js +8448 -78631
  26. package/src/creatorDist/assets/index-CjddKHB_.css +1 -1688
  27. package/src/managers/config/exercise.ts +3 -15
  28. package/src/managers/readmeHistoryService.ts +3 -1
  29. package/src/managers/server/routes.ts +15 -6
  30. package/src/managers/session.ts +184 -184
  31. package/src/ui/_app/app.css +1 -1
  32. package/src/ui/_app/app.js +1950 -1878
  33. package/src/ui/app.tar.gz +0 -0
  34. package/src/utils/api.ts +675 -675
  35. package/src/utils/configBuilder.ts +102 -100
  36. package/src/utils/creatorUtilities.ts +536 -536
  37. package/src/utils/errors.ts +108 -108
  38. package/src/utils/exerciseFileOrder.ts +50 -0
  39. package/src/utils/export/epub.ts +553 -553
  40. package/src/utils/export/index.ts +4 -4
  41. package/src/utils/export/scorm.ts +121 -121
  42. package/src/utils/export/shared.ts +61 -61
  43. package/src/utils/export/types.ts +25 -25
  44. package/src/utils/export/zip.ts +55 -55
  45. package/src/utils/readmeSanitizer.ts +10 -0
  46. package/src/utils/rigoActions.ts +642 -642
  47. package/src/utils/templates/epub/epub.css +146 -146
  48. package/src/utils/templates/scorm/config/api.js +175 -175
@@ -1,4 +1,4 @@
1
- export { exportToScorm } from "./scorm";
2
- export { exportToEpub } from "./epub";
3
- export { exportToZip } from "./zip";
4
- export { ExportFormat, ExportOptions, EpubMetadata } from "./types";
1
+ export { exportToScorm } from "./scorm";
2
+ export { exportToEpub } from "./epub";
3
+ export { exportToZip } from "./zip";
4
+ export { ExportFormat, ExportOptions, EpubMetadata } from "./types";
@@ -1,121 +1,121 @@
1
- import * as path from "path";
2
- import * as fs from "fs";
3
- import * as archiver from "archiver";
4
- import * as mkdirp from "mkdirp";
5
- import * as rimraf from "rimraf";
6
- import { v4 as uuidv4 } from "uuid";
7
- import { ExportOptions, CourseMetadata } from "./types";
8
- import {
9
- copyDir,
10
- downloadS3Folder,
11
- getFilesList,
12
- getCourseMetadata,
13
- } from "./shared";
14
-
15
- export async function exportToScorm(options: ExportOptions): Promise<string> {
16
- const { courseSlug, bucket, outDir } = options;
17
-
18
- // 1. Create temporary folder
19
- const tmpName = uuidv4();
20
- const scormOutDir = path.join(outDir, tmpName);
21
- rimraf.sync(scormOutDir);
22
- mkdirp.sync(scormOutDir);
23
-
24
- // 2. Copy SCORM template
25
- const scormTpl = path.join(__dirname, "../templates/scorm");
26
- copyDir(scormTpl, scormOutDir);
27
-
28
- // Paths
29
- const appDir = path.resolve(__dirname, "../../ui/_app");
30
- const configDir = path.join(scormOutDir, "config");
31
-
32
- // Check if _app exists and has content
33
- if (fs.existsSync(appDir) && fs.readdirSync(appDir).length > 0) {
34
- // Copy app.css and app.js to config/
35
- for (const file of ["app.css", "app.js"]) {
36
- const src = path.join(appDir, file);
37
- const dest = path.join(configDir, file);
38
- if (fs.existsSync(src)) {
39
- fs.copyFileSync(src, dest);
40
- }
41
- }
42
-
43
- // Copy logo-192.png and logo-512.png to SCORM build root
44
- for (const file of ["logo-192.png", "logo-512.png"]) {
45
- const src = path.join(appDir, file);
46
- const dest = path.join(scormOutDir, file);
47
- if (fs.existsSync(src)) {
48
- fs.copyFileSync(src, dest);
49
- }
50
- }
51
- }
52
-
53
- // 3. Download exercises and .learn from bucket
54
- await downloadS3Folder(
55
- bucket,
56
- `courses/${courseSlug}/exercises/`,
57
- path.join(scormOutDir, "exercises")
58
- );
59
- await downloadS3Folder(
60
- bucket,
61
- `courses/${courseSlug}/.learn/`,
62
- path.join(scormOutDir, ".learn")
63
- );
64
-
65
- // 4. Read learn.json for course info
66
- const learnJson = await getCourseMetadata(bucket, courseSlug);
67
-
68
- // 5. Replace imsmanifest.xml
69
- const manifestPath = path.join(scormOutDir, "imsmanifest.xml");
70
- let manifest = fs.readFileSync(manifestPath, "utf-8");
71
- manifest = manifest.replace(/{{organization_name}}/g, "FourGeeksAcademy");
72
- manifest = manifest.replace(
73
- /{{course_title}}/g,
74
- learnJson.title?.en || learnJson.title || courseSlug
75
- );
76
-
77
- // Generate exercises list
78
- const exercisesList = getFilesList(
79
- path.join(scormOutDir, "exercises"),
80
- "exercises"
81
- ).join("\n ");
82
- manifest = manifest.replace(/{{exercises_list}}/g, exercisesList);
83
-
84
- // Generate images list (optional, if you have assets)
85
- let imagesList = "";
86
- const assetsDir = path.join(scormOutDir, ".learn", "assets");
87
- if (fs.existsSync(assetsDir)) {
88
- imagesList = getFilesList(assetsDir, ".learn/assets").join("\n ");
89
- }
90
- manifest = manifest.replace(/{{images_list}}/g, imagesList);
91
-
92
- fs.writeFileSync(manifestPath, manifest, "utf-8");
93
-
94
- // 6. Replace title in config/index.html
95
- const configIndexPath = path.join(scormOutDir, "config", "index.html");
96
- let indexHtml = fs.readFileSync(configIndexPath, "utf-8");
97
- indexHtml = indexHtml.replace(
98
- /{{title}}/g,
99
- learnJson.title?.en || learnJson.title || courseSlug
100
- );
101
- fs.writeFileSync(configIndexPath, indexHtml, "utf-8");
102
-
103
- // 7. Compress as ZIP
104
- const zipPath = `${scormOutDir}.zip`;
105
- await new Promise((resolve: any, reject: any) => {
106
- const output = fs.createWriteStream(zipPath);
107
- const archive = archiver("zip", { zlib: { level: 9 } });
108
-
109
- output.on("close", resolve);
110
- archive.on("error", reject);
111
-
112
- archive.pipe(output);
113
- archive.directory(scormOutDir, false);
114
- archive.finalize();
115
- });
116
-
117
- // Clean up temporary directory
118
- rimraf.sync(scormOutDir);
119
-
120
- return zipPath;
121
- }
1
+ import * as path from "path";
2
+ import * as fs from "fs";
3
+ import * as archiver from "archiver";
4
+ import * as mkdirp from "mkdirp";
5
+ import * as rimraf from "rimraf";
6
+ import { v4 as uuidv4 } from "uuid";
7
+ import { ExportOptions, CourseMetadata } from "./types";
8
+ import {
9
+ copyDir,
10
+ downloadS3Folder,
11
+ getFilesList,
12
+ getCourseMetadata,
13
+ } from "./shared";
14
+
15
+ export async function exportToScorm(options: ExportOptions): Promise<string> {
16
+ const { courseSlug, bucket, outDir } = options;
17
+
18
+ // 1. Create temporary folder
19
+ const tmpName = uuidv4();
20
+ const scormOutDir = path.join(outDir, tmpName);
21
+ rimraf.sync(scormOutDir);
22
+ mkdirp.sync(scormOutDir);
23
+
24
+ // 2. Copy SCORM template
25
+ const scormTpl = path.join(__dirname, "../templates/scorm");
26
+ copyDir(scormTpl, scormOutDir);
27
+
28
+ // Paths
29
+ const appDir = path.resolve(__dirname, "../../ui/_app");
30
+ const configDir = path.join(scormOutDir, "config");
31
+
32
+ // Check if _app exists and has content
33
+ if (fs.existsSync(appDir) && fs.readdirSync(appDir).length > 0) {
34
+ // Copy app.css and app.js to config/
35
+ for (const file of ["app.css", "app.js"]) {
36
+ const src = path.join(appDir, file);
37
+ const dest = path.join(configDir, file);
38
+ if (fs.existsSync(src)) {
39
+ fs.copyFileSync(src, dest);
40
+ }
41
+ }
42
+
43
+ // Copy logo-192.png and logo-512.png to SCORM build root
44
+ for (const file of ["logo-192.png", "logo-512.png"]) {
45
+ const src = path.join(appDir, file);
46
+ const dest = path.join(scormOutDir, file);
47
+ if (fs.existsSync(src)) {
48
+ fs.copyFileSync(src, dest);
49
+ }
50
+ }
51
+ }
52
+
53
+ // 3. Download exercises and .learn from bucket
54
+ await downloadS3Folder(
55
+ bucket,
56
+ `courses/${courseSlug}/exercises/`,
57
+ path.join(scormOutDir, "exercises")
58
+ );
59
+ await downloadS3Folder(
60
+ bucket,
61
+ `courses/${courseSlug}/.learn/`,
62
+ path.join(scormOutDir, ".learn")
63
+ );
64
+
65
+ // 4. Read learn.json for course info
66
+ const learnJson = await getCourseMetadata(bucket, courseSlug);
67
+
68
+ // 5. Replace imsmanifest.xml
69
+ const manifestPath = path.join(scormOutDir, "imsmanifest.xml");
70
+ let manifest = fs.readFileSync(manifestPath, "utf-8");
71
+ manifest = manifest.replace(/{{organization_name}}/g, "FourGeeksAcademy");
72
+ manifest = manifest.replace(
73
+ /{{course_title}}/g,
74
+ learnJson.title?.en || learnJson.title || courseSlug
75
+ );
76
+
77
+ // Generate exercises list
78
+ const exercisesList = getFilesList(
79
+ path.join(scormOutDir, "exercises"),
80
+ "exercises"
81
+ ).join("\n ");
82
+ manifest = manifest.replace(/{{exercises_list}}/g, exercisesList);
83
+
84
+ // Generate images list (optional, if you have assets)
85
+ let imagesList = "";
86
+ const assetsDir = path.join(scormOutDir, ".learn", "assets");
87
+ if (fs.existsSync(assetsDir)) {
88
+ imagesList = getFilesList(assetsDir, ".learn/assets").join("\n ");
89
+ }
90
+ manifest = manifest.replace(/{{images_list}}/g, imagesList);
91
+
92
+ fs.writeFileSync(manifestPath, manifest, "utf-8");
93
+
94
+ // 6. Replace title in config/index.html
95
+ const configIndexPath = path.join(scormOutDir, "config", "index.html");
96
+ let indexHtml = fs.readFileSync(configIndexPath, "utf-8");
97
+ indexHtml = indexHtml.replace(
98
+ /{{title}}/g,
99
+ learnJson.title?.en || learnJson.title || courseSlug
100
+ );
101
+ fs.writeFileSync(configIndexPath, indexHtml, "utf-8");
102
+
103
+ // 7. Compress as ZIP
104
+ const zipPath = `${scormOutDir}.zip`;
105
+ await new Promise((resolve: any, reject: any) => {
106
+ const output = fs.createWriteStream(zipPath);
107
+ const archive = archiver("zip", { zlib: { level: 9 } });
108
+
109
+ output.on("close", resolve);
110
+ archive.on("error", reject);
111
+
112
+ archive.pipe(output);
113
+ archive.directory(scormOutDir, false);
114
+ archive.finalize();
115
+ });
116
+
117
+ // Clean up temporary directory
118
+ rimraf.sync(scormOutDir);
119
+
120
+ return zipPath;
121
+ }
@@ -1,61 +1,61 @@
1
- import * as path from "path";
2
- import * as fs from "fs";
3
- import * as mkdirp from "mkdirp";
4
-
5
- // Utility function to copy directory recursively
6
- export function copyDir(src: string, dest: string) {
7
- if (!fs.existsSync(dest)) mkdirp.sync(dest);
8
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
9
- const srcPath = path.join(src, entry.name);
10
- const destPath = path.join(dest, entry.name);
11
- if (entry.isDirectory()) {
12
- copyDir(srcPath, destPath);
13
- } else {
14
- fs.copyFileSync(srcPath, destPath);
15
- }
16
- }
17
- }
18
-
19
- // Download all files from a GCS Bucket prefix to a local folder
20
- export async function downloadS3Folder(
21
- bucket: any,
22
- prefix: string,
23
- localPath: string
24
- ) {
25
- const [files] = await bucket.getFiles({ prefix });
26
- for (const file of files) {
27
- const relPath = file.name.replace(prefix, "");
28
- if (!relPath) continue;
29
- const outPath = path.join(localPath, relPath);
30
- mkdirp.sync(path.dirname(outPath));
31
- // esling-disable-next-line
32
- const [buf] = await file.download();
33
- fs.writeFileSync(outPath, buf);
34
- }
35
- }
36
-
37
- // Generate file list for manifest, given a folder
38
- export function getFilesList(folder: string, base: string) {
39
- let list: string[] = [];
40
- const files = fs.readdirSync(folder, { withFileTypes: true });
41
- for (const file of files) {
42
- const filePath = path.join(folder, file.name);
43
- if (file.isDirectory()) {
44
- // esling-disable-next-line
45
- list = list.concat(getFilesList(filePath, path.join(base, file.name)));
46
- } else {
47
- list.push(
48
- `<file href="${path.join(base, file.name).replace(/\\/g, "/")}" />`
49
- );
50
- }
51
- }
52
- return list;
53
- }
54
-
55
- // Read course metadata from learn.json
56
- export async function getCourseMetadata(bucket: any, courseSlug: string) {
57
- const [learnBuf] = await bucket
58
- .file(`courses/${courseSlug}/learn.json`)
59
- .download();
60
- return JSON.parse(learnBuf.toString());
61
- }
1
+ import * as path from "path";
2
+ import * as fs from "fs";
3
+ import * as mkdirp from "mkdirp";
4
+
5
+ // Utility function to copy directory recursively
6
+ export function copyDir(src: string, dest: string) {
7
+ if (!fs.existsSync(dest)) mkdirp.sync(dest);
8
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
9
+ const srcPath = path.join(src, entry.name);
10
+ const destPath = path.join(dest, entry.name);
11
+ if (entry.isDirectory()) {
12
+ copyDir(srcPath, destPath);
13
+ } else {
14
+ fs.copyFileSync(srcPath, destPath);
15
+ }
16
+ }
17
+ }
18
+
19
+ // Download all files from a GCS Bucket prefix to a local folder
20
+ export async function downloadS3Folder(
21
+ bucket: any,
22
+ prefix: string,
23
+ localPath: string
24
+ ) {
25
+ const [files] = await bucket.getFiles({ prefix });
26
+ for (const file of files) {
27
+ const relPath = file.name.replace(prefix, "");
28
+ if (!relPath) continue;
29
+ const outPath = path.join(localPath, relPath);
30
+ mkdirp.sync(path.dirname(outPath));
31
+ // esling-disable-next-line
32
+ const [buf] = await file.download();
33
+ fs.writeFileSync(outPath, buf);
34
+ }
35
+ }
36
+
37
+ // Generate file list for manifest, given a folder
38
+ export function getFilesList(folder: string, base: string) {
39
+ let list: string[] = [];
40
+ const files = fs.readdirSync(folder, { withFileTypes: true });
41
+ for (const file of files) {
42
+ const filePath = path.join(folder, file.name);
43
+ if (file.isDirectory()) {
44
+ // esling-disable-next-line
45
+ list = list.concat(getFilesList(filePath, path.join(base, file.name)));
46
+ } else {
47
+ list.push(
48
+ `<file href="${path.join(base, file.name).replace(/\\/g, "/")}" />`
49
+ );
50
+ }
51
+ }
52
+ return list;
53
+ }
54
+
55
+ // Read course metadata from learn.json
56
+ export async function getCourseMetadata(bucket: any, courseSlug: string) {
57
+ const [learnBuf] = await bucket
58
+ .file(`courses/${courseSlug}/learn.json`)
59
+ .download();
60
+ return JSON.parse(learnBuf.toString());
61
+ }
@@ -1,25 +1,25 @@
1
- export type ExportFormat = "scorm" | "epub" | "zip";
2
-
3
- export interface ExportOptions {
4
- courseSlug: string;
5
- format: ExportFormat;
6
- bucket: any; // Google Cloud Storage Bucket
7
- outDir: string;
8
- language?: string; // Optional language parameter for EPUB export
9
- }
10
-
11
- export interface EpubMetadata {
12
- creator: string;
13
- publisher: string;
14
- title: string;
15
- rights: string;
16
- lang: string;
17
- }
18
-
19
- export interface CourseMetadata {
20
- title: string;
21
- description?: string;
22
- language?: string;
23
- technologies?: string[];
24
- difficulty?: string;
25
- }
1
+ export type ExportFormat = "scorm" | "epub" | "zip";
2
+
3
+ export interface ExportOptions {
4
+ courseSlug: string;
5
+ format: ExportFormat;
6
+ bucket: any; // Google Cloud Storage Bucket
7
+ outDir: string;
8
+ language?: string; // Optional language parameter for EPUB export
9
+ }
10
+
11
+ export interface EpubMetadata {
12
+ creator: string;
13
+ publisher: string;
14
+ title: string;
15
+ rights: string;
16
+ lang: string;
17
+ }
18
+
19
+ export interface CourseMetadata {
20
+ title: string;
21
+ description?: string;
22
+ language?: string;
23
+ technologies?: string[];
24
+ difficulty?: string;
25
+ }
@@ -1,55 +1,55 @@
1
- import * as path from "path";
2
- import * as fs from "fs";
3
- import * as archiver from "archiver";
4
- import * as mkdirp from "mkdirp";
5
- import * as rimraf from "rimraf";
6
- import { v4 as uuidv4 } from "uuid";
7
- import { ExportOptions } from "./types";
8
- import { downloadS3Folder, getCourseMetadata } from "./shared";
9
-
10
- export async function exportToZip(options: ExportOptions): Promise<string> {
11
- const { courseSlug, bucket, outDir } = options;
12
-
13
- // 1. Create temporary folder
14
- const tmpName = uuidv4();
15
- const zipOutDir = path.join(outDir, tmpName);
16
- rimraf.sync(zipOutDir);
17
- mkdirp.sync(zipOutDir);
18
-
19
- // 2. Download all course files from bucket to temporary directory
20
- await downloadS3Folder(bucket, `courses/${courseSlug}/`, zipOutDir);
21
-
22
- // 3. Create ZIP file
23
- const zipName = `${courseSlug}.zip`;
24
- const zipPath = path.join(outDir, zipName);
25
-
26
- // Remove existing zip file if it exists
27
- if (fs.existsSync(zipPath)) {
28
- fs.unlinkSync(zipPath);
29
- }
30
-
31
- const output = fs.createWriteStream(zipPath);
32
- const archive = archiver("zip", { zlib: { level: 9 } });
33
-
34
- return new Promise((resolve, reject) => {
35
- output.on("close", () => {
36
- console.log(`✅ ZIP export completed: ${archive.pointer()} total bytes`);
37
- // Clean up temporary directory
38
- rimraf.sync(zipOutDir);
39
- resolve(zipPath);
40
- });
41
-
42
- archive.on("error", (err) => {
43
- console.error("❌ ZIP creation error:", err);
44
- rimraf.sync(zipOutDir);
45
- reject(err);
46
- });
47
-
48
- archive.pipe(output);
49
-
50
- // Add all files from the temporary directory to the ZIP
51
- archive.directory(zipOutDir, false);
52
-
53
- archive.finalize();
54
- });
55
- }
1
+ import * as path from "path";
2
+ import * as fs from "fs";
3
+ import * as archiver from "archiver";
4
+ import * as mkdirp from "mkdirp";
5
+ import * as rimraf from "rimraf";
6
+ import { v4 as uuidv4 } from "uuid";
7
+ import { ExportOptions } from "./types";
8
+ import { downloadS3Folder, getCourseMetadata } from "./shared";
9
+
10
+ export async function exportToZip(options: ExportOptions): Promise<string> {
11
+ const { courseSlug, bucket, outDir } = options;
12
+
13
+ // 1. Create temporary folder
14
+ const tmpName = uuidv4();
15
+ const zipOutDir = path.join(outDir, tmpName);
16
+ rimraf.sync(zipOutDir);
17
+ mkdirp.sync(zipOutDir);
18
+
19
+ // 2. Download all course files from bucket to temporary directory
20
+ await downloadS3Folder(bucket, `courses/${courseSlug}/`, zipOutDir);
21
+
22
+ // 3. Create ZIP file
23
+ const zipName = `${courseSlug}.zip`;
24
+ const zipPath = path.join(outDir, zipName);
25
+
26
+ // Remove existing zip file if it exists
27
+ if (fs.existsSync(zipPath)) {
28
+ fs.unlinkSync(zipPath);
29
+ }
30
+
31
+ const output = fs.createWriteStream(zipPath);
32
+ const archive = archiver("zip", { zlib: { level: 9 } });
33
+
34
+ return new Promise((resolve, reject) => {
35
+ output.on("close", () => {
36
+ console.log(`✅ ZIP export completed: ${archive.pointer()} total bytes`);
37
+ // Clean up temporary directory
38
+ rimraf.sync(zipOutDir);
39
+ resolve(zipPath);
40
+ });
41
+
42
+ archive.on("error", (err) => {
43
+ console.error("❌ ZIP creation error:", err);
44
+ rimraf.sync(zipOutDir);
45
+ reject(err);
46
+ });
47
+
48
+ archive.pipe(output);
49
+
50
+ // Add all files from the temporary directory to the ZIP
51
+ archive.directory(zipOutDir, false);
52
+
53
+ archive.finalize();
54
+ });
55
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Replaces sequences of 3+ newlines (with optional spaces in between)
3
+ * with a single double newline. Prevents excessive whitespace that can
4
+ * cause translation API token limit issues.
5
+ * @param content - Raw README string
6
+ * @returns Sanitized string
7
+ */
8
+ export function sanitizeReadmeNewlines(content: string): string {
9
+ return content.replace(/(\n\s*){3,}/g, "\n\n")
10
+ }