feat(nx-dev): add epic nx release course (#29777)

This commit is contained in:
Juri Strumpflohner 2025-01-31 15:03:08 +01:00 committed by GitHub
parent 7d864c8db8
commit 999dcfbb0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 53 additions and 19 deletions

View File

@ -0,0 +1,7 @@
---
title: 'Versioning and Releasing NPM packages with Nx'
description: 'Learn how Nx Release automates package versioning, changelog generation, and publishing workflows, making releases faster and more reliable.'
authors: [Juri Strumpflohner]
externalLink: 'https://www.epicweb.dev/tutorials/versioning-and-releasing-npm-packages-with-nx'
lessonCount: 20
---

View File

@ -2,6 +2,7 @@
title: 'Introduction to Nx' title: 'Introduction to Nx'
description: 'New to Nx? Then this is where you should start.' description: 'New to Nx? Then this is where you should start.'
authors: [Juri Strumpflohner] authors: [Juri Strumpflohner]
order: 1
--- ---
This course gives you a quick high-level overview of Nx, how running tasks works, task caching, how Nx provides code scaffolding functionality and how you can use `nx migrate` to automatically update your workspace dependencies and code across breaking changes. This course gives you a quick high-level overview of Nx, how running tasks works, task caching, how Nx provides code scaffolding functionality and how you can use `nx migrate` to automatically update your workspace dependencies and code across breaking changes.

View File

@ -3,6 +3,7 @@ title: 'From PNPM Workspaces to Distributed CI'
description: 'Learn how to transform a PNPM workspace monorepo into a high-performance distributed CI setup using Nx.' description: 'Learn how to transform a PNPM workspace monorepo into a high-performance distributed CI setup using Nx.'
authors: [Juri Strumpflohner] authors: [Juri Strumpflohner]
repository: 'https://github.com/nrwl/nx-course-pnpm-nx' repository: 'https://github.com/nrwl/nx-course-pnpm-nx'
order: 2
--- ---
In this course, we'll walk through a step-by-step guide using the Tasker application as our example. Tasker is a task management app built with Next.js, structured as a PNPM workspace monorepo. The monorepo contains the Next.js application which is modularized into packages that handle data access via Prisma to a local DB, UI components, and more. In this course, we'll walk through a step-by-step guide using the Tasker application as our example. Tasker is a task management app built with Next.js, structured as a PNPM workspace monorepo. The monorepo contains the Next.js application which is modularized into packages that handle data access via Prisma to a local DB, UI components, and more.

View File

@ -8,8 +8,11 @@ export interface Course {
authors: BlogAuthor[]; authors: BlogAuthor[];
repository?: string; repository?: string;
lessons: Lesson[]; lessons: Lesson[];
lessonCount?: number;
filePath: string; filePath: string;
externalLink?: string;
totalDuration: string; totalDuration: string;
order?: number;
} }
export interface Lesson { export interface Lesson {

View File

@ -29,7 +29,17 @@ export class CoursesApi {
}) })
.map((folder) => this.getCourse(folder)) .map((folder) => this.getCourse(folder))
); );
return courses; return courses.sort((a, b) => {
// If both courses have order, sort by order
if (a.order !== undefined && b.order !== undefined) {
return a.order - b.order;
}
// If only one has order, prioritize the one with order
if (a.order !== undefined) return -1;
if (b.order !== undefined) return 1;
// If neither has order, sort by id (folder name)
return a.id.localeCompare(b.id);
});
} }
async getCourse(folderName: string): Promise<Course> { async getCourse(folderName: string): Promise<Course> {
@ -42,8 +52,10 @@ export class CoursesApi {
const content = await readFile(courseFilePath, 'utf-8'); const content = await readFile(courseFilePath, 'utf-8');
const frontmatter = extractFrontmatter(content); const frontmatter = extractFrontmatter(content);
let lessons: Lesson[] = [];
if (!frontmatter.externalLink) {
const lessonFolders = await readdir(coursePath); const lessonFolders = await readdir(coursePath);
const lessons = await Promise.all( const tmpLessons = await Promise.all(
lessonFolders lessonFolders
.filter((folder) => { .filter((folder) => {
const stat = lstatSync(join(coursePath, folder)); const stat = lstatSync(join(coursePath, folder));
@ -51,7 +63,8 @@ export class CoursesApi {
}) })
.map((folder) => this.getLessons(folderName, folder)) .map((folder) => this.getLessons(folderName, folder))
); );
const flattenedLessons = lessons.flat(); lessons = tmpLessons.flat();
}
return { return {
id: folderName, id: folderName,
@ -62,9 +75,12 @@ export class CoursesApi {
frontmatter.authors.includes(author.name) frontmatter.authors.includes(author.name)
), ),
repository: frontmatter.repository, repository: frontmatter.repository,
lessons: flattenedLessons, lessons,
filePath: courseFilePath, filePath: courseFilePath,
totalDuration: calculateTotalDuration(flattenedLessons), totalDuration: calculateTotalDuration(lessons),
lessonCount: frontmatter.lessonCount,
externalLink: frontmatter.externalLink,
order: frontmatter.order,
}; };
} }

View File

@ -15,7 +15,7 @@ export function CourseOverview({ courses }: CourseOverviewProps): JSX.Element {
{courses.map((course) => ( {courses.map((course) => (
<Link <Link
key={course.id} key={course.id}
href={`/courses/${course.id}`} href={course.externalLink || `/courses/${course.id}`}
className="block h-full transform-gpu" className="block h-full transform-gpu"
prefetch={false} prefetch={false}
> >
@ -43,14 +43,20 @@ export function CourseOverview({ courses }: CourseOverviewProps): JSX.Element {
</span> </span>
</> </>
)} )}
<span>{course.lessons.length} lessons</span> <span>
{course.lessons.length > 0
? `${course.lessons.length} lessons`
: `${course.lessonCount} lessons`}
</span>
<span className="text-slate-300 dark:text-slate-600"> <span className="text-slate-300 dark:text-slate-600">
</span> </span>
{course.lessons.length > 0 && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<ClockIcon className="h-3 w-3" /> <ClockIcon className="h-3 w-3" />
{course.totalDuration} {course.totalDuration}
</span> </span>
)}
</div> </div>
<div className="mt-4"> <div className="mt-4">