@learnpack/learnpack 5.0.334 → 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.
- package/bin/run +17 -17
- package/lib/commands/init.js +41 -41
- package/lib/commands/serve.js +645 -129
- package/lib/creatorDist/assets/index-BhqDgBS9.js +8448 -78631
- package/lib/creatorDist/assets/index-CjddKHB_.css +1 -1688
- package/lib/managers/config/exercise.js +2 -14
- package/lib/managers/readmeHistoryService.js +3 -1
- package/lib/managers/server/routes.js +2 -1
- package/lib/utils/configBuilder.js +2 -1
- package/lib/utils/creatorUtilities.js +14 -14
- package/lib/utils/exerciseFileOrder.d.ts +20 -0
- package/lib/utils/exerciseFileOrder.js +49 -0
- package/lib/utils/export/epub.js +26 -26
- package/lib/utils/readmeSanitizer.d.ts +8 -0
- package/lib/utils/readmeSanitizer.js +13 -0
- package/lib/utils/templates/epub/epub.css +146 -146
- package/lib/utils/templates/scorm/config/api.js +175 -175
- package/package.json +1 -1
- package/src/commands/init.ts +655 -655
- package/src/commands/publish.ts +670 -670
- package/src/commands/serve.ts +5853 -5148
- package/src/creator/eslint.config.js +28 -28
- package/src/creator/src/index.css +227 -227
- package/src/creator/src/utils/lib.ts +471 -471
- package/src/creatorDist/assets/index-BhqDgBS9.js +8448 -78631
- package/src/creatorDist/assets/index-CjddKHB_.css +1 -1688
- package/src/managers/config/exercise.ts +3 -15
- package/src/managers/readmeHistoryService.ts +3 -1
- package/src/managers/server/routes.ts +15 -6
- package/src/managers/session.ts +184 -184
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +1950 -1878
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/api.ts +675 -675
- package/src/utils/configBuilder.ts +102 -100
- package/src/utils/creatorUtilities.ts +536 -536
- package/src/utils/errors.ts +108 -108
- package/src/utils/exerciseFileOrder.ts +50 -0
- package/src/utils/export/epub.ts +553 -553
- package/src/utils/export/index.ts +4 -4
- package/src/utils/export/scorm.ts +121 -121
- package/src/utils/export/shared.ts +61 -61
- package/src/utils/export/types.ts +25 -25
- package/src/utils/export/zip.ts +55 -55
- package/src/utils/readmeSanitizer.ts +10 -0
- package/src/utils/rigoActions.ts +642 -642
- package/src/utils/templates/epub/epub.css +146 -146
- 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
|
+
}
|
package/src/utils/export/zip.ts
CHANGED
|
@@ -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
|
+
}
|