@learnpack/learnpack 5.0.350 → 5.0.352

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,58 @@
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
+ description: Record<string, string | null>;
15
+ video: Record<string, string | null>;
16
+ };
17
+ export type PackageManifest = {
18
+ schemaVersion: number;
19
+ generatedAt: string;
20
+ publishedAt: string | null;
21
+ slug: string;
22
+ title: Record<string, string>;
23
+ description: Record<string, string>;
24
+ preview: string;
25
+ technologies?: string[];
26
+ difficulty?: string;
27
+ duration?: number;
28
+ lessons: PackageManifestLesson[];
29
+ };
30
+ export type ReadmeFrontmatter = {
31
+ tutorial?: string;
32
+ intro?: string;
33
+ title?: string;
34
+ };
35
+ export type PackageManifestBuildInput = {
36
+ learnJson: Record<string, unknown>;
37
+ syllabus: Syllabus | null;
38
+ sidebar: Record<string, Record<string, string>>;
39
+ exercises: Exercise[];
40
+ readmeFrontmatters: Record<string, Record<string, ReadmeFrontmatter>>;
41
+ options: {
42
+ slug: string;
43
+ generatedAt: string;
44
+ publishedAt: string | null;
45
+ };
46
+ };
47
+ export declare function packageManifestGcsPath(courseSlug: string): string;
48
+ export declare function resolvePreviewUrl(slug: string, preview?: string): string;
49
+ export declare function extractReadmeH1(body: string): string | null;
50
+ export declare function normalizeReadmeTitle(raw: string): string | null;
51
+ export declare function buildPackageManifestFromSources(input: PackageManifestBuildInput): PackageManifest;
52
+ export declare function parseReadmeContent(content: string): ReadmeFrontmatter;
53
+ export declare function comparePackageManifests(existing: PackageManifest | null, next: PackageManifest): "created" | "updated" | "skipped";
54
+ export declare function serializePackageManifest(manifest: PackageManifest): string;
55
+ export declare function generateAndPersistPackageManifest(bucket: Bucket, courseSlug: string, options: {
56
+ publishedAt: string | null;
57
+ localDir?: string;
58
+ }): Promise<PackageManifest>;
@@ -0,0 +1,250 @@
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.parseReadmeContent = parseReadmeContent;
10
+ exports.comparePackageManifests = comparePackageManifests;
11
+ exports.serializePackageManifest = serializePackageManifest;
12
+ exports.generateAndPersistPackageManifest = generateAndPersistPackageManifest;
13
+ const tslib_1 = require("tslib");
14
+ const fs = require("fs");
15
+ const mkdirp = require("mkdirp");
16
+ const path = require("path");
17
+ const configBuilder_1 = require("./configBuilder");
18
+ const creatorUtilities_1 = require("./creatorUtilities");
19
+ const titlefy_1 = require("./titlefy");
20
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
21
+ const frontMatter = require("front-matter");
22
+ exports.PACKAGE_MANIFEST_FILENAME = "package-manifest.json";
23
+ exports.PACKAGE_MANIFEST_REL_PATH = ".learn/package-manifest.json";
24
+ exports.SCHEMA_VERSION = 1;
25
+ const VALID_LESSON_TYPES = new Set(["READ", "CODE", "QUIZ"]);
26
+ function packageManifestGcsPath(courseSlug) {
27
+ return `courses/${courseSlug}/${exports.PACKAGE_MANIFEST_REL_PATH}`;
28
+ }
29
+ function resolvePreviewUrl(slug, preview) {
30
+ if (preview && preview.startsWith("http")) {
31
+ return preview;
32
+ }
33
+ return `https://${slug}.learn-pack.com/preview.png`;
34
+ }
35
+ function findSyllabusLesson(exerciseSlug, lessons) {
36
+ return lessons.find(lesson => {
37
+ const candidates = [
38
+ (0, creatorUtilities_1.slugify)(lesson.id + "-" + lesson.title),
39
+ lesson.uid,
40
+ ].filter(Boolean);
41
+ return candidates.includes(exerciseSlug);
42
+ });
43
+ }
44
+ function deriveLessonId(exerciseSlug) {
45
+ const match = exerciseSlug.match(/^(\d+(?:\.\d+)?)/);
46
+ return match ? match[1] : exerciseSlug;
47
+ }
48
+ function coerceLessonType(type) {
49
+ if (type && VALID_LESSON_TYPES.has(type)) {
50
+ return type;
51
+ }
52
+ if (type) {
53
+ console.warn(`[package-manifest] Invalid lesson type "${type}", defaulting to READ`);
54
+ }
55
+ return "READ";
56
+ }
57
+ function extractReadmeH1(body) {
58
+ const match = body.match(/^#\s+(.+)$/m);
59
+ if (!match) {
60
+ return null;
61
+ }
62
+ const title = match[1].replace(/\r/g, "").trim();
63
+ return title || null;
64
+ }
65
+ function normalizeReadmeTitle(raw) {
66
+ const normalized = raw
67
+ .trim()
68
+ .replace(/^\d+(?:\.\d+)?\s*[.:-]?\s*/, "")
69
+ .trim();
70
+ return normalized || null;
71
+ }
72
+ function firstNonEmpty(...candidates) {
73
+ for (const candidate of candidates) {
74
+ const value = candidate === null || candidate === void 0 ? void 0 : candidate.trim();
75
+ if (value) {
76
+ return value;
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+ function resolveLessonTitle(exercise, sidebar, syllabusLesson, courseLang, readmeByLang) {
82
+ var _a, _b;
83
+ const sidebarEntry = (_a = sidebar[exercise.slug]) !== null && _a !== void 0 ? _a : {};
84
+ const languages = new Set([
85
+ ...Object.keys(exercise.translations),
86
+ ...Object.keys(sidebarEntry),
87
+ ]);
88
+ const titles = {};
89
+ for (const lang of languages) {
90
+ 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);
91
+ if (title) {
92
+ titles[lang] = title;
93
+ }
94
+ }
95
+ return titles;
96
+ }
97
+ function resolveLessonDescriptions(exercise) {
98
+ const description = {};
99
+ for (const lang of Object.keys(exercise.translations)) {
100
+ // TODO: add description content
101
+ description[lang] = null;
102
+ }
103
+ return description;
104
+ }
105
+ function resolveLessonVideos(exercise, readmeFrontmatters) {
106
+ const video = {};
107
+ const frontmatters = readmeFrontmatters[exercise.slug] || {};
108
+ for (const lang of Object.keys(exercise.translations)) {
109
+ const attrs = frontmatters[lang] || {};
110
+ const url = attrs.tutorial || attrs.intro || null;
111
+ video[lang] = url && url.trim().length > 0 ? url.trim() : null;
112
+ }
113
+ return video;
114
+ }
115
+ function buildPackageManifestFromSources(input) {
116
+ var _a, _b;
117
+ const { learnJson, syllabus, sidebar, exercises, readmeFrontmatters, options, } = input;
118
+ const syllabusLessons = (_a = syllabus === null || syllabus === void 0 ? void 0 : syllabus.lessons) !== null && _a !== void 0 ? _a : [];
119
+ const courseLang = ((_b = syllabus === null || syllabus === void 0 ? void 0 : syllabus.courseInfo) === null || _b === void 0 ? void 0 : _b.language) || "en";
120
+ const sortedExercises = [...exercises].sort((a, b) => a.position - b.position);
121
+ const lessons = sortedExercises.map(exercise => {
122
+ var _a, _b;
123
+ const syllabusLesson = findSyllabusLesson(exercise.slug, syllabusLessons);
124
+ const id = (_a = syllabusLesson === null || syllabusLesson === void 0 ? void 0 : syllabusLesson.id) !== null && _a !== void 0 ? _a : deriveLessonId(exercise.slug);
125
+ const type = syllabusLesson ?
126
+ coerceLessonType(syllabusLesson.type) :
127
+ "READ";
128
+ if (!syllabusLesson) {
129
+ console.warn(`[package-manifest] No syllabus match for exercise "${exercise.slug}", using derived id "${id}"`);
130
+ }
131
+ return {
132
+ id,
133
+ slug: exercise.slug,
134
+ position: exercise.position,
135
+ type,
136
+ title: resolveLessonTitle(exercise, sidebar, syllabusLesson, courseLang, (_b = readmeFrontmatters[exercise.slug]) !== null && _b !== void 0 ? _b : {}),
137
+ description: resolveLessonDescriptions(exercise),
138
+ video: resolveLessonVideos(exercise, readmeFrontmatters),
139
+ };
140
+ });
141
+ const title = learnJson.title || {};
142
+ const description = learnJson.description || {};
143
+ return {
144
+ schemaVersion: exports.SCHEMA_VERSION,
145
+ generatedAt: options.generatedAt,
146
+ publishedAt: options.publishedAt,
147
+ slug: options.slug,
148
+ title,
149
+ description,
150
+ preview: resolvePreviewUrl(options.slug, learnJson.preview),
151
+ technologies: learnJson.technologies,
152
+ difficulty: learnJson.difficulty,
153
+ duration: learnJson.duration,
154
+ lessons,
155
+ };
156
+ }
157
+ async function downloadJsonFile(bucket, filePath) {
158
+ try {
159
+ const [buf] = await bucket.file(filePath).download();
160
+ return JSON.parse(buf.toString());
161
+ }
162
+ catch (_a) {
163
+ return null;
164
+ }
165
+ }
166
+ function parseReadmeContent(content) {
167
+ const parsed = frontMatter(content);
168
+ const attributes = parsed.attributes || {};
169
+ const h1 = extractReadmeH1(parsed.body || "");
170
+ const title = h1 ? normalizeReadmeTitle(h1) : null;
171
+ return Object.assign(Object.assign(Object.assign({}, (attributes.tutorial ? { tutorial: attributes.tutorial } : {})), (attributes.intro ? { intro: attributes.intro } : {})), (title ? { title } : {}));
172
+ }
173
+ function comparePackageManifests(existing, next) {
174
+ if (existing === null) {
175
+ return "created";
176
+ }
177
+ const { generatedAt: _existingGeneratedAt } = existing, existingRest = tslib_1.__rest(existing, ["generatedAt"]);
178
+ const { generatedAt: _nextGeneratedAt } = next, nextRest = tslib_1.__rest(next, ["generatedAt"]);
179
+ if (JSON.stringify(existingRest) === JSON.stringify(nextRest)) {
180
+ return "skipped";
181
+ }
182
+ return "updated";
183
+ }
184
+ function serializePackageManifest(manifest) {
185
+ return JSON.stringify(manifest, null, 2);
186
+ }
187
+ async function readReadmeFrontmatter(bucket, exerciseSlug, lang, readmePath) {
188
+ try {
189
+ const [buf] = await bucket.file(readmePath).download();
190
+ return {
191
+ exerciseSlug,
192
+ lang,
193
+ frontmatter: parseReadmeContent(buf.toString()),
194
+ };
195
+ }
196
+ catch (error) {
197
+ console.warn(`[package-manifest] Could not read README for ${exerciseSlug} (${lang}):`, error.message);
198
+ return { exerciseSlug, lang, frontmatter: {} };
199
+ }
200
+ }
201
+ async function collectReadmeFrontmatters(bucket, courseSlug, exercises) {
202
+ const readmeTasks = exercises.flatMap(exercise => Object.keys(exercise.translations).map(lang => {
203
+ const readmeFilename = `README${(0, creatorUtilities_1.getReadmeExtension)(lang)}`;
204
+ const readmePath = `courses/${courseSlug}/exercises/${exercise.slug}/${readmeFilename}`;
205
+ return readReadmeFrontmatter(bucket, exercise.slug, lang, readmePath);
206
+ }));
207
+ const readmeEntries = await Promise.all(readmeTasks);
208
+ const result = {};
209
+ for (const { exerciseSlug, lang, frontmatter } of readmeEntries) {
210
+ if (!result[exerciseSlug]) {
211
+ result[exerciseSlug] = {};
212
+ }
213
+ result[exerciseSlug][lang] = frontmatter;
214
+ }
215
+ return result;
216
+ }
217
+ function writeManifestJson(manifest, localDir) {
218
+ const json = serializePackageManifest(manifest);
219
+ if (localDir) {
220
+ const learnDir = path.join(localDir, ".learn");
221
+ mkdirp.sync(learnDir);
222
+ const localPath = path.join(learnDir, exports.PACKAGE_MANIFEST_FILENAME);
223
+ fs.writeFileSync(localPath, json, "utf-8");
224
+ }
225
+ return json;
226
+ }
227
+ async function generateAndPersistPackageManifest(bucket, courseSlug, options) {
228
+ const generatedAt = new Date().toISOString();
229
+ const { config: learnJson, exercises } = await (0, configBuilder_1.buildConfig)(bucket, courseSlug);
230
+ const syllabus = await downloadJsonFile(bucket, `courses/${courseSlug}/.learn/initialSyllabus.json`);
231
+ const sidebar = (await downloadJsonFile(bucket, `courses/${courseSlug}/.learn/sidebar.json`)) || {};
232
+ const readmeFrontmatters = await collectReadmeFrontmatters(bucket, courseSlug, exercises);
233
+ const manifest = buildPackageManifestFromSources({
234
+ learnJson,
235
+ syllabus,
236
+ sidebar,
237
+ exercises,
238
+ readmeFrontmatters,
239
+ options: {
240
+ slug: courseSlug,
241
+ generatedAt,
242
+ publishedAt: options.publishedAt,
243
+ },
244
+ });
245
+ const json = writeManifestJson(manifest, options.localDir);
246
+ await bucket
247
+ .file(packageManifestGcsPath(courseSlug))
248
+ .save(Buffer.from(json, "utf8"));
249
+ return manifest;
250
+ }
@@ -0,0 +1,82 @@
1
+ import { Exercise } from "../configBuilder";
2
+ import { Syllabus } from "../../models/creator";
3
+ import { PackageManifest, ReadmeFrontmatter } from "../packageManifest";
4
+ export type PackageResultStatus = "created" | "updated" | "skipped" | "failed";
5
+ export type PackageResult = {
6
+ slug: string;
7
+ status: PackageResultStatus;
8
+ durationMs: number;
9
+ error?: string;
10
+ step?: string;
11
+ sources?: {
12
+ learnJson: boolean;
13
+ configJson: boolean;
14
+ sidebar: boolean;
15
+ syllabus: boolean;
16
+ readmes: number;
17
+ };
18
+ };
19
+ export type BackfillOptions = {
20
+ dryRun: boolean;
21
+ force: boolean;
22
+ skipExisting: boolean;
23
+ noInvalidate: boolean;
24
+ concurrency: number;
25
+ publishedAt: string | null;
26
+ resumeFrom?: string;
27
+ reportOut: string;
28
+ slug?: string;
29
+ bucket: string;
30
+ region: string;
31
+ cloudFrontDistributionId?: string;
32
+ };
33
+ export type BatchReport = {
34
+ total: number;
35
+ created: number;
36
+ updated: number;
37
+ skipped: number;
38
+ failed: number;
39
+ startedAt: string;
40
+ finishedAt: string;
41
+ options: BackfillOptions;
42
+ results: PackageResult[];
43
+ invalidation?: {
44
+ attempted: boolean;
45
+ skippedReason?: string;
46
+ id?: string;
47
+ paths?: string[];
48
+ error?: string;
49
+ };
50
+ };
51
+ export type AwsClient = {
52
+ send(command: unknown): Promise<unknown>;
53
+ };
54
+ type PackageSources = {
55
+ learnJson: Record<string, unknown>;
56
+ exercises: Exercise[];
57
+ sidebar: Record<string, Record<string, string>>;
58
+ syllabus: Syllabus | null;
59
+ readmeFrontmatters: Record<string, Record<string, ReadmeFrontmatter>>;
60
+ meta: PackageResult["sources"];
61
+ };
62
+ export declare function streamToString(body: unknown): Promise<string>;
63
+ export declare function withRetry<T>(fn: () => Promise<T>, attempts?: number, attempt?: number): Promise<T>;
64
+ export declare function mapWithConcurrency<T, R>(items: T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]>;
65
+ export declare function listPublishedSlugs(s3: AwsClient, bucket: string): Promise<string[]>;
66
+ export declare function fetchJsonObject<T>(s3: AwsClient, bucket: string, key: string): Promise<T | null>;
67
+ export declare function fetchPackageSources(s3: AwsClient, bucket: string, slug: string): Promise<PackageSources>;
68
+ export declare function buildManifestFromS3Sources(slug: string, sources: PackageSources, options: {
69
+ publishedAt: string | null;
70
+ generatedAt?: string;
71
+ }): PackageManifest;
72
+ export declare function processPackage(s3: AwsClient, bucket: string, slug: string, options: Pick<BackfillOptions, "dryRun" | "force" | "skipExisting" | "publishedAt">): Promise<PackageResult>;
73
+ export declare function invalidatePackageManifestPaths(cf: AwsClient, distributionId: string, paths: string[]): Promise<{
74
+ id: string;
75
+ }>;
76
+ export declare function runBatch(options: BackfillOptions, clients?: {
77
+ s3?: AwsClient;
78
+ cf?: AwsClient;
79
+ onProgress?: (index: number, total: number, result: PackageResult) => void;
80
+ }): Promise<BatchReport>;
81
+ export declare function getBatchExitCode(report: BatchReport): number;
82
+ export {};