@learnpack/learnpack 5.0.349 → 5.0.351

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.
@@ -33,6 +33,7 @@ const creatorUtilities_2 = require("../utils/creatorUtilities");
33
33
  const sidebarGenerator_1 = require("../utils/sidebarGenerator");
34
34
  const publish_1 = require("./publish");
35
35
  const export_1 = require("../utils/export");
36
+ const packageManifest_1 = require("../utils/packageManifest");
36
37
  const errorHandler_1 = require("../utils/errorHandler");
37
38
  const jsdom_1 = require("jsdom");
38
39
  const redis_1 = require("redis");
@@ -3956,6 +3957,10 @@ class ServeCommand extends SessionCommand_1.default {
3956
3957
  const [buf] = await file.download();
3957
3958
  fs.writeFileSync(out, buf);
3958
3959
  }));
3960
+ await (0, packageManifest_1.generateAndPersistPackageManifest)(bucket, slug, {
3961
+ publishedAt: new Date().toISOString(),
3962
+ localDir: buildRoot,
3963
+ });
3959
3964
  // 8) Crear el config.json en buildRoot con lo que retorna buildConfig
3960
3965
  const fullConfig = { config, exercises };
3961
3966
  fullConfig.config.slug = slug;
@@ -4576,6 +4581,7 @@ class ServeCommand extends SessionCommand_1.default {
4576
4581
  ".learn/config.json",
4577
4582
  ".learn/initialSyllabus.json",
4578
4583
  ".learn/sidebar.json",
4584
+ ".learn/package-manifest.json",
4579
4585
  ".learn/memory_bank.txt",
4580
4586
  ]);
4581
4587
  const isStructuralFile = (relativePath) => GITHUB_STRUCTURAL_FILES.has(relativePath) ||
@@ -8,6 +8,7 @@ const mkdirp = require("mkdirp");
8
8
  const rimraf = require("rimraf");
9
9
  const uuid_1 = require("uuid");
10
10
  const shared_1 = require("./shared");
11
+ const packageManifest_1 = require("../packageManifest");
11
12
  async function exportToScorm(options) {
12
13
  var _a;
13
14
  const { courseSlug, bucket, outDir } = options;
@@ -45,6 +46,10 @@ async function exportToScorm(options) {
45
46
  // 3. Download exercises and .learn from bucket
46
47
  await (0, shared_1.downloadS3Folder)(bucket, `courses/${courseSlug}/exercises/`, path.join(scormOutDir, "exercises"));
47
48
  await (0, shared_1.downloadS3Folder)(bucket, `courses/${courseSlug}/.learn/`, path.join(scormOutDir, ".learn"));
49
+ await (0, packageManifest_1.generateAndPersistPackageManifest)(bucket, courseSlug, {
50
+ publishedAt: null,
51
+ localDir: scormOutDir,
52
+ });
48
53
  // 4. Read learn.json for course info
49
54
  const learnJson = await (0, shared_1.getCourseMetadata)(bucket, courseSlug);
50
55
  // Resolve the course title once, escaped for safe XML/HTML embedding
@@ -8,6 +8,7 @@ const mkdirp = require("mkdirp");
8
8
  const rimraf = require("rimraf");
9
9
  const uuid_1 = require("uuid");
10
10
  const shared_1 = require("./shared");
11
+ const packageManifest_1 = require("../packageManifest");
11
12
  async function exportToZip(options) {
12
13
  const { courseSlug, bucket, outDir } = options;
13
14
  // 1. Create temporary folder
@@ -15,6 +16,9 @@ async function exportToZip(options) {
15
16
  const zipOutDir = path.join(outDir, tmpName);
16
17
  rimraf.sync(zipOutDir);
17
18
  mkdirp.sync(zipOutDir);
19
+ await (0, packageManifest_1.generateAndPersistPackageManifest)(bucket, courseSlug, {
20
+ publishedAt: null,
21
+ });
18
22
  // 2. Download all course files from bucket to temporary directory
19
23
  await (0, shared_1.downloadS3Folder)(bucket, `courses/${courseSlug}/`, zipOutDir);
20
24
  // 3. Create ZIP file
@@ -0,0 +1,54 @@
1
+ import { Bucket } from "@google-cloud/storage";
2
+ import { Syllabus } from "../models/creator";
3
+ import { Exercise } from "./configBuilder";
4
+ export declare const PACKAGE_MANIFEST_FILENAME = "package-manifest.json";
5
+ export declare const PACKAGE_MANIFEST_REL_PATH = ".learn/package-manifest.json";
6
+ export declare const SCHEMA_VERSION = 1;
7
+ export type LessonType = "READ" | "CODE" | "QUIZ";
8
+ export type PackageManifestLesson = {
9
+ id: string;
10
+ slug: string;
11
+ position: number;
12
+ type: LessonType;
13
+ title: Record<string, string>;
14
+ video: Record<string, string | null>;
15
+ };
16
+ export type PackageManifest = {
17
+ schemaVersion: number;
18
+ generatedAt: string;
19
+ publishedAt: string | null;
20
+ slug: string;
21
+ title: Record<string, string>;
22
+ description: Record<string, string>;
23
+ preview: string;
24
+ technologies?: string[];
25
+ difficulty?: string;
26
+ duration?: number;
27
+ lessons: PackageManifestLesson[];
28
+ };
29
+ export type ReadmeFrontmatter = {
30
+ tutorial?: string;
31
+ intro?: string;
32
+ title?: string;
33
+ };
34
+ export type PackageManifestBuildInput = {
35
+ learnJson: Record<string, unknown>;
36
+ syllabus: Syllabus | null;
37
+ sidebar: Record<string, Record<string, string>>;
38
+ exercises: Exercise[];
39
+ readmeFrontmatters: Record<string, Record<string, ReadmeFrontmatter>>;
40
+ options: {
41
+ slug: string;
42
+ generatedAt: string;
43
+ publishedAt: string | null;
44
+ };
45
+ };
46
+ export declare function packageManifestGcsPath(courseSlug: string): string;
47
+ export declare function resolvePreviewUrl(slug: string, preview?: string): string;
48
+ export declare function extractReadmeH1(body: string): string | null;
49
+ export declare function normalizeReadmeTitle(raw: string): string | null;
50
+ export declare function buildPackageManifestFromSources(input: PackageManifestBuildInput): PackageManifest;
51
+ export declare function generateAndPersistPackageManifest(bucket: Bucket, courseSlug: string, options: {
52
+ publishedAt: string | null;
53
+ localDir?: string;
54
+ }): Promise<PackageManifest>;
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SCHEMA_VERSION = exports.PACKAGE_MANIFEST_REL_PATH = exports.PACKAGE_MANIFEST_FILENAME = void 0;
4
+ exports.packageManifestGcsPath = packageManifestGcsPath;
5
+ exports.resolvePreviewUrl = resolvePreviewUrl;
6
+ exports.extractReadmeH1 = extractReadmeH1;
7
+ exports.normalizeReadmeTitle = normalizeReadmeTitle;
8
+ exports.buildPackageManifestFromSources = buildPackageManifestFromSources;
9
+ exports.generateAndPersistPackageManifest = generateAndPersistPackageManifest;
10
+ const fs = require("fs");
11
+ const mkdirp = require("mkdirp");
12
+ const path = require("path");
13
+ const configBuilder_1 = require("./configBuilder");
14
+ const creatorUtilities_1 = require("./creatorUtilities");
15
+ const titlefy_1 = require("./titlefy");
16
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
17
+ const frontMatter = require("front-matter");
18
+ exports.PACKAGE_MANIFEST_FILENAME = "package-manifest.json";
19
+ exports.PACKAGE_MANIFEST_REL_PATH = ".learn/package-manifest.json";
20
+ exports.SCHEMA_VERSION = 1;
21
+ const VALID_LESSON_TYPES = new Set(["READ", "CODE", "QUIZ"]);
22
+ function packageManifestGcsPath(courseSlug) {
23
+ return `courses/${courseSlug}/${exports.PACKAGE_MANIFEST_REL_PATH}`;
24
+ }
25
+ function resolvePreviewUrl(slug, preview) {
26
+ if (preview && preview.startsWith("http")) {
27
+ return preview;
28
+ }
29
+ return `https://${slug}.learn-pack.com/preview.png`;
30
+ }
31
+ function findSyllabusLesson(exerciseSlug, lessons) {
32
+ return lessons.find(lesson => {
33
+ const candidates = [
34
+ (0, creatorUtilities_1.slugify)(lesson.id + "-" + lesson.title),
35
+ lesson.uid,
36
+ ].filter(Boolean);
37
+ return candidates.includes(exerciseSlug);
38
+ });
39
+ }
40
+ function deriveLessonId(exerciseSlug) {
41
+ const match = exerciseSlug.match(/^(\d+(?:\.\d+)?)/);
42
+ return match ? match[1] : exerciseSlug;
43
+ }
44
+ function coerceLessonType(type) {
45
+ if (type && VALID_LESSON_TYPES.has(type)) {
46
+ return type;
47
+ }
48
+ if (type) {
49
+ console.warn(`[package-manifest] Invalid lesson type "${type}", defaulting to READ`);
50
+ }
51
+ return "READ";
52
+ }
53
+ function extractReadmeH1(body) {
54
+ const match = body.match(/^#\s+(.+)$/m);
55
+ if (!match) {
56
+ return null;
57
+ }
58
+ const title = match[1].replace(/\r/g, "").trim();
59
+ return title || null;
60
+ }
61
+ function normalizeReadmeTitle(raw) {
62
+ const normalized = raw
63
+ .trim()
64
+ .replace(/^\d+(?:\.\d+)?\s*[.:-]?\s*/, "")
65
+ .trim();
66
+ return normalized || null;
67
+ }
68
+ function firstNonEmpty(...candidates) {
69
+ for (const candidate of candidates) {
70
+ const value = candidate === null || candidate === void 0 ? void 0 : candidate.trim();
71
+ if (value) {
72
+ return value;
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ function resolveLessonTitle(exercise, sidebar, syllabusLesson, courseLang, readmeByLang) {
78
+ var _a, _b;
79
+ const sidebarEntry = (_a = sidebar[exercise.slug]) !== null && _a !== void 0 ? _a : {};
80
+ const languages = new Set([
81
+ ...Object.keys(exercise.translations),
82
+ ...Object.keys(sidebarEntry),
83
+ ]);
84
+ const titles = {};
85
+ for (const lang of languages) {
86
+ const title = firstNonEmpty((_b = readmeByLang[lang]) === null || _b === void 0 ? void 0 : _b.title, sidebarEntry[lang] ? (0, titlefy_1.formatSidebarTitle)(sidebarEntry[lang]) : null, lang === courseLang ? syllabusLesson === null || syllabusLesson === void 0 ? void 0 : syllabusLesson.title : null);
87
+ if (title) {
88
+ titles[lang] = title;
89
+ }
90
+ }
91
+ return titles;
92
+ }
93
+ function resolveLessonVideos(exercise, readmeFrontmatters) {
94
+ const video = {};
95
+ const frontmatters = readmeFrontmatters[exercise.slug] || {};
96
+ for (const lang of Object.keys(exercise.translations)) {
97
+ const attrs = frontmatters[lang] || {};
98
+ const url = attrs.tutorial || attrs.intro || null;
99
+ video[lang] = url && url.trim().length > 0 ? url.trim() : null;
100
+ }
101
+ return video;
102
+ }
103
+ function buildPackageManifestFromSources(input) {
104
+ var _a, _b;
105
+ const { learnJson, syllabus, sidebar, exercises, readmeFrontmatters, options, } = input;
106
+ const syllabusLessons = (_a = syllabus === null || syllabus === void 0 ? void 0 : syllabus.lessons) !== null && _a !== void 0 ? _a : [];
107
+ const courseLang = ((_b = syllabus === null || syllabus === void 0 ? void 0 : syllabus.courseInfo) === null || _b === void 0 ? void 0 : _b.language) || "en";
108
+ const sortedExercises = [...exercises].sort((a, b) => a.position - b.position);
109
+ const lessons = sortedExercises.map(exercise => {
110
+ var _a, _b;
111
+ const syllabusLesson = findSyllabusLesson(exercise.slug, syllabusLessons);
112
+ const id = (_a = syllabusLesson === null || syllabusLesson === void 0 ? void 0 : syllabusLesson.id) !== null && _a !== void 0 ? _a : deriveLessonId(exercise.slug);
113
+ const type = syllabusLesson ?
114
+ coerceLessonType(syllabusLesson.type) :
115
+ "READ";
116
+ if (!syllabusLesson) {
117
+ console.warn(`[package-manifest] No syllabus match for exercise "${exercise.slug}", using derived id "${id}"`);
118
+ }
119
+ return {
120
+ id,
121
+ slug: exercise.slug,
122
+ position: exercise.position,
123
+ type,
124
+ title: resolveLessonTitle(exercise, sidebar, syllabusLesson, courseLang, (_b = readmeFrontmatters[exercise.slug]) !== null && _b !== void 0 ? _b : {}),
125
+ video: resolveLessonVideos(exercise, readmeFrontmatters),
126
+ };
127
+ });
128
+ const title = learnJson.title || {};
129
+ const description = learnJson.description || {};
130
+ return {
131
+ schemaVersion: exports.SCHEMA_VERSION,
132
+ generatedAt: options.generatedAt,
133
+ publishedAt: options.publishedAt,
134
+ slug: options.slug,
135
+ title,
136
+ description,
137
+ preview: resolvePreviewUrl(options.slug, learnJson.preview),
138
+ technologies: learnJson.technologies,
139
+ difficulty: learnJson.difficulty,
140
+ duration: learnJson.duration,
141
+ lessons,
142
+ };
143
+ }
144
+ async function downloadJsonFile(bucket, filePath) {
145
+ try {
146
+ const [buf] = await bucket.file(filePath).download();
147
+ return JSON.parse(buf.toString());
148
+ }
149
+ catch (_a) {
150
+ return null;
151
+ }
152
+ }
153
+ async function readReadmeFrontmatter(bucket, exerciseSlug, lang, readmePath) {
154
+ try {
155
+ const [buf] = await bucket.file(readmePath).download();
156
+ const parsed = frontMatter(buf.toString());
157
+ const attributes = parsed.attributes || {};
158
+ const h1 = extractReadmeH1(parsed.body || "");
159
+ const title = h1 ? normalizeReadmeTitle(h1) : null;
160
+ return {
161
+ exerciseSlug,
162
+ lang,
163
+ frontmatter: Object.assign({ tutorial: attributes.tutorial, intro: attributes.intro }, (title ? { title } : {})),
164
+ };
165
+ }
166
+ catch (error) {
167
+ console.warn(`[package-manifest] Could not read README for ${exerciseSlug} (${lang}):`, error.message);
168
+ return { exerciseSlug, lang, frontmatter: {} };
169
+ }
170
+ }
171
+ async function collectReadmeFrontmatters(bucket, courseSlug, exercises) {
172
+ const readmeTasks = exercises.flatMap(exercise => Object.keys(exercise.translations).map(lang => {
173
+ const readmeFilename = `README${(0, creatorUtilities_1.getReadmeExtension)(lang)}`;
174
+ const readmePath = `courses/${courseSlug}/exercises/${exercise.slug}/${readmeFilename}`;
175
+ return readReadmeFrontmatter(bucket, exercise.slug, lang, readmePath);
176
+ }));
177
+ const readmeEntries = await Promise.all(readmeTasks);
178
+ const result = {};
179
+ for (const { exerciseSlug, lang, frontmatter } of readmeEntries) {
180
+ if (!result[exerciseSlug]) {
181
+ result[exerciseSlug] = {};
182
+ }
183
+ result[exerciseSlug][lang] = frontmatter;
184
+ }
185
+ return result;
186
+ }
187
+ function writeManifestJson(manifest, localDir) {
188
+ const json = JSON.stringify(manifest, null, 2);
189
+ if (localDir) {
190
+ const learnDir = path.join(localDir, ".learn");
191
+ mkdirp.sync(learnDir);
192
+ const localPath = path.join(learnDir, exports.PACKAGE_MANIFEST_FILENAME);
193
+ fs.writeFileSync(localPath, json, "utf-8");
194
+ }
195
+ return json;
196
+ }
197
+ async function generateAndPersistPackageManifest(bucket, courseSlug, options) {
198
+ const generatedAt = new Date().toISOString();
199
+ const { config: learnJson, exercises } = await (0, configBuilder_1.buildConfig)(bucket, courseSlug);
200
+ const syllabus = await downloadJsonFile(bucket, `courses/${courseSlug}/.learn/initialSyllabus.json`);
201
+ const sidebar = (await downloadJsonFile(bucket, `courses/${courseSlug}/.learn/sidebar.json`)) || {};
202
+ const readmeFrontmatters = await collectReadmeFrontmatters(bucket, courseSlug, exercises);
203
+ const manifest = buildPackageManifestFromSources({
204
+ learnJson,
205
+ syllabus,
206
+ sidebar,
207
+ exercises,
208
+ readmeFrontmatters,
209
+ options: {
210
+ slug: courseSlug,
211
+ generatedAt,
212
+ publishedAt: options.publishedAt,
213
+ },
214
+ });
215
+ const json = writeManifestJson(manifest, options.localDir);
216
+ await bucket
217
+ .file(packageManifestGcsPath(courseSlug))
218
+ .save(Buffer.from(json, "utf8"));
219
+ return manifest;
220
+ }
@@ -9,43 +9,22 @@
9
9
  <script type="text/javascript">
10
10
 
11
11
 
12
- var currentPage = null;
13
12
  var startTimeStamp = null;
14
13
  var processedUnload = false;
15
14
  var reachedEnd = false;
16
- var language = 'es';
17
15
 
18
16
  function doStart() {
19
17
  startTimeStamp = new Date();
20
18
 
21
19
  ScormProcessInitialize();
22
20
 
21
+ // Mark the attempt as started as an early fallback. The IDE's
22
+ // ScormReporter re-asserts this (with a commit) and reports
23
+ // per-step progress/score once it mounts.
23
24
  var completionStatus = ScormProcessGetValue('cmi.core.lesson_status');
24
25
  if (completionStatus == 'not attempted') {
25
26
  ScormProcessSetValue('cmi.core.lesson_status', 'incomplete');
26
27
  }
27
-
28
- var bookmark = ScormProcessGetValue('cmi.core.lesson_location');
29
-
30
- if (bookmark == '') {
31
- currentPage = 0;
32
- } else {
33
- if (
34
- confirm(
35
- 'Would you like to resume from where you previously left off?'
36
- )
37
- ) {
38
- currentPage = parseInt(bookmark, 10);
39
- } else {
40
- currentPage = 0;
41
- }
42
- }
43
-
44
- goToPage();
45
- }
46
-
47
- function goToPage() {
48
- ScormProcessSetValue('cmi.core.lesson_location', currentPage);
49
28
  }
50
29
 
51
30
  function doUnload(pressedExit) {
@@ -72,20 +51,6 @@
72
51
  ScormProcessFinish();
73
52
  }
74
53
 
75
- function doPrevious() {
76
- if (currentPage > 0) {
77
- currentPage--;
78
- }
79
- goToPage();
80
- }
81
-
82
- function doNext() {
83
- if (currentPage < pageArray.length - 1) {
84
- currentPage++;
85
- }
86
- goToPage();
87
- }
88
-
89
54
  function doExit() {
90
55
  if (
91
56
  reachedEnd == false &&
@@ -0,0 +1,2 @@
1
+ export declare function titlefy(str: string): string;
2
+ export declare function formatSidebarTitle(value: string): string | null;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.titlefy = titlefy;
4
+ exports.formatSidebarTitle = formatSidebarTitle;
5
+ const SLUG_PATTERN = /^\d+(?:\.\d+)?-/;
6
+ function titlefy(str) {
7
+ const arr = str.split("-");
8
+ arr.shift();
9
+ const result = arr.join(" ");
10
+ if (!result) {
11
+ return "";
12
+ }
13
+ return result.charAt(0).toUpperCase() + result.slice(1);
14
+ }
15
+ function formatSidebarTitle(value) {
16
+ const trimmed = value.trim();
17
+ if (!trimmed) {
18
+ return null;
19
+ }
20
+ if (/^\d+(?:\.\d+)?$/.test(trimmed)) {
21
+ return null;
22
+ }
23
+ const formatted = SLUG_PATTERN.test(trimmed) ? titlefy(trimmed) : trimmed;
24
+ return formatted.trim() || null;
25
+ }
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.349",
4
+ "version": "5.0.351",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -73,6 +73,7 @@ import {
73
73
  SyncNotification,
74
74
  } from "../models/creator"
75
75
  import { exportToScorm, exportToEpub, exportToZip } from "../utils/export"
76
+ import { generateAndPersistPackageManifest } from "../utils/packageManifest"
76
77
  import {
77
78
  errorHandler,
78
79
  notFoundHandler,
@@ -5665,6 +5666,11 @@ class ServeCommand extends SessionCommand {
5665
5666
  })
5666
5667
  )
5667
5668
 
5669
+ await generateAndPersistPackageManifest(bucket, slug, {
5670
+ publishedAt: new Date().toISOString(),
5671
+ localDir: buildRoot,
5672
+ })
5673
+
5668
5674
  // 8) Crear el config.json en buildRoot con lo que retorna buildConfig
5669
5675
  const fullConfig = { config, exercises }
5670
5676
  fullConfig.config.slug = slug
@@ -6500,6 +6506,7 @@ class ServeCommand extends SessionCommand {
6500
6506
  ".learn/config.json",
6501
6507
  ".learn/initialSyllabus.json",
6502
6508
  ".learn/sidebar.json",
6509
+ ".learn/package-manifest.json",
6503
6510
  ".learn/memory_bank.txt",
6504
6511
  ])
6505
6512