diff --git a/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx b/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx index dbf7af7b..ee8b6e1b 100644 --- a/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx +++ b/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx @@ -91,18 +91,32 @@ Configure whether or not the editor should be rendered. If an object is provided ##### `previews` Configure which ports should be used for the previews allowing you to align the behavior with your demo application's dev server setup. If not specified, the lowest port will be used. -You can optionally provide these as an array of tuples where the first element is the port number and the second is the title of the preview, or as an object. The `Preview` type has the following shape: ```ts -type Preview = string +type Preview = + | number + | string | [port: number, title: string] - | { port: number, title: string } + | [port: number, title: string, pathname: string] + | { port: number, title: string, pathname?: string } ``` +Example value: + +```yaml +previews: + - 3000 # Preview is on :3000/ + - "3001/docs" # Preview is on :3001/docs/ + - [3002, "Dev Server"] # Preview is on :3002/. Displayed title is "Dev Server". + - [3003, "Dev Server", "/docs"] # Preview is on :3003/docs/. Displayed title is "Dev Server". + - { port: 3004, title: "Dev Server" } # Preview is on :3004/. Displayed title is "Dev Server". + - { port: 3005, title: "Dev Server", pathname: "/docs" } # Preview is on :3005/docs/. Displayed title is "Dev Server". +``` + ##### `mainCommand` The main command to be executed. This command will run after the `prepareCommands`. diff --git a/packages/runtime/src/store/previews.ts b/packages/runtime/src/store/previews.ts index acbf5b5c..bd7c791f 100644 --- a/packages/runtime/src/store/previews.ts +++ b/packages/runtime/src/store/previews.ts @@ -4,7 +4,7 @@ import { PreviewInfo } from '../webcontainer/preview-info.js'; import type { WebContainer } from '@webcontainer/api'; export class PreviewsStore { - private _availablePreviews = new Map(); + private _availablePreviews: PreviewInfo[] = []; private _previewsLayout: PreviewInfo[] = []; /** @@ -21,18 +21,21 @@ export class PreviewsStore { const webcontainer = await webcontainerPromise; webcontainer.on('port', (port, type, url) => { - let previewInfo = this._availablePreviews.get(port); + const previewInfos = this._availablePreviews.filter((preview) => preview.port === port); - if (!previewInfo) { - previewInfo = new PreviewInfo(port, type === 'open'); - this._availablePreviews.set(port, previewInfo); + if (previewInfos.length === 0) { + const info = new PreviewInfo(port, type === 'open'); + previewInfos.push(info); + this._availablePreviews.push(info); } - previewInfo.ready = type === 'open'; - previewInfo.baseUrl = url; + previewInfos.forEach((info) => { + info.ready = type === 'open'; + info.baseUrl = url; + }); if (this._previewsLayout.length === 0) { - this.previews.set([previewInfo]); + this.previews.set(previewInfos); } else { this._previewsLayout = [...this._previewsLayout]; this.previews.set(this._previewsLayout); @@ -58,14 +61,12 @@ export class PreviewsStore { const previewInfos = previews.map((preview) => { const info = new PreviewInfo(preview); - let previewInfo = this._availablePreviews.get(info.port); + let previewInfo = this._availablePreviews.find((availablePreview) => PreviewInfo.equals(info, availablePreview)); if (!previewInfo) { previewInfo = info; - this._availablePreviews.set(previewInfo.port, previewInfo); - } else { - previewInfo.title = info.title; + this._availablePreviews.push(previewInfo); } return previewInfo; diff --git a/packages/runtime/src/webcontainer/preview-info.spec.ts b/packages/runtime/src/webcontainer/preview-info.spec.ts index 6dfe3e4b..3096a928 100644 --- a/packages/runtime/src/webcontainer/preview-info.spec.ts +++ b/packages/runtime/src/webcontainer/preview-info.spec.ts @@ -2,10 +2,20 @@ import { describe, it, expect } from 'vitest'; import { PreviewInfo } from './preview-info.js'; describe('PreviewInfo', () => { - it('should accept a port', () => { + it('should accept a number for port', () => { const previewInfo = new PreviewInfo(3000); expect(previewInfo.port).toBe(3000); + expect(previewInfo.title).toBe(undefined); + expect(previewInfo.pathname).toBe(undefined); + }); + + it('should accept a string for port and pathname', () => { + const previewInfo = new PreviewInfo('3000/some/nested/path'); + + expect(previewInfo.port).toBe(3000); + expect(previewInfo.pathname).toBe('some/nested/path'); + expect(previewInfo.title).toBe(undefined); }); it('should accept a tuple of [port, title]', () => { @@ -13,6 +23,15 @@ describe('PreviewInfo', () => { expect(previewInfo.port).toBe(3000); expect(previewInfo.title).toBe('Local server'); + expect(previewInfo.pathname).toBe(undefined); + }); + + it('should accept a tuple of [port, title, pathname]', () => { + const previewInfo = new PreviewInfo([3000, 'Local server', '/docs']); + + expect(previewInfo.port).toBe(3000); + expect(previewInfo.title).toBe('Local server'); + expect(previewInfo.pathname).toBe('/docs'); }); it('should accept an object with { port, title }', () => { @@ -20,6 +39,15 @@ describe('PreviewInfo', () => { expect(previewInfo.port).toBe(3000); expect(previewInfo.title).toBe('Local server'); + expect(previewInfo.pathname).toBe(undefined); + }); + + it('should accept an object with { port, title, pathname }', () => { + const previewInfo = new PreviewInfo({ port: 3000, title: 'Local server', pathname: '/docs' }); + + expect(previewInfo.port).toBe(3000); + expect(previewInfo.title).toBe('Local server'); + expect(previewInfo.pathname).toBe('/docs'); }); it('should not be ready by default', () => { @@ -41,9 +69,8 @@ describe('PreviewInfo', () => { }); it('should have a url with a custom pathname and baseUrl', () => { - const previewInfo = new PreviewInfo(3000); + const previewInfo = new PreviewInfo('3000/foo'); previewInfo.baseUrl = 'https://example.com'; - previewInfo.pathname = '/foo'; expect(previewInfo.url).toBe('https://example.com/foo'); }); @@ -71,10 +98,10 @@ describe('PreviewInfo', () => { it('should not be equal to another preview info with a different pathname', () => { const a = new PreviewInfo(3000); - const b = new PreviewInfo(3000); - - a.pathname = '/foo'; + const b = new PreviewInfo('3000/b'); + const c = new PreviewInfo('3000/c'); expect(PreviewInfo.equals(a, b)).toBe(false); + expect(PreviewInfo.equals(b, c)).toBe(false); }); }); diff --git a/packages/runtime/src/webcontainer/preview-info.ts b/packages/runtime/src/webcontainer/preview-info.ts index 55c781e7..050cc326 100644 --- a/packages/runtime/src/webcontainer/preview-info.ts +++ b/packages/runtime/src/webcontainer/preview-info.ts @@ -18,12 +18,18 @@ export class PreviewInfo { constructor(preview: Exclude[0], ready?: boolean) { if (typeof preview === 'number') { this.port = preview; + } else if (typeof preview === 'string') { + const [port, ...rest] = preview.split('/'); + this.port = parseInt(port); + this.pathname = rest.join('/'); } else if (Array.isArray(preview)) { this.port = preview[0]; this.title = preview[1]; + this.pathname = preview[2]; } else { this.port = preview.port; this.title = preview.title; + this.pathname = preview.pathname; } this.ready = !!ready; diff --git a/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/content.mdx b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/content.mdx index b60458a2..614725e1 100644 --- a/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/content.mdx +++ b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/content.mdx @@ -4,9 +4,10 @@ title: Foo from part 1 slug: foo focus: /src/index.html previews: - - [8080, 'Main Page'] + - { title: 'Main Page', port: 8080, pathname: '/src'} - [1, 'Test Runner'] - - [2, 'Bar'] + - '2/some/custom/pathname' + - '2/another/pathname' terminal: panels: 'terminal' editPageLink: 'https://tutorialkit.dev' diff --git a/packages/template/src/templates/default/src/index.js b/packages/template/src/templates/default/src/index.js index b942c5c9..1c72cdef 100644 --- a/packages/template/src/templates/default/src/index.js +++ b/packages/template/src/templates/default/src/index.js @@ -15,7 +15,7 @@ createServer((_req, res) => { `); }).listen(1); -createServer((_req, res) => res.end('Server 2')).listen(2); +createServer((req, res) => res.end(`Server 2\n${req.method} ${req.url}`)).listen(2); servor({ root: 'src/', diff --git a/packages/types/src/schemas/common.ts b/packages/types/src/schemas/common.ts index 06d36ec5..56c56c2c 100644 --- a/packages/types/src/schemas/common.ts +++ b/packages/types/src/schemas/common.ts @@ -37,12 +37,17 @@ export const previewSchema = z.union([ // a single number, the port for the preview z.number(), - // a tuple, the port followed by a title + // a string, the port and pathname + z.string(), + + // a tuple, the port followed by a title and optional pathname z.tuple([z.number(), z.string()]), + z.tuple([z.number(), z.string(), z.string()]), z.strictObject({ port: z.number().describe('Port number of the preview.'), title: z.string().describe('Title of the preview.'), + pathname: z.string().optional().describe('Pathname of the preview URL.'), }), ]) .array(),