@learnpack/learnpack 5.0.276 → 5.0.277

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.
@@ -20,6 +20,7 @@ const file_1 = require("../managers/file");
20
20
  const fs = require("fs");
21
21
  const rigoActions_1 = require("../utils/rigoActions");
22
22
  const dotenv = require("dotenv");
23
+ // import { v4 as uuidv4 } from "uuid"
23
24
  const creatorUtilities_1 = require("../utils/creatorUtilities");
24
25
  // import { handleAssetCreation } from "./publish"
25
26
  const axios_1 = require("axios");
@@ -30,8 +31,11 @@ const configBuilder_1 = require("../utils/configBuilder");
30
31
  const creatorUtilities_2 = require("../utils/creatorUtilities");
31
32
  const sidebarGenerator_1 = require("../utils/sidebarGenerator");
32
33
  const publish_1 = require("./publish");
34
+ const export_1 = require("../utils/export");
33
35
  const frontMatter = require("front-matter");
34
36
  dotenv.config();
37
+ // Asegúrate de tener uuid instalado
38
+ // npm install uuid
35
39
  function findLast(array, predicate) {
36
40
  for (let i = array.length - 1; i >= 0; i--) {
37
41
  if (predicate(array[i]))
@@ -643,6 +647,7 @@ class ServeCommand extends SessionCommand_1.default {
643
647
  const { config, exercises } = await (0, configBuilder_1.buildConfig)(bucket, courseSlug);
644
648
  res.set("X-Creator-Web", "true");
645
649
  res.set("Access-Control-Expose-Headers", "X-Creator-Web");
650
+ await uploadFileToBucket(bucket, JSON.stringify({ config, exercises }), `courses/${courseSlug}/.learn/config.json`);
646
651
  res.json({ config, exercises });
647
652
  }
648
653
  catch (error) {
@@ -1298,6 +1303,53 @@ class ServeCommand extends SessionCommand_1.default {
1298
1303
  res.status(500).json({ error: "Failed to fetch the resource" });
1299
1304
  }
1300
1305
  });
1306
+ app.get("/export/:course_slug/:format", async (req, res) => {
1307
+ const { course_slug, format } = req.params;
1308
+ const { language } = req.query;
1309
+ try {
1310
+ let outputPath;
1311
+ let filename;
1312
+ if (format === "scorm") {
1313
+ outputPath = await (0, export_1.exportToScorm)({
1314
+ courseSlug: course_slug,
1315
+ format: "scorm",
1316
+ bucket,
1317
+ outDir: path.join(__dirname, "../output/directory"),
1318
+ });
1319
+ filename = `${course_slug}-scorm.zip`;
1320
+ }
1321
+ else if (format === "epub") {
1322
+ outputPath = await (0, export_1.exportToEpub)({
1323
+ courseSlug: course_slug,
1324
+ format: "epub",
1325
+ bucket,
1326
+ outDir: path.join(__dirname, "../output/directory"),
1327
+ language: language || "en",
1328
+ });
1329
+ filename = `${course_slug}.epub`;
1330
+ }
1331
+ else {
1332
+ return res
1333
+ .status(400)
1334
+ .json({ error: "Invalid format. Supported formats: scorm, epub" });
1335
+ }
1336
+ // Send the file and clean up
1337
+ res.download(outputPath, filename, err => {
1338
+ if (err)
1339
+ console.error("Error sending file:", err);
1340
+ // Clean up the generated file
1341
+ if (fs.existsSync(outputPath)) {
1342
+ fs.unlinkSync(outputPath);
1343
+ }
1344
+ });
1345
+ }
1346
+ catch (error) {
1347
+ console.error("Export error:", error);
1348
+ res
1349
+ .status(500)
1350
+ .json({ error: "Export failed", details: error.message });
1351
+ }
1352
+ });
1301
1353
  server.listen(PORT, () => {
1302
1354
  console.log(`🚀 Creator UI server running at http://localhost:${PORT}/creator`);
1303
1355
  });
@@ -0,0 +1,2 @@
1
+ import { ExportOptions } from "./types";
2
+ export declare function exportToEpub(options: ExportOptions): Promise<string>;
@@ -0,0 +1,298 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.exportToEpub = exportToEpub;
4
+ const path = require("path");
5
+ const fs = require("fs");
6
+ const mkdirp = require("mkdirp");
7
+ const rimraf = require("rimraf");
8
+ const uuid_1 = require("uuid");
9
+ const child_process_1 = require("child_process");
10
+ const shared_1 = require("./shared");
11
+ // Convert markdown to HTML using pandoc
12
+ async function convertMarkdownToHtml(markdownPath, outputPath) {
13
+ return new Promise((resolve, reject) => {
14
+ let stdout = "";
15
+ let stderr = "";
16
+ const pandocArgs = [
17
+ markdownPath,
18
+ "-o",
19
+ outputPath,
20
+ "--from",
21
+ "markdown",
22
+ "--to",
23
+ "html",
24
+ "--standalone",
25
+ "--css",
26
+ path.join(__dirname, "../templates/epub/epub.css"),
27
+ ];
28
+ console.log("Executing pandoc command:", "pandoc", pandocArgs.join(" "));
29
+ console.log("Input file exists:", fs.existsSync(markdownPath));
30
+ console.log("Output directory exists:", fs.existsSync(path.dirname(outputPath)));
31
+ const pandoc = (0, child_process_1.spawn)("pandoc", pandocArgs);
32
+ pandoc.stdout.on("data", (data) => {
33
+ stdout += data.toString();
34
+ });
35
+ pandoc.stderr.on("data", (data) => {
36
+ stderr += data.toString();
37
+ });
38
+ pandoc.on("close", (code) => {
39
+ if (code === 0) {
40
+ resolve();
41
+ }
42
+ else {
43
+ const errorMessage = `Pandoc process exited with code ${code}\n\nSTDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`;
44
+ console.error("Pandoc Error Details:", errorMessage);
45
+ reject(new Error(errorMessage));
46
+ }
47
+ });
48
+ pandoc.on("error", (err) => {
49
+ console.error("Pandoc spawn error:", err);
50
+ reject(err);
51
+ });
52
+ });
53
+ }
54
+ // Generate EPUB using pandoc
55
+ async function generateEpub(htmlFiles, outputPath, metadata) {
56
+ return new Promise((resolve, reject) => {
57
+ var _a;
58
+ let stdout = "";
59
+ let stderr = "";
60
+ // Include all HTML files in the EPUB
61
+ const pandocArgs = [
62
+ ...htmlFiles, // All HTML files
63
+ "-o",
64
+ outputPath,
65
+ "--from",
66
+ "html",
67
+ "--to",
68
+ "epub",
69
+ "--metadata",
70
+ `title=${metadata.title}`,
71
+ "--metadata",
72
+ `language=${metadata.language || "en"}`,
73
+ "--metadata",
74
+ `creator=FourGeeksAcademy`,
75
+ "--metadata",
76
+ `description=${metadata.description || ""}`,
77
+ "--metadata",
78
+ `subject=${((_a = metadata.technologies) === null || _a === void 0 ? void 0 : _a.join(", ")) || "Programming"}`,
79
+ "--css",
80
+ path.join(__dirname, "../templates/epub/epub.css"),
81
+ ];
82
+ console.log("Executing pandoc EPUB command:", "pandoc", pandocArgs.join(" "));
83
+ console.log("HTML files to include:", htmlFiles);
84
+ console.log("All input files exist:", htmlFiles.every((file) => fs.existsSync(file)));
85
+ console.log("Output directory exists:", fs.existsSync(path.dirname(outputPath)));
86
+ const pandoc = (0, child_process_1.spawn)("pandoc", pandocArgs);
87
+ pandoc.stdout.on("data", (data) => {
88
+ stdout += data.toString();
89
+ });
90
+ pandoc.stderr.on("data", (data) => {
91
+ stderr += data.toString();
92
+ });
93
+ pandoc.on("close", (code) => {
94
+ if (code === 0) {
95
+ resolve();
96
+ }
97
+ else {
98
+ const errorMessage = `Pandoc process exited with code ${code}\n\nSTDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`;
99
+ console.error("Pandoc Error Details:", errorMessage);
100
+ reject(new Error(errorMessage));
101
+ }
102
+ });
103
+ pandoc.on("error", (err) => {
104
+ console.error("Pandoc spawn error:", err);
105
+ reject(err);
106
+ });
107
+ });
108
+ }
109
+ // Process exercises and convert to HTML
110
+ async function processExercises(exercisesDir, outputDir, language = "en") {
111
+ const htmlFiles = [];
112
+ console.log("Processing exercises directory:", exercisesDir);
113
+ console.log("Output directory:", outputDir);
114
+ console.log("Language:", language);
115
+ if (!fs.existsSync(exercisesDir)) {
116
+ console.log("Exercises directory does not exist:", exercisesDir);
117
+ return htmlFiles;
118
+ }
119
+ console.log("Exercises directory exists, scanning for files...");
120
+ const processDirectory = async (dir, relativePath = "") => {
121
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
122
+ // Sort entries to prioritize README files first
123
+ entries.sort((a, b) => {
124
+ const aName = a.name.toLowerCase();
125
+ const bName = b.name.toLowerCase();
126
+ // Prioritize README files
127
+ if (aName === "readme.md" && bName !== "readme.md")
128
+ return -1;
129
+ if (bName === "readme.md" && aName !== "readme.md")
130
+ return 1;
131
+ return aName.localeCompare(bName);
132
+ });
133
+ for (const entry of entries) {
134
+ const fullPath = path.join(dir, entry.name);
135
+ const relativeFilePath = path.join(relativePath, entry.name);
136
+ if (entry.isDirectory()) {
137
+ console.log("Processing subdirectory:", relativeFilePath);
138
+ // esling-disable-next-line
139
+ await processDirectory(fullPath, relativeFilePath);
140
+ }
141
+ else if (entry.name.endsWith(".md") ||
142
+ entry.name.endsWith(".markdown")) {
143
+ // Apply language filtering for all markdown files
144
+ const baseName = entry.name.replace(/\.(md|markdown)$/, "");
145
+ if (language === "en") {
146
+ // For English: exclude files with language suffixes
147
+ if (baseName.includes(".") &&
148
+ /\.(es|fr|de|it|pt|ja|ko|zh|ar|ru)$/i.test(baseName)) {
149
+ console.log("Skipping language-specific file for English:", relativeFilePath);
150
+ continue;
151
+ }
152
+ }
153
+ else {
154
+ // For other languages: only include files with the correct language suffix
155
+ if (!baseName.endsWith(`.${language}`)) {
156
+ console.log("Skipping file with different language:", relativeFilePath);
157
+ continue;
158
+ }
159
+ }
160
+ // Convert markdown to HTML
161
+ console.log("Converting markdown file:", relativeFilePath);
162
+ const htmlFileName = entry.name.replace(/\.(md|markdown)$/, ".html");
163
+ const htmlPath = path.join(outputDir, relativeFilePath.replace(/\.(md|markdown)$/, ".html"));
164
+ console.log("HTML output path:", htmlPath);
165
+ mkdirp.sync(path.dirname(htmlPath));
166
+ // esling-disable-next-line
167
+ await convertMarkdownToHtml(fullPath, htmlPath);
168
+ htmlFiles.push(htmlPath);
169
+ console.log("Successfully converted:", relativeFilePath, "->", htmlPath);
170
+ }
171
+ else if (entry.name.endsWith(".html")) {
172
+ // Copy HTML files as-is
173
+ console.log("Copying HTML file:", relativeFilePath);
174
+ const destPath = path.join(outputDir, relativeFilePath);
175
+ mkdirp.sync(path.dirname(destPath));
176
+ // esling-disable-next-line
177
+ fs.copyFileSync(fullPath, destPath);
178
+ htmlFiles.push(destPath);
179
+ console.log("Successfully copied:", relativeFilePath, "->", destPath);
180
+ }
181
+ else {
182
+ console.log("Skipping file:", relativeFilePath, "(not markdown or HTML)");
183
+ }
184
+ }
185
+ };
186
+ await processDirectory(exercisesDir);
187
+ return htmlFiles;
188
+ }
189
+ // Create EPUB table of contents
190
+ function createTocHtml(htmlFiles, metadata) {
191
+ let tocContent = `
192
+ <!DOCTYPE html>
193
+ <html>
194
+ <head>
195
+ <meta charset="UTF-8">
196
+ <title>Table of Contents - ${metadata.title}</title>
197
+ <link rel="stylesheet" href="epub.css">
198
+ </head>
199
+ <body>
200
+ <h1>${metadata.title}</h1>
201
+ <nav id="toc">
202
+ <h2>Table of Contents</h2>
203
+ <ol>
204
+ <li><a href="main.html">Course Overview</a></li>
205
+ `;
206
+ htmlFiles.forEach((file) => {
207
+ const fileName = path.basename(file, ".html");
208
+ const displayName = fileName
209
+ .replace(/[-_]/g, " ")
210
+ .replace(/\b\w/g, (l) => l.toUpperCase());
211
+ // Use the full relative path for the href to ensure proper navigation
212
+ const relativePath = file.replace(/.*\/html\//, "").replace(/\\/g, "/");
213
+ tocContent += ` <li><a href="${relativePath}">${displayName}</a></li>\n`;
214
+ });
215
+ tocContent += `
216
+ </ol>
217
+ </nav>
218
+ </body>
219
+ </html>`;
220
+ return tocContent;
221
+ }
222
+ async function exportToEpub(options) {
223
+ var _a, _b, _c;
224
+ const { courseSlug, bucket, outDir, language = "en" } = options;
225
+ console.log("Starting EPUB export for course:", courseSlug);
226
+ console.log("Output directory:", outDir);
227
+ console.log("Language:", language);
228
+ // 1. Create temporary folder
229
+ const tmpName = (0, uuid_1.v4)();
230
+ const epubOutDir = path.join(outDir, tmpName);
231
+ rimraf.sync(epubOutDir);
232
+ mkdirp.sync(epubOutDir);
233
+ console.log("Created temporary directory:", epubOutDir);
234
+ // 2. Create EPUB template directory
235
+ const epubTemplateDir = path.join(epubOutDir, "template");
236
+ mkdirp.sync(epubTemplateDir);
237
+ // 3. Download exercises and .learn from bucket
238
+ console.log("Downloading exercises...");
239
+ await (0, shared_1.downloadS3Folder)(bucket, `courses/${courseSlug}/exercises/`, path.join(epubOutDir, "exercises"));
240
+ console.log("Downloading .learn files...");
241
+ await (0, shared_1.downloadS3Folder)(bucket, `courses/${courseSlug}/.learn/`, path.join(epubOutDir, ".learn"));
242
+ // 4. Read learn.json for course info
243
+ console.log("Reading course metadata...");
244
+ const learnJson = await (0, shared_1.getCourseMetadata)(bucket, courseSlug);
245
+ const metadata = {
246
+ title: ((_a = learnJson.title) === null || _a === void 0 ? void 0 : _a.en) || learnJson.title || courseSlug,
247
+ description: ((_b = learnJson.description) === null || _b === void 0 ? void 0 : _b.en) || learnJson.description,
248
+ language: learnJson.language || "en",
249
+ technologies: learnJson.technologies || [],
250
+ difficulty: learnJson.difficulty || "beginner",
251
+ };
252
+ console.log("Course metadata:", metadata);
253
+ // 5. Process exercises and convert to HTML
254
+ console.log("Processing exercises and converting to HTML...");
255
+ const htmlFiles = await processExercises(path.join(epubOutDir, "exercises"), path.join(epubOutDir, "html"), language);
256
+ console.log("Generated HTML files:", htmlFiles.length);
257
+ console.log("HTML files:", htmlFiles);
258
+ // 6. Create table of contents
259
+ const tocHtml = createTocHtml(htmlFiles, metadata);
260
+ const tocPath = path.join(epubOutDir, "html", "toc.html");
261
+ mkdirp.sync(path.dirname(tocPath));
262
+ fs.writeFileSync(tocPath, tocHtml, "utf-8");
263
+ // 7. Create main content file
264
+ const mainContent = `
265
+ <!DOCTYPE html>
266
+ <html>
267
+ <head>
268
+ <meta charset="UTF-8">
269
+ <title>${metadata.title}</title>
270
+ <link rel="stylesheet" href="epub.css">
271
+ </head>
272
+ <body>
273
+ <h1>${metadata.title}</h1>
274
+ <p>${metadata.description || ""}</p>
275
+ <p><strong>Technologies:</strong> ${((_c = metadata.technologies) === null || _c === void 0 ? void 0 : _c.join(", ")) || "Programming"}</p>
276
+ <p><strong>Difficulty:</strong> ${metadata.difficulty}</p>
277
+ <p><strong>Language:</strong> ${language}</p>
278
+ <p><a href="toc.html">Go to Table of Contents</a></p>
279
+ </body>
280
+ </html>`;
281
+ const mainContentPath = path.join(epubOutDir, "html", "main.html");
282
+ fs.writeFileSync(mainContentPath, mainContent, "utf-8");
283
+ // 8. Prepare all HTML files for EPUB generation
284
+ const allHtmlFiles = [mainContentPath, tocPath, ...htmlFiles];
285
+ console.log("All HTML files for EPUB:", allHtmlFiles);
286
+ // 9. Generate EPUB using pandoc
287
+ console.log("Generating EPUB file...");
288
+ const epubPath = path.join(epubOutDir, `${courseSlug}.epub`);
289
+ await generateEpub(allHtmlFiles, epubPath, metadata);
290
+ console.log("EPUB generated successfully:", epubPath);
291
+ // Clean up temporary files
292
+ console.log("Cleaning up temporary files...");
293
+ rimraf.sync(path.join(epubOutDir, "html"));
294
+ rimraf.sync(path.join(epubOutDir, "exercises"));
295
+ rimraf.sync(path.join(epubOutDir, ".learn"));
296
+ console.log("EPUB export completed successfully");
297
+ return epubPath;
298
+ }
@@ -0,0 +1,3 @@
1
+ export { exportToScorm } from "./scorm";
2
+ export { exportToEpub } from "./epub";
3
+ export { ExportFormat, ExportOptions } from "./types";
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.exportToEpub = exports.exportToScorm = void 0;
4
+ var scorm_1 = require("./scorm");
5
+ Object.defineProperty(exports, "exportToScorm", { enumerable: true, get: function () { return scorm_1.exportToScorm; } });
6
+ var epub_1 = require("./epub");
7
+ Object.defineProperty(exports, "exportToEpub", { enumerable: true, get: function () { return epub_1.exportToEpub; } });
@@ -0,0 +1,2 @@
1
+ import { ExportOptions } from "./types";
2
+ export declare function exportToScorm(options: ExportOptions): Promise<string>;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.exportToScorm = exportToScorm;
4
+ const path = require("path");
5
+ const fs = require("fs");
6
+ const archiver = require("archiver");
7
+ const mkdirp = require("mkdirp");
8
+ const rimraf = require("rimraf");
9
+ const uuid_1 = require("uuid");
10
+ const shared_1 = require("./shared");
11
+ async function exportToScorm(options) {
12
+ var _a, _b;
13
+ const { courseSlug, bucket, outDir } = options;
14
+ // 1. Create temporary folder
15
+ const tmpName = (0, uuid_1.v4)();
16
+ const scormOutDir = path.join(outDir, tmpName);
17
+ rimraf.sync(scormOutDir);
18
+ mkdirp.sync(scormOutDir);
19
+ // 2. Copy SCORM template
20
+ const scormTpl = path.join(__dirname, "../templates/scorm");
21
+ (0, shared_1.copyDir)(scormTpl, scormOutDir);
22
+ // Paths
23
+ const appDir = path.resolve(__dirname, "../../ui/_app");
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
+ }
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
+ }
42
+ }
43
+ }
44
+ // 3. Download exercises and .learn from bucket
45
+ await (0, shared_1.downloadS3Folder)(bucket, `courses/${courseSlug}/exercises/`, path.join(scormOutDir, "exercises"));
46
+ await (0, shared_1.downloadS3Folder)(bucket, `courses/${courseSlug}/.learn/`, path.join(scormOutDir, ".learn"));
47
+ // 4. Read learn.json for course info
48
+ const learnJson = await (0, shared_1.getCourseMetadata)(bucket, courseSlug);
49
+ // 5. Replace imsmanifest.xml
50
+ const manifestPath = path.join(scormOutDir, "imsmanifest.xml");
51
+ 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);
54
+ // Generate exercises list
55
+ const exercisesList = (0, shared_1.getFilesList)(path.join(scormOutDir, "exercises"), "exercises").join("\n ");
56
+ 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);
64
+ fs.writeFileSync(manifestPath, manifest, "utf-8");
65
+ // 6. Replace title in config/index.html
66
+ const configIndexPath = path.join(scormOutDir, "config", "index.html");
67
+ 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);
69
+ fs.writeFileSync(configIndexPath, indexHtml, "utf-8");
70
+ // 7. Compress as ZIP
71
+ const zipPath = `${scormOutDir}.zip`;
72
+ await new Promise((resolve, reject) => {
73
+ const output = fs.createWriteStream(zipPath);
74
+ const archive = archiver("zip", { zlib: { level: 9 } });
75
+ output.on("close", resolve);
76
+ archive.on("error", reject);
77
+ archive.pipe(output);
78
+ archive.directory(scormOutDir, false);
79
+ archive.finalize();
80
+ });
81
+ // Clean up temporary directory
82
+ rimraf.sync(scormOutDir);
83
+ return zipPath;
84
+ }
@@ -0,0 +1,4 @@
1
+ export declare function copyDir(src: string, dest: string): void;
2
+ export declare function downloadS3Folder(bucket: any, prefix: string, localPath: string): Promise<void>;
3
+ export declare function getFilesList(folder: string, base: string): string[];
4
+ export declare function getCourseMetadata(bucket: any, courseSlug: string): Promise<any>;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.copyDir = copyDir;
4
+ exports.downloadS3Folder = downloadS3Folder;
5
+ exports.getFilesList = getFilesList;
6
+ exports.getCourseMetadata = getCourseMetadata;
7
+ const path = require("path");
8
+ const fs = require("fs");
9
+ const mkdirp = require("mkdirp");
10
+ // Utility function to copy directory recursively
11
+ function copyDir(src, dest) {
12
+ if (!fs.existsSync(dest))
13
+ mkdirp.sync(dest);
14
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
15
+ const srcPath = path.join(src, entry.name);
16
+ const destPath = path.join(dest, entry.name);
17
+ if (entry.isDirectory()) {
18
+ copyDir(srcPath, destPath);
19
+ }
20
+ else {
21
+ fs.copyFileSync(srcPath, destPath);
22
+ }
23
+ }
24
+ }
25
+ // Download all files from a GCS Bucket prefix to a local folder
26
+ async function downloadS3Folder(bucket, prefix, localPath) {
27
+ const [files] = await bucket.getFiles({ prefix });
28
+ for (const file of files) {
29
+ const relPath = file.name.replace(prefix, "");
30
+ if (!relPath)
31
+ continue;
32
+ const outPath = path.join(localPath, relPath);
33
+ mkdirp.sync(path.dirname(outPath));
34
+ // esling-disable-next-line
35
+ const [buf] = await file.download();
36
+ fs.writeFileSync(outPath, buf);
37
+ }
38
+ }
39
+ // Generate file list for manifest, given a folder
40
+ function getFilesList(folder, base) {
41
+ let list = [];
42
+ const files = fs.readdirSync(folder, { withFileTypes: true });
43
+ for (const file of files) {
44
+ const filePath = path.join(folder, file.name);
45
+ if (file.isDirectory()) {
46
+ // esling-disable-next-line
47
+ list = list.concat(getFilesList(filePath, path.join(base, file.name)));
48
+ }
49
+ else {
50
+ list.push(`<file href="${path.join(base, file.name).replace(/\\/g, "/")}" />`);
51
+ }
52
+ }
53
+ return list;
54
+ }
55
+ // Read course metadata from learn.json
56
+ async function getCourseMetadata(bucket, courseSlug) {
57
+ const [learnBuf] = await bucket
58
+ .file(`courses/${courseSlug}/learn.json`)
59
+ .download();
60
+ return JSON.parse(learnBuf.toString());
61
+ }
@@ -0,0 +1,15 @@
1
+ export type ExportFormat = "scorm" | "epub";
2
+ export interface ExportOptions {
3
+ courseSlug: string;
4
+ format: ExportFormat;
5
+ bucket: any;
6
+ outDir: string;
7
+ language?: string;
8
+ }
9
+ export interface CourseMetadata {
10
+ title: string;
11
+ description?: string;
12
+ language?: string;
13
+ technologies?: string[];
14
+ difficulty?: string;
15
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
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.276",
4
+ "version": "5.0.277",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -66,6 +66,7 @@
66
66
  "targz": "^1.0.1",
67
67
  "text-readability": "^1.1.0",
68
68
  "tslib": "^1",
69
+ "uuid": "^11.1.0",
69
70
  "validator": "^13.1.1",
70
71
  "xxhashjs": "^0.2.2",
71
72
  "youtube-transcript": "^1.2.1",