@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.
- package/lib/commands/serve.js +52 -0
- package/lib/utils/export/epub.d.ts +2 -0
- package/lib/utils/export/epub.js +298 -0
- package/lib/utils/export/index.d.ts +3 -0
- package/lib/utils/export/index.js +7 -0
- package/lib/utils/export/scorm.d.ts +2 -0
- package/lib/utils/export/scorm.js +84 -0
- package/lib/utils/export/shared.d.ts +4 -0
- package/lib/utils/export/shared.js +61 -0
- package/lib/utils/export/types.d.ts +15 -0
- package/lib/utils/export/types.js +2 -0
- package/package.json +2 -1
- package/src/commands/serve.ts +61 -1
- package/src/creator/eslint.config.js +28 -28
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +355 -355
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/export/README.md +178 -0
- package/src/utils/export/epub.ts +400 -0
- package/src/utils/export/index.ts +3 -0
- package/src/utils/export/scorm.ts +121 -0
- package/src/utils/export/shared.ts +61 -0
- package/src/utils/export/types.ts +17 -0
- package/src/utils/templates/epub/epub.css +133 -0
- package/src/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -0
- package/src/utils/templates/scorm/config/api.js +175 -0
- package/src/utils/templates/scorm/config/index.html +210 -0
- package/src/utils/templates/scorm/ims_xml.xsd +1 -0
- package/src/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -0
- package/src/utils/templates/scorm/imsmanifest.xml +38 -0
- package/src/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -0
package/lib/commands/serve.js
CHANGED
@@ -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,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,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,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
|
+
}
|
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.
|
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",
|