fix(course-format): directories are not lesson files; a failing course never rejects loadAll

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas Winther
2026-10-30 02:54:23 +01:00
co-authored by Claude Opus 5.5
parent b8c65626f9
commit a82ab18bee
2 changed files with 59 additions and 6 deletions
+31 -6
View File
@@ -1,5 +1,5 @@
import { readdir, readFile, realpath } from "node:fs/promises";
import { realpathSync } from "node:fs";
import { realpathSync, statSync } from "node:fs";
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { extractHints, extractSection } from "./markdown";
import { parseManifest, type Lesson, type Manifest } from "./schema";
@@ -67,8 +67,27 @@ export async function discoverManifests(courseDirs: string[]): Promise<string[]>
return found;
}
/** Never throws: an unexpected failure becomes an error diagnostic on that course only. */
export async function loadCourse(manifestPath: string): Promise<LoadedCourse> {
manifestPath = resolve(manifestPath);
const dir = dirname(manifestPath);
try {
return await loadCourseUnsafe(manifestPath);
} catch (e) {
const id = basename(dir);
return {
id,
manifestPath,
root: dir,
manifest: null,
status: "error",
diagnostics: [{ level: "error", courseId: id, path: manifestPath, message: `unexpected error loading course: ${(e as Error).message}` }],
lessonIndex: new Map(),
};
}
}
async function loadCourseUnsafe(manifestPath: string): Promise<LoadedCourse> {
const dir = dirname(manifestPath);
const diagnostics: Diagnostic[] = [];
const course: LoadedCourse = {
@@ -118,10 +137,16 @@ export async function loadCourse(manifestPath: string): Promise<LoadedCourse> {
if (!mdCache.has(abs)) mdCache.set(abs, await Bun.file(abs).text());
return mdCache.get(abs)!;
};
/** Reports an error and returns false when `rel` does not exist inside the root. */
const requireFile = (rel: string, what: string, lessonId?: string): boolean => {
if (resolveInside(root, rel)) return true;
err(`${what} not found: ${rel}`, { lessonId, path: rel });
/** Reports an error and returns false when `rel` is not a regular file (or directory, if asked) inside the root. */
const requireFile = (rel: string, what: string, lessonId?: string, dir = false): boolean => {
const abs = resolveInside(root, rel);
if (!abs) {
err(`${what} not found: ${rel}`, { lessonId, path: rel });
return false;
}
const st = statSync(abs);
if (dir ? st.isDirectory() : st.isFile()) return true;
err(`${what} is not a ${dir ? "directory" : "regular file"}: ${rel}`, { lessonId, path: rel });
return false;
};
@@ -131,7 +156,7 @@ export async function loadCourse(manifestPath: string): Promise<LoadedCourse> {
for (const a of manifest.attachments ?? []) requireFile(a.file, "attachment");
for (const mod of manifest.modules) {
if (mod.code) requireFile(mod.code.dir, `module "${mod.id}" code dir`);
if (mod.code) requireFile(mod.code.dir, `module "${mod.id}" code dir`, undefined, true);
for (const l of mod.lessons) {
const id = l.id;
let fileOk = true;