Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warn-malformed-course.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'tessera-learn': patch
---

A course directory under `courses/` that has a `pages/` folder but is missing its `course.config.js` is now reported as skipped when listing the workspace's courses, instead of being silently dropped.
46 changes: 37 additions & 9 deletions packages/tessera-learn/src/plugin/course-root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,57 @@ export function findWorkspaceRoot(cwd: string): string | null {
}
}

export function listCourses(workspaceRoot: string): string[] {
function scanCourses(workspaceRoot: string): {
courses: string[];
malformed: string[];
} {
const coursesDir = join(workspaceRoot, 'courses');
const courses: string[] = [];
const malformed: string[] = [];
try {
return readdirSync(coursesDir, { withFileTypes: true })
.filter((e) => e.isDirectory() && isCourse(join(coursesDir, e.name)))
.map((e) => e.name)
.sort();
for (const e of readdirSync(coursesDir, { withFileTypes: true })) {
if (!e.isDirectory()) continue;
const dir = join(coursesDir, e.name);
if (isCourse(dir)) courses.push(e.name);
else if (isDir(join(dir, 'pages'))) malformed.push(e.name);
}
} catch {
return [];
return { courses, malformed };
}
return { courses: courses.sort(), malformed: malformed.sort() };
}

export function listCourses(workspaceRoot: string): string[] {
return scanCourses(workspaceRoot).courses;
}

export function listMalformedCourses(workspaceRoot: string): string[] {
return scanCourses(workspaceRoot).malformed;
}

const NOT_A_WORKSPACE =
'Not inside a Tessera workspace — no `courses/` directory was found at or above the current directory.';

function malformedHint(malformed: string[]): string {
if (malformed.length === 0) return '';
return (
`\nSkipped (missing course.config.js):\n` +
malformed.map((c) => ` courses/${c}`).join('\n')
);
}

function listHint(workspaceRoot: string): string {
const courses = listCourses(workspaceRoot);
const { courses, malformed } = scanCourses(workspaceRoot);
if (courses.length === 0) {
return '\nNo courses found. Create one with `tessera new <name>`.';
return (
'\nNo courses found. Create one with `tessera new <name>`.' +
malformedHint(malformed)
);
}
return (
`\nAvailable courses:\n${courses.map((c) => ` ${c}`).join('\n')}` +
'\nName one (`tessera <command> <course>`) or cd into its folder.'
'\nName one (`tessera <command> <course>`) or cd into its folder.' +
malformedHint(malformed)
);
}

Expand Down
28 changes: 28 additions & 0 deletions packages/tessera-learn/tests/course-root.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
resolveCourse,
findWorkspaceRoot,
listCourses,
listMalformedCourses,
} from '../src/plugin/course-root.js';

let ws: string;
Expand Down Expand Up @@ -62,6 +63,23 @@ describe('listCourses', () => {
});
});

describe('listMalformedCourses', () => {
it('flags a directory that has pages/ but no course.config.js', () => {
mkdirSync(join(ws, 'courses', 'half-built', 'pages'), { recursive: true });
expect(listMalformedCourses(ws)).toEqual(['half-built']);
});

it('does not flag legitimate non-course directories', () => {
mkdirSync(join(ws, 'courses', 'shared-assets'), { recursive: true });
writeFileSync(join(ws, 'courses', 'README.md'), '# courses');
expect(listMalformedCourses(ws)).toEqual([]);
});

it('does not flag valid courses', () => {
expect(listMalformedCourses(ws)).toEqual([]);
});
});

describe('resolveCourse', () => {
it('resolves cwd as the course root when it holds course.config.js (no name)', () => {
const cwd = join(ws, 'courses', 'getting-started');
Expand Down Expand Up @@ -135,4 +153,14 @@ describe('resolveCourse', () => {
const result = resolveCourse(tmpdir(), 'getting-started');
expect(result.ok).toBe(false);
});

it('surfaces a malformed course in the hint instead of silently dropping it', () => {
mkdirSync(join(ws, 'courses', 'half-built', 'pages'), { recursive: true });
const result = resolveCourse(ws, 'half-built');
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain('half-built');
expect(result.error).toContain('course.config.js');
}
});
});