Skip to content

Commit 648e6bd

Browse files
authored
Merge branch 'main' into feat/theme/callout-text-and-code
2 parents d37154c + 3e7830b commit 648e6bd

File tree

18 files changed

+127
-21
lines changed

18 files changed

+127
-21
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
- name: Setup
2626
uses: pnpm/action-setup@v4
2727
with:
28-
version: 8
28+
version: 8.15.6
2929
- name: Checkout
3030
uses: actions/checkout@v4
3131
- name: Install dependencies
@@ -44,7 +44,7 @@ jobs:
4444
- name: Setup
4545
uses: pnpm/action-setup@v4
4646
with:
47-
version: 8
47+
version: 8.15.6
4848
- name: Checkout
4949
uses: actions/checkout@v4
5050
- name: Install dependencies

.tool-versions

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
nodejs 18.18.0
2-
pnpm 8.10.5

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"test": "pnpm run --filter=@tutorialkit/* --filter=tutorialkit test"
1616
},
1717
"license": "MIT",
18-
"packageManager": "pnpm@8.10.5",
18+
"packageManager": "pnpm@8.15.6",
1919
"devDependencies": {
2020
"@blitz/eslint-plugin": "0.1.0",
2121
"@commitlint/config-conventional": "^19.2.2",
@@ -42,7 +42,8 @@
4242
}
4343
},
4444
"engines": {
45-
"node": ">=18.18.0"
45+
"node": ">=18.18.0",
46+
"pnpm": "8.15.6"
4647
},
4748
"resolutions": {
4849
"@typescript-eslint/utils": "^8.0.0-alpha.30"

packages/astro/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"dist"
1818
],
1919
"scripts": {
20-
"build": "node ./scripts/build.js"
20+
"build": "node ./scripts/build.js",
21+
"test": "vitest"
2122
},
2223
"dependencies": {
2324
"@astrojs/mdx": "^3.1.1",
@@ -55,7 +56,8 @@
5556
"esbuild-node-externals": "^1.13.1",
5657
"execa": "^9.2.0",
5758
"typescript": "^5.4.5",
58-
"vite-plugin-inspect": "0.8.4"
59+
"vite-plugin-inspect": "0.8.4",
60+
"vitest": "^1.6.0"
5961
},
6062
"peerDependencies": {
6163
"astro": "^4.10.2"

packages/astro/scripts/build.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
33
import { cp, rm } from 'node:fs/promises';
44
import { execa } from 'execa';
55
import esbuild from 'esbuild';
6+
import glob from 'fast-glob';
67
import { nodeExternalsPlugin } from 'esbuild-node-externals';
78

89
// clean dist
@@ -15,7 +16,7 @@ execa('tsc', ['--emitDeclarationOnly', '--project', './tsconfig.build.json'], {
1516
});
1617

1718
// build with esbuild
18-
esbuild.build({
19+
await esbuild.build({
1920
entryPoints: ['src/index.ts'],
2021
bundle: true,
2122
tsconfig: './tsconfig.build.json',
@@ -34,3 +35,6 @@ if (existsSync('./dist/default')) {
3435

3536
// copy default folder unmodified
3637
await cp('./src/default', './dist/default', { recursive: true });
38+
39+
// remove test files
40+
await glob('./dist/default/**/*.spec.ts').then((testFiles) => Promise.all(testFiles.map((testFile) => rm(testFile))));

packages/astro/src/default/components/Logo.astro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
import fs from 'node:fs';
33
import path from 'node:path';
4+
import { joinPaths } from '../utils/url';
45
56
const LOGO_EXTENSIONS = ['svg', 'png', 'jpeg', 'jpg'];
67
@@ -16,7 +17,7 @@ function readLogoFile(logoPrefix: string) {
1617
const exists = fs.existsSync(path.join('public', logoFilename));
1718
1819
if (exists) {
19-
logo = `/${logoFilename}`;
20+
logo = joinPaths(import.meta.env.BASE_URL, logoFilename);
2021
break;
2122
}
2223
}

packages/astro/src/default/components/webcontainer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useAuth } from './setup.js';
33

44
import { TutorialStore } from '@tutorialkit/runtime';
55
import { auth, WebContainer } from '@webcontainer/api';
6+
import { joinPaths } from '../utils/url.js';
67

78
interface WebContainerContext {
89
useAuth: boolean;
@@ -24,7 +25,11 @@ if (!import.meta.env.SSR) {
2425
});
2526
}
2627

27-
export const tutorialStore = new TutorialStore({ webcontainer, useAuth });
28+
export const tutorialStore = new TutorialStore({
29+
webcontainer,
30+
useAuth,
31+
basePathname: joinPaths(import.meta.env.BASE_URL, '/'),
32+
});
2833

2934
export async function login() {
3035
auth.startAuthFlow({ popup: true });

packages/astro/src/default/layouts/Layout.astro

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
---
22
import { ViewTransitions } from 'astro:transitions';
3+
import { joinPaths } from '../utils/url';
34
45
interface Props {
56
title: string;
67
}
78
89
const { title } = Astro.props;
10+
const baseURL = import.meta.env.BASE_URL;
911
---
1012

1113
<!doctype html>
@@ -16,7 +18,7 @@ const { title } = Astro.props;
1618
<meta name="viewport" content="width=device-width" />
1719
<meta name="generator" content={Astro.generator} />
1820
<title>{title}</title>
19-
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
21+
<link rel="icon" type="image/svg+xml" href={joinPaths(baseURL, '/favicon.svg')} />
2022
<link rel="preconnect" href="https://fonts.googleapis.com" />
2123
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
2224
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />

packages/astro/src/default/pages/index.astro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
---
22
import { getTutorial } from '../utils/content';
3+
import { joinPaths } from '../utils/url';
34
45
const tutorial = await getTutorial();
56
67
const part = tutorial.parts[tutorial.firstPartId!];
78
const chapter = part.chapters[part?.firstChapterId!];
89
const lesson = chapter.lessons[chapter?.firstLessonId!];
910
10-
const redirect = `/${part.slug}/${chapter.slug}/${lesson.slug}`;
11+
const redirect = joinPaths(import.meta.env.BASE_URL, `/${part.slug}/${chapter.slug}/${lesson.slug}`);
1112
---
1213

1314
<!doctype html>

packages/astro/src/default/utils/content.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { getCollection } from 'astro:content';
1212
import glob from 'fast-glob';
1313
import path from 'node:path';
1414
import { logger } from './logger';
15+
import { joinPaths } from './url';
1516

1617
const CONTENT_DIR = path.join(process.cwd(), 'src/content/tutorial');
1718

@@ -226,6 +227,8 @@ export async function getTutorial(): Promise<Tutorial> {
226227
return 0;
227228
});
228229

230+
const baseURL = import.meta.env.BASE_URL;
231+
229232
// now we link all lessons together
230233
for (const [i, lesson] of lessons.entries()) {
231234
const prevLesson = i > 0 ? lessons.at(i - 1) : undefined;
@@ -248,7 +251,7 @@ export async function getTutorial(): Promise<Tutorial> {
248251

249252
lesson.prev = {
250253
title: prevLesson.data.title,
251-
href: `/${partSlug}/${chapterSlug}/${prevLesson.slug}`,
254+
href: joinPaths(baseURL, `/${partSlug}/${chapterSlug}/${prevLesson.slug}`),
252255
};
253256
}
254257

@@ -258,7 +261,7 @@ export async function getTutorial(): Promise<Tutorial> {
258261

259262
lesson.next = {
260263
title: nextLesson.data.title,
261-
href: `/${partSlug}/${chapterSlug}/${nextLesson.slug}`,
264+
href: joinPaths(baseURL, `/${partSlug}/${chapterSlug}/${nextLesson.slug}`),
262265
};
263266
}
264267
}

packages/astro/src/default/utils/nav.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Tutorial, NavList } from '@tutorialkit/types';
2+
import { joinPaths } from './url';
23

3-
export function generateNavigationList(tutorial: Tutorial): NavList {
4+
export function generateNavigationList(tutorial: Tutorial, baseURL: string): NavList {
45
return objectToSortedArray(tutorial.parts).map((part) => {
56
return {
67
id: part.id,
@@ -13,7 +14,7 @@ export function generateNavigationList(tutorial: Tutorial): NavList {
1314
return {
1415
id: lesson.id,
1516
title: lesson.data.title,
16-
href: `/${part.slug}/${chapter.slug}/${lesson.slug}`,
17+
href: joinPaths(baseURL, `/${part.slug}/${chapter.slug}/${lesson.slug}`),
1718
};
1819
}),
1920
};

packages/astro/src/default/utils/routes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export async function generateStaticRoutes() {
2424
},
2525
props: {
2626
logoLink: tutorial.logoLink,
27-
navList: generateNavigationList(tutorial),
27+
navList: generateNavigationList(tutorial, import.meta.env.BASE_URL),
2828
title: `${part.data.title} / ${chapter.data.title} / ${lesson.data.title}`,
2929
lesson: lesson as Lesson<AstroComponentFactory>,
3030
},
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { it, describe, expect } from 'vitest';
2+
import { joinPaths } from './url';
3+
4+
describe('joinPaths', () => {
5+
it('should join paths', () => {
6+
expect(joinPaths('/a', 'b')).toBe('/a/b');
7+
expect(joinPaths('/a/', 'b')).toBe('/a/b');
8+
expect(joinPaths('/a', '/b')).toBe('/a/b');
9+
expect(joinPaths('/a/', '/b')).toBe('/a/b');
10+
expect(joinPaths('/', '/')).toBe('/');
11+
});
12+
13+
it('should join multiple paths', () => {
14+
expect(joinPaths('/a', 'b', '/c')).toBe('/a/b/c');
15+
expect(joinPaths('/a', '/b', 'c')).toBe('/a/b/c');
16+
expect(joinPaths('/a/', '/b', '/c')).toBe('/a/b/c');
17+
});
18+
19+
it('should join paths with empty strings', () => {
20+
expect(joinPaths('', 'b')).toBe('/b');
21+
expect(joinPaths('/a', '')).toBe('/a');
22+
expect(joinPaths('', '')).toBe('/');
23+
});
24+
25+
it('should join paths with empty strings in the middle', () => {
26+
expect(joinPaths('/a', '', 'b')).toBe('/a/b');
27+
expect(joinPaths('/a', '', '', 'b')).toBe('/a/b');
28+
});
29+
30+
it('should keep trailing slashes', () => {
31+
expect(joinPaths('/a/')).toBe('/a/');
32+
expect(joinPaths('/a/', 'b/')).toBe('/a/b/');
33+
expect(joinPaths('/a/', 'b/', '/c')).toBe('/a/b/c');
34+
expect(joinPaths('/a/', 'b/', '/c/')).toBe('/a/b/c/');
35+
});
36+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export function joinPaths(basePath: string, ...paths: string[]): string {
2+
let result = basePath || '/';
3+
4+
for (const subpath of paths) {
5+
if (subpath.length === 0) {
6+
continue;
7+
}
8+
9+
const resultEndsWithSlash = result.endsWith('/');
10+
const subpathStartsWithSlash = subpath.startsWith('/');
11+
12+
if (resultEndsWithSlash && subpathStartsWithSlash) {
13+
result += subpath.slice(1);
14+
} else if (resultEndsWithSlash || subpathStartsWithSlash) {
15+
result += subpath;
16+
} else {
17+
result += `/${subpath}`;
18+
}
19+
}
20+
21+
return result;
22+
}

packages/runtime/src/lesson-files.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ describe('LessonFilesFetcher', () => {
3535
expect(fetchSpy).toHaveBeenCalledWith('/template-default.json', expect.anything());
3636
});
3737

38+
test('getLessonTemplate should fetch at a different pathname if the fetcher is configured to use a different base', async () => {
39+
fetchBody = { 'a.txt': 'content' };
40+
41+
const fetcher = new LessonFilesFetcher('/foo');
42+
const files = await fetcher.getLessonTemplate({ data: { template: 'default' } } as any);
43+
44+
expect(files).toEqual({ 'a.txt': 'content' });
45+
expect(fetchSpy).toHaveBeenCalledWith('/foo/template-default.json', expect.anything());
46+
});
47+
3848
test('getLessonFiles should fetch files', async () => {
3949
fetchBody = { 'a.txt': 'content' };
4050

packages/runtime/src/lesson-files.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ export class LessonFilesFetcher {
1414
private _templateLoadTask?: Task<Files>;
1515
private _templateLoaded: string | undefined;
1616

17+
constructor(private _basePathname: string = '/') {
18+
if (!this._basePathname.endsWith('/')) {
19+
this._basePathname = this._basePathname + '/';
20+
}
21+
}
22+
1723
async invalidate(filesRef: string): Promise<InvalidationResult> {
1824
if (!this._map.has(filesRef)) {
1925
return { type: 'none' };
@@ -63,7 +69,7 @@ export class LessonFilesFetcher {
6369
this._templateLoadTask?.cancel();
6470

6571
const task = newTask(async (signal) => {
66-
const response = await fetch(`/${templatePathname}`, { signal });
72+
const response = await fetch(`${this._basePathname}${templatePathname}`, { signal });
6773

6874
if (!response.ok) {
6975
throw new Error(`Failed to fetch: status ${response.status}`);
@@ -106,7 +112,7 @@ export class LessonFilesFetcher {
106112

107113
while (true) {
108114
try {
109-
const response = await fetch(`/${pathname}`);
115+
const response = await fetch(`${this._basePathname}${pathname}`);
110116

111117
if (!response.ok) {
112118
throw new Error(`Failed to fetch ${pathname}: ${response.status} ${response.statusText}`);

packages/runtime/src/store/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,16 @@ import { TerminalStore } from './terminal.js';
1414

1515
interface StoreOptions {
1616
webcontainer: Promise<WebContainer>;
17+
18+
/**
19+
* Whether or not authentication is used for the WebContainer API.
20+
*/
1721
useAuth: boolean;
22+
23+
/**
24+
* The base path to use when fetching files.
25+
*/
26+
basePathname?: string;
1827
}
1928

2029
export class TutorialStore {
@@ -26,7 +35,7 @@ export class TutorialStore {
2635
private _terminalStore: TerminalStore;
2736

2837
private _stepController = new StepsController();
29-
private _lessonFilesFetcher = new LessonFilesFetcher();
38+
private _lessonFilesFetcher: LessonFilesFetcher;
3039
private _lessonTask: Task<unknown> | undefined;
3140
private _lesson: Lesson | undefined;
3241
private _ref: number = 1;
@@ -42,9 +51,10 @@ export class TutorialStore {
4251
*/
4352
readonly lessonFullyLoaded = atom<boolean>(false);
4453

45-
constructor({ useAuth, webcontainer }: StoreOptions) {
54+
constructor({ useAuth, webcontainer, basePathname }: StoreOptions) {
4655
this._webcontainer = webcontainer;
4756
this._editorStore = new EditorStore();
57+
this._lessonFilesFetcher = new LessonFilesFetcher(basePathname);
4858
this._previewsStore = new PreviewsStore(this._webcontainer);
4959
this._terminalStore = new TerminalStore(this._webcontainer, useAuth);
5060
this._runner = new TutorialRunner(this._webcontainer, this._terminalStore, this._stepController);

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)