Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
20 changes: 17 additions & 3 deletions docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<PropertyTable inherited type={'Preview[]'} />

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`.
<PropertyTable inherited type="Command" />
Expand Down
33 changes: 16 additions & 17 deletions packages/runtime/src/store/previews.ts
Original file line number Diff line number Diff line change
@@ -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 = new Map<number, PreviewInfo>();
private _availablePreviews = new Map<number, PortInfo>();
private _previewsLayout: PreviewInfo[] = [];

/**
Expand All @@ -21,18 +22,19 @@ export class PreviewsStore {
const webcontainer = await webcontainerPromise;

webcontainer.on('port', (port, type, url) => {
let previewInfo = this._availablePreviews.get(port);
let portInfo = this._availablePreviews.get(port);

if (!portInfo) {
portInfo = new PortInfo(port, url, type === 'open');

if (!previewInfo) {
previewInfo = new PreviewInfo(port, type === 'open');
this._availablePreviews.set(port, previewInfo);
this._availablePreviews.set(port, portInfo);
}

previewInfo.ready = type === 'open';
previewInfo.baseUrl = url;
portInfo.ready = type === 'open';
portInfo.origin = url;

if (this._previewsLayout.length === 0) {
this.previews.set([previewInfo]);
this.previews.set([new PreviewInfo(portInfo)]);
} else {
this._previewsLayout = [...this._previewsLayout];
this.previews.set(this._previewsLayout);
Expand All @@ -57,18 +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.get(info.port);

if (!previewInfo) {
previewInfo = info;

this._availablePreviews.set(previewInfo.port, previewInfo);
if (!portInfo) {
this._availablePreviews.set(info.port, info.portInfo);
} else {
previewInfo.title = info.title;
info.portInfo = portInfo;
}

return previewInfo;
return info;
});

let areDifferent = previewInfos.length != this._previewsLayout.length;
Expand Down
7 changes: 7 additions & 0 deletions packages/runtime/src/webcontainer/port-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class PortInfo {
constructor(
readonly port: number,
public origin?: string,
public ready?: boolean,
) {}
}
41 changes: 34 additions & 7 deletions packages/runtime/src/webcontainer/preview-info.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,52 @@ 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]', () => {
const previewInfo = new PreviewInfo([3000, 'Local server']);

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 }', () => {
const previewInfo = new PreviewInfo({ port: 3000, title: 'Local server' });

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', () => {
Expand All @@ -41,9 +69,8 @@ describe('PreviewInfo', () => {
});

it('should have a url with a custom pathname and baseUrl', () => {
const previewInfo = new PreviewInfo(3000);
previewInfo.baseUrl = 'https://example.com';
previewInfo.pathname = '/foo';
const previewInfo = new PreviewInfo('3000/foo');
previewInfo.portInfo.origin = 'https://example.com';

expect(previewInfo.url).toBe('https://example.com/foo');
});
Expand Down Expand Up @@ -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);
});
});
40 changes: 30 additions & 10 deletions packages/runtime/src/webcontainer/preview-info.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -15,21 +15,41 @@ export class PreviewInfo {
return undefined;
}

constructor(preview: Exclude<PreviewSchema, boolean>[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<PreviewSchema, boolean>[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.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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion packages/template/src/templates/default/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/',
Expand Down
7 changes: 6 additions & 1 deletion packages/types/src/schemas/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down