From adefbd56425c2b964a2e0b3084915fda14a78754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Thu, 8 Aug 2024 14:14:16 +0300 Subject: [PATCH 1/5] feat(runtime): add `preview.pathname` --- .../content/docs/reference/configuration.mdx | 20 ++++++++-- packages/runtime/src/store/previews.ts | 25 ++++++------ .../src/webcontainer/preview-info.spec.ts | 39 ++++++++++++++++--- .../runtime/src/webcontainer/preview-info.ts | 6 +++ .../1-basics/1-introduction/2-foo/content.mdx | 5 ++- .../src/templates/default/src/index.js | 2 +- packages/types/src/schemas/common.ts | 7 +++- 7 files changed, 79 insertions(+), 25 deletions(-) diff --git a/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx b/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx index dbf7af7bf..ee8b6e1b2 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 acbf5b5c4..bd7c791f9 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 6dfe3e4ba..3096a9281 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 55c781e77..050cc3263 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 b60458a21..614725e13 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 b942c5c9f..1c72cdefe 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 06d36ec54..56c56c2c1 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(), From c7829e4de85b27c7d828d6d3f2e581f03278c6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Mon, 12 Aug 2024 10:13:18 +0300 Subject: [PATCH 2/5] fix: code review --- packages/runtime/src/store/previews.ts | 36 +++++++++---------- .../runtime/src/webcontainer/port-info.ts | 7 ++++ .../src/webcontainer/preview-info.spec.ts | 2 +- .../runtime/src/webcontainer/preview-info.ts | 36 +++++++++++++------ 4 files changed, 50 insertions(+), 31 deletions(-) create mode 100644 packages/runtime/src/webcontainer/port-info.ts diff --git a/packages/runtime/src/store/previews.ts b/packages/runtime/src/store/previews.ts index bd7c791f9..0adedd2ca 100644 --- a/packages/runtime/src/store/previews.ts +++ b/packages/runtime/src/store/previews.ts @@ -1,10 +1,11 @@ import type { PreviewSchema } from '@tutorialkit/types'; +import type { WebContainer } from '@webcontainer/api'; import { atom } from 'nanostores'; import { PreviewInfo } from '../webcontainer/preview-info.js'; -import type { WebContainer } from '@webcontainer/api'; +import { PortInfo } from '../webcontainer/port-info.js'; export class PreviewsStore { - private _availablePreviews: PreviewInfo[] = []; + private _availablePreviews = new Map(); private _previewsLayout: PreviewInfo[] = []; /** @@ -21,21 +22,19 @@ export class PreviewsStore { const webcontainer = await webcontainerPromise; webcontainer.on('port', (port, type, url) => { - const previewInfos = this._availablePreviews.filter((preview) => preview.port === port); + let portInfo = this._availablePreviews.get(port); + + if (!portInfo) { + portInfo = new PortInfo(port, url, type === 'open'); - if (previewInfos.length === 0) { - const info = new PreviewInfo(port, type === 'open'); - previewInfos.push(info); - this._availablePreviews.push(info); + this._availablePreviews.set(port, portInfo); } - previewInfos.forEach((info) => { - info.ready = type === 'open'; - info.baseUrl = url; - }); + portInfo.ready = type === 'open'; + portInfo.origin = url; if (this._previewsLayout.length === 0) { - this.previews.set(previewInfos); + this.previews.set([new PreviewInfo(portInfo)]); } else { this._previewsLayout = [...this._previewsLayout]; this.previews.set(this._previewsLayout); @@ -60,16 +59,15 @@ export class PreviewsStore { const previewInfos = previews.map((preview) => { const info = new PreviewInfo(preview); + const portInfo = this._availablePreviews.get(info.port); - let previewInfo = this._availablePreviews.find((availablePreview) => PreviewInfo.equals(info, availablePreview)); - - if (!previewInfo) { - previewInfo = info; - - this._availablePreviews.push(previewInfo); + if (!portInfo) { + this._availablePreviews.set(info.port, info.portInfo); + } else { + info.portInfo = portInfo; } - return previewInfo; + return info; }); let areDifferent = previewInfos.length != this._previewsLayout.length; diff --git a/packages/runtime/src/webcontainer/port-info.ts b/packages/runtime/src/webcontainer/port-info.ts new file mode 100644 index 000000000..b609305a7 --- /dev/null +++ b/packages/runtime/src/webcontainer/port-info.ts @@ -0,0 +1,7 @@ +export class PortInfo { + constructor( + readonly port: number, + public origin?: string, + public ready?: boolean, + ) {} +} diff --git a/packages/runtime/src/webcontainer/preview-info.spec.ts b/packages/runtime/src/webcontainer/preview-info.spec.ts index 3096a9281..e66144920 100644 --- a/packages/runtime/src/webcontainer/preview-info.spec.ts +++ b/packages/runtime/src/webcontainer/preview-info.spec.ts @@ -70,7 +70,7 @@ describe('PreviewInfo', () => { it('should have a url with a custom pathname and baseUrl', () => { const previewInfo = new PreviewInfo('3000/foo'); - previewInfo.baseUrl = 'https://example.com'; + previewInfo.portInfo.origin = 'https://example.com'; expect(previewInfo.url).toBe('https://example.com/foo'); }); diff --git a/packages/runtime/src/webcontainer/preview-info.ts b/packages/runtime/src/webcontainer/preview-info.ts index 050cc3263..c03ab9d66 100644 --- a/packages/runtime/src/webcontainer/preview-info.ts +++ b/packages/runtime/src/webcontainer/preview-info.ts @@ -1,10 +1,10 @@ import type { PreviewSchema } from '@tutorialkit/types'; +import { PortInfo } from './port-info.js'; export class PreviewInfo { - port: number; - ready: boolean; + portInfo: PortInfo; + title?: string; - baseUrl?: string; pathname?: string; get url(): string | undefined { @@ -15,27 +15,41 @@ export class PreviewInfo { return undefined; } - constructor(preview: Exclude[0], ready?: boolean) { - if (typeof preview === 'number') { - this.port = preview; + get port() { + return this.portInfo.port; + } + + get baseUrl() { + return this.portInfo.origin; + } + + get ready() { + return this.portInfo.ready; + } + + constructor(preview: PortInfo | Exclude[0], ready?: boolean) { + if (preview instanceof PortInfo) { + this.portInfo = preview; + } else if (typeof preview === 'number') { + this.portInfo = new PortInfo(preview); } else if (typeof preview === 'string') { const [port, ...rest] = preview.split('/'); - this.port = parseInt(port); + this.portInfo = new PortInfo(parseInt(port)); this.pathname = rest.join('/'); } else if (Array.isArray(preview)) { - this.port = preview[0]; + this.portInfo = new PortInfo(preview[0]); this.title = preview[1]; this.pathname = preview[2]; } else { - this.port = preview.port; + this.portInfo = new PortInfo(preview.port); this.title = preview.title; this.pathname = preview.pathname; } - this.ready = !!ready; + this.portInfo.ready ||= !!ready; } static equals(a: PreviewInfo, b: PreviewInfo) { - return a.port === b.port && a.pathname === b.pathname && a.title === b.title; + return a.portInfo.port === b.portInfo.port && a.pathname === b.pathname && a.title === b.title; } } From d05cd13603fb8c812ac7924bb738523503e8a5c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Mon, 12 Aug 2024 13:55:17 +0300 Subject: [PATCH 3/5] fix: code review --- packages/runtime/src/store/previews.ts | 15 +++--- .../runtime/src/webcontainer/port-info.ts | 2 +- .../src/webcontainer/preview-info.spec.ts | 51 ++++++++++++------- .../runtime/src/webcontainer/preview-info.ts | 45 +++++++++------- 4 files changed, 68 insertions(+), 45 deletions(-) diff --git a/packages/runtime/src/store/previews.ts b/packages/runtime/src/store/previews.ts index 0adedd2ca..6f7175cd5 100644 --- a/packages/runtime/src/store/previews.ts +++ b/packages/runtime/src/store/previews.ts @@ -34,7 +34,7 @@ export class PreviewsStore { portInfo.origin = url; if (this._previewsLayout.length === 0) { - this.previews.set([new PreviewInfo(portInfo)]); + this.previews.set([new PreviewInfo({}, portInfo)]); } else { this._previewsLayout = [...this._previewsLayout]; this.previews.set(this._previewsLayout); @@ -57,17 +57,16 @@ export class PreviewsStore { // if the schema is `true`, we just use the default empty array const previews = config === true ? [] : config ?? []; - const previewInfos = previews.map((preview) => { - const info = new PreviewInfo(preview); - const portInfo = this._availablePreviews.get(info.port); + const previewInfos = previews.map((previewConfig) => { + const preview = PreviewInfo.parse(previewConfig); + let portInfo = this._availablePreviews.get(preview.port); if (!portInfo) { - this._availablePreviews.set(info.port, info.portInfo); - } else { - info.portInfo = portInfo; + portInfo = new PortInfo(preview.port); + this._availablePreviews.set(preview.port, portInfo); } - return info; + return new PreviewInfo(preview, portInfo); }); let areDifferent = previewInfos.length != this._previewsLayout.length; diff --git a/packages/runtime/src/webcontainer/port-info.ts b/packages/runtime/src/webcontainer/port-info.ts index b609305a7..47093f0d9 100644 --- a/packages/runtime/src/webcontainer/port-info.ts +++ b/packages/runtime/src/webcontainer/port-info.ts @@ -2,6 +2,6 @@ export class PortInfo { constructor( readonly port: number, public origin?: string, - public ready?: boolean, + public ready: boolean = false, ) {} } diff --git a/packages/runtime/src/webcontainer/preview-info.spec.ts b/packages/runtime/src/webcontainer/preview-info.spec.ts index e66144920..0b008fbc6 100644 --- a/packages/runtime/src/webcontainer/preview-info.spec.ts +++ b/packages/runtime/src/webcontainer/preview-info.spec.ts @@ -1,9 +1,10 @@ import { describe, it, expect } from 'vitest'; import { PreviewInfo } from './preview-info.js'; +import { PortInfo } from './port-info.js'; describe('PreviewInfo', () => { it('should accept a number for port', () => { - const previewInfo = new PreviewInfo(3000); + const previewInfo = PreviewInfo.parse(3000); expect(previewInfo.port).toBe(3000); expect(previewInfo.title).toBe(undefined); @@ -11,7 +12,7 @@ describe('PreviewInfo', () => { }); it('should accept a string for port and pathname', () => { - const previewInfo = new PreviewInfo('3000/some/nested/path'); + const previewInfo = PreviewInfo.parse('3000/some/nested/path'); expect(previewInfo.port).toBe(3000); expect(previewInfo.pathname).toBe('some/nested/path'); @@ -19,7 +20,7 @@ describe('PreviewInfo', () => { }); it('should accept a tuple of [port, title]', () => { - const previewInfo = new PreviewInfo([3000, 'Local server']); + const previewInfo = PreviewInfo.parse([3000, 'Local server']); expect(previewInfo.port).toBe(3000); expect(previewInfo.title).toBe('Local server'); @@ -27,7 +28,7 @@ describe('PreviewInfo', () => { }); it('should accept a tuple of [port, title, pathname]', () => { - const previewInfo = new PreviewInfo([3000, 'Local server', '/docs']); + const previewInfo = PreviewInfo.parse([3000, 'Local server', '/docs']); expect(previewInfo.port).toBe(3000); expect(previewInfo.title).toBe('Local server'); @@ -35,7 +36,7 @@ describe('PreviewInfo', () => { }); it('should accept an object with { port, title }', () => { - const previewInfo = new PreviewInfo({ port: 3000, title: 'Local server' }); + const previewInfo = PreviewInfo.parse({ port: 3000, title: 'Local server' }); expect(previewInfo.port).toBe(3000); expect(previewInfo.title).toBe('Local server'); @@ -43,7 +44,7 @@ describe('PreviewInfo', () => { }); it('should accept an object with { port, title, pathname }', () => { - const previewInfo = new PreviewInfo({ port: 3000, title: 'Local server', pathname: '/docs' }); + const previewInfo = PreviewInfo.parse({ port: 3000, title: 'Local server', pathname: '/docs' }); expect(previewInfo.port).toBe(3000); expect(previewInfo.title).toBe('Local server'); @@ -51,55 +52,67 @@ describe('PreviewInfo', () => { }); it('should not be ready by default', () => { - const previewInfo = new PreviewInfo(3000); + const previewInfo = new PreviewInfo({}, new PortInfo(3000)); expect(previewInfo.ready).toBe(false); }); it('should be ready if explicitly set', () => { - const previewInfo = new PreviewInfo(3000, true); + const previewInfo = new PreviewInfo({}, new PortInfo(3000, undefined, true)); expect(previewInfo.ready).toBe(true); }); it('should not be ready if explicitly set', () => { - const previewInfo = new PreviewInfo(3000, false); + const previewInfo = new PreviewInfo({}, new PortInfo(3000, undefined, false)); expect(previewInfo.ready).toBe(false); }); it('should have a url with a custom pathname and baseUrl', () => { - const previewInfo = new PreviewInfo('3000/foo'); + const parsed = PreviewInfo.parse('3000/foo'); + const previewInfo = new PreviewInfo(parsed, new PortInfo(parsed.port)); previewInfo.portInfo.origin = 'https://example.com'; expect(previewInfo.url).toBe('https://example.com/foo'); }); it('should be equal to another preview info with the same port and title', () => { - const a = new PreviewInfo(3000); - const b = new PreviewInfo(3000); + const a = new PreviewInfo({}, new PortInfo(3000)); + const b = new PreviewInfo({}, new PortInfo(3000)); expect(PreviewInfo.equals(a, b)).toBe(true); }); it('should not be equal to another preview info with a different port', () => { - const a = new PreviewInfo(3000); - const b = new PreviewInfo(4000); + const a = new PreviewInfo({}, new PortInfo(3000)); + const b = new PreviewInfo({}, new PortInfo(4000)); expect(PreviewInfo.equals(a, b)).toBe(false); }); it('should not be equal to another preview info with a different title', () => { - const a = new PreviewInfo([3000, 'Local server']); - const b = new PreviewInfo([3000, 'Remote server']); + const parsed = { + a: PreviewInfo.parse([3000, 'Local server']), + b: PreviewInfo.parse([3000, 'Remote server']), + }; + + const a = new PreviewInfo(parsed.a, new PortInfo(parsed.a.port)); + const b = new PreviewInfo(parsed.b, new PortInfo(parsed.b.port)); expect(PreviewInfo.equals(a, b)).toBe(false); }); it('should not be equal to another preview info with a different pathname', () => { - const a = new PreviewInfo(3000); - const b = new PreviewInfo('3000/b'); - const c = new PreviewInfo('3000/c'); + const parsed = { + a: PreviewInfo.parse(3000), + b: PreviewInfo.parse('3000/b'), + c: PreviewInfo.parse('3000/c'), + }; + + const a = new PreviewInfo(parsed.a, new PortInfo(parsed.a.port)); + const b = new PreviewInfo(parsed.b, new PortInfo(parsed.b.port)); + const c = new PreviewInfo(parsed.c, new PortInfo(parsed.c.port)); 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 c03ab9d66..f71bfe4c7 100644 --- a/packages/runtime/src/webcontainer/preview-info.ts +++ b/packages/runtime/src/webcontainer/preview-info.ts @@ -2,8 +2,7 @@ import type { PreviewSchema } from '@tutorialkit/types'; import { PortInfo } from './port-info.js'; export class PreviewInfo { - portInfo: PortInfo; - + readonly portInfo: PortInfo; title?: string; pathname?: string; @@ -27,29 +26,41 @@ export class PreviewInfo { return this.portInfo.ready; } - constructor(preview: PortInfo | Exclude[0], ready?: boolean) { - if (preview instanceof PortInfo) { - this.portInfo = preview; - } else if (typeof preview === 'number') { - this.portInfo = new PortInfo(preview); + constructor(preview: Omit, portInfo: PortInfo) { + this.title = preview.title; + this.pathname = preview.pathname; + this.portInfo = portInfo; + } + + static parse(preview: Exclude[0]): Preview { + if (typeof preview === 'number') { + return { + port: preview, + }; } else if (typeof preview === 'string') { const [port, ...rest] = preview.split('/'); - this.portInfo = new PortInfo(parseInt(port)); - this.pathname = rest.join('/'); + return { + port: parseInt(port), + pathname: rest.join('/'), + }; } else if (Array.isArray(preview)) { - this.portInfo = new PortInfo(preview[0]); - this.title = preview[1]; - this.pathname = preview[2]; + return { + port: preview[0], + title: preview[1], + pathname: preview[2], + }; } else { - this.portInfo = new PortInfo(preview.port); - this.title = preview.title; - this.pathname = preview.pathname; + return preview; } - - this.portInfo.ready ||= !!ready; } static equals(a: PreviewInfo, b: PreviewInfo) { return a.portInfo.port === b.portInfo.port && a.pathname === b.pathname && a.title === b.title; } } + +interface Preview { + port: number; + pathname?: string; + title?: string; +} From a1c73aa1863b699403deea20d1faf5fb2c95ff2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Mon, 12 Aug 2024 14:36:03 +0300 Subject: [PATCH 4/5] test: multiple previews sharing same port --- packages/runtime/src/store/previews.spec.ts | 52 +++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 packages/runtime/src/store/previews.spec.ts diff --git a/packages/runtime/src/store/previews.spec.ts b/packages/runtime/src/store/previews.spec.ts new file mode 100644 index 000000000..beae213ae --- /dev/null +++ b/packages/runtime/src/store/previews.spec.ts @@ -0,0 +1,52 @@ +import { assert, expect, test, vi } from 'vitest'; +import { PreviewsStore } from './previews.js'; +import type { PortListener, WebContainer } from '@webcontainer/api'; + +test('marks multiple preview infos ready', async () => { + const { store, emit } = await getStore(); + + store.setPreviews([ + { port: 3000, title: 'Dev' }, + { port: 3000, title: 'Docs', pathname: '/docs' }, + ]); + + expect(store.previews.value).toHaveLength(2); + expect(store.previews.value![0].ready).toBe(false); + expect(store.previews.value![1].ready).toBe(false); + + emit(3000, 'open', 'https://localhost'); + + await vi.waitFor(() => { + expect(store.previews.value![0].ready).toBe(true); + expect(store.previews.value![1].ready).toBe(true); + }); +}); + +async function getStore() { + const listeners: PortListener[] = []; + + const webcontainer: Pick = { + on: (type, listener) => { + if (type === 'port') { + listeners.push(listener as PortListener); + } + + return () => undefined; + }, + }; + + const promise = new Promise((resolve) => { + resolve(webcontainer as WebContainer); + }); + + await promise; + + return { + store: new PreviewsStore(promise), + emit: (...args: Parameters) => { + assert(listeners.length > 0, 'Port listeners were not captured'); + + listeners.forEach((cb) => cb(...args)); + }, + }; +} From 9f80711408e5de192c23934e403508c3ec327262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Mon, 12 Aug 2024 15:20:46 +0300 Subject: [PATCH 5/5] test: single preview --- packages/runtime/src/store/previews.spec.ts | 42 +++++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/packages/runtime/src/store/previews.spec.ts b/packages/runtime/src/store/previews.spec.ts index beae213ae..8540fa632 100644 --- a/packages/runtime/src/store/previews.spec.ts +++ b/packages/runtime/src/store/previews.spec.ts @@ -1,25 +1,51 @@ -import { assert, expect, test, vi } from 'vitest'; +import { assert, expect, test } from 'vitest'; import { PreviewsStore } from './previews.js'; import type { PortListener, WebContainer } from '@webcontainer/api'; -test('marks multiple preview infos ready', async () => { +test("preview is set ready on webcontainer's event", async () => { const { store, emit } = await getStore(); + store.setPreviews([3000]); + + assert(store.previews.value); + expect(store.previews.value[0].ready).toBe(false); + + emit(3000, 'open', 'https://localhost'); + + expect(store.previews.value![0].ready).toBe(true); +}); + +test('preview is not set ready when different port is ready', async () => { + const { store, emit } = await getStore(); + store.setPreviews([3000]); + + assert(store.previews.value); + expect(store.previews.value[0].ready).toBe(false); + emit(3001, 'open', 'https://localhost'); + + expect(store.previews.value[0].ready).toBe(false); +}); + +test('marks multiple preview infos ready', async () => { + const { store, emit } = await getStore(); store.setPreviews([ { port: 3000, title: 'Dev' }, { port: 3000, title: 'Docs', pathname: '/docs' }, ]); + assert(store.previews.value); expect(store.previews.value).toHaveLength(2); - expect(store.previews.value![0].ready).toBe(false); - expect(store.previews.value![1].ready).toBe(false); + + expect(store.previews.value[0].ready).toBe(false); + expect(store.previews.value[0].pathname).toBe(undefined); + + expect(store.previews.value[1].ready).toBe(false); + expect(store.previews.value[1].pathname).toBe('/docs'); emit(3000, 'open', 'https://localhost'); - await vi.waitFor(() => { - expect(store.previews.value![0].ready).toBe(true); - expect(store.previews.value![1].ready).toBe(true); - }); + expect(store.previews.value[0].ready).toBe(true); + expect(store.previews.value[1].ready).toBe(true); }); async function getStore() {