Skip to content

Commit 9bf2156

Browse files
authored
feat(runtime): add preview.pathname (#233)
1 parent bd0ca1a commit 9bf2156

File tree

9 files changed

+232
-58
lines changed

9 files changed

+232
-58
lines changed

docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,32 @@ Configure whether or not the editor should be rendered. If an object is provided
9191
##### `previews`
9292
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.
9393

94-
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.
9594
<PropertyTable inherited type={'Preview[]'} />
9695

9796
The `Preview` type has the following shape:
9897

9998
```ts
100-
type Preview = string
99+
type Preview =
100+
| number
101+
| string
101102
| [port: number, title: string]
102-
| { port: number, title: string }
103+
| [port: number, title: string, pathname: string]
104+
| { port: number, title: string, pathname?: string }
103105

104106
```
105107

108+
Example value:
109+
110+
```yaml
111+
previews:
112+
- 3000 # Preview is on :3000/
113+
- "3001/docs" # Preview is on :3001/docs/
114+
- [3002, "Dev Server"] # Preview is on :3002/. Displayed title is "Dev Server".
115+
- [3003, "Dev Server", "/docs"] # Preview is on :3003/docs/. Displayed title is "Dev Server".
116+
- { port: 3004, title: "Dev Server" } # Preview is on :3004/. Displayed title is "Dev Server".
117+
- { port: 3005, title: "Dev Server", pathname: "/docs" } # Preview is on :3005/docs/. Displayed title is "Dev Server".
118+
```
119+
106120
##### `mainCommand`
107121
The main command to be executed. This command will run after the `prepareCommands`.
108122
<PropertyTable inherited type="Command" />
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { assert, expect, test } from 'vitest';
2+
import { PreviewsStore } from './previews.js';
3+
import type { PortListener, WebContainer } from '@webcontainer/api';
4+
5+
test("preview is set ready on webcontainer's event", async () => {
6+
const { store, emit } = await getStore();
7+
store.setPreviews([3000]);
8+
9+
assert(store.previews.value);
10+
expect(store.previews.value[0].ready).toBe(false);
11+
12+
emit(3000, 'open', 'https://localhost');
13+
14+
expect(store.previews.value![0].ready).toBe(true);
15+
});
16+
17+
test('preview is not set ready when different port is ready', async () => {
18+
const { store, emit } = await getStore();
19+
store.setPreviews([3000]);
20+
21+
assert(store.previews.value);
22+
expect(store.previews.value[0].ready).toBe(false);
23+
24+
emit(3001, 'open', 'https://localhost');
25+
26+
expect(store.previews.value[0].ready).toBe(false);
27+
});
28+
29+
test('marks multiple preview infos ready', async () => {
30+
const { store, emit } = await getStore();
31+
store.setPreviews([
32+
{ port: 3000, title: 'Dev' },
33+
{ port: 3000, title: 'Docs', pathname: '/docs' },
34+
]);
35+
36+
assert(store.previews.value);
37+
expect(store.previews.value).toHaveLength(2);
38+
39+
expect(store.previews.value[0].ready).toBe(false);
40+
expect(store.previews.value[0].pathname).toBe(undefined);
41+
42+
expect(store.previews.value[1].ready).toBe(false);
43+
expect(store.previews.value[1].pathname).toBe('/docs');
44+
45+
emit(3000, 'open', 'https://localhost');
46+
47+
expect(store.previews.value[0].ready).toBe(true);
48+
expect(store.previews.value[1].ready).toBe(true);
49+
});
50+
51+
async function getStore() {
52+
const listeners: PortListener[] = [];
53+
54+
const webcontainer: Pick<WebContainer, 'on'> = {
55+
on: (type, listener) => {
56+
if (type === 'port') {
57+
listeners.push(listener as PortListener);
58+
}
59+
60+
return () => undefined;
61+
},
62+
};
63+
64+
const promise = new Promise<WebContainer>((resolve) => {
65+
resolve(webcontainer as WebContainer);
66+
});
67+
68+
await promise;
69+
70+
return {
71+
store: new PreviewsStore(promise),
72+
emit: (...args: Parameters<PortListener>) => {
73+
assert(listeners.length > 0, 'Port listeners were not captured');
74+
75+
listeners.forEach((cb) => cb(...args));
76+
},
77+
};
78+
}

packages/runtime/src/store/previews.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { PreviewSchema } from '@tutorialkit/types';
2+
import type { WebContainer } from '@webcontainer/api';
23
import { atom } from 'nanostores';
34
import { PreviewInfo } from '../webcontainer/preview-info.js';
4-
import type { WebContainer } from '@webcontainer/api';
5+
import { PortInfo } from '../webcontainer/port-info.js';
56

67
export class PreviewsStore {
7-
private _availablePreviews = new Map<number, PreviewInfo>();
8+
private _availablePreviews = new Map<number, PortInfo>();
89
private _previewsLayout: PreviewInfo[] = [];
910

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

2324
webcontainer.on('port', (port, type, url) => {
24-
let previewInfo = this._availablePreviews.get(port);
25+
let portInfo = this._availablePreviews.get(port);
26+
27+
if (!portInfo) {
28+
portInfo = new PortInfo(port, url, type === 'open');
2529

26-
if (!previewInfo) {
27-
previewInfo = new PreviewInfo(port, type === 'open');
28-
this._availablePreviews.set(port, previewInfo);
30+
this._availablePreviews.set(port, portInfo);
2931
}
3032

31-
previewInfo.ready = type === 'open';
32-
previewInfo.baseUrl = url;
33+
portInfo.ready = type === 'open';
34+
portInfo.origin = url;
3335

3436
if (this._previewsLayout.length === 0) {
35-
this.previews.set([previewInfo]);
37+
this.previews.set([new PreviewInfo({}, portInfo)]);
3638
} else {
3739
this._previewsLayout = [...this._previewsLayout];
3840
this.previews.set(this._previewsLayout);
@@ -55,20 +57,16 @@ export class PreviewsStore {
5557
// if the schema is `true`, we just use the default empty array
5658
const previews = config === true ? [] : config ?? [];
5759

58-
const previewInfos = previews.map((preview) => {
59-
const info = new PreviewInfo(preview);
60+
const previewInfos = previews.map((previewConfig) => {
61+
const preview = PreviewInfo.parse(previewConfig);
62+
let portInfo = this._availablePreviews.get(preview.port);
6063

61-
let previewInfo = this._availablePreviews.get(info.port);
62-
63-
if (!previewInfo) {
64-
previewInfo = info;
65-
66-
this._availablePreviews.set(previewInfo.port, previewInfo);
67-
} else {
68-
previewInfo.title = info.title;
64+
if (!portInfo) {
65+
portInfo = new PortInfo(preview.port);
66+
this._availablePreviews.set(preview.port, portInfo);
6967
}
7068

71-
return previewInfo;
69+
return new PreviewInfo(preview, portInfo);
7270
});
7371

7472
let areDifferent = previewInfos.length != this._previewsLayout.length;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export class PortInfo {
2+
constructor(
3+
readonly port: number,
4+
public origin?: string,
5+
public ready: boolean = false,
6+
) {}
7+
}
Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,120 @@
11
import { describe, it, expect } from 'vitest';
22
import { PreviewInfo } from './preview-info.js';
3+
import { PortInfo } from './port-info.js';
34

45
describe('PreviewInfo', () => {
5-
it('should accept a port', () => {
6-
const previewInfo = new PreviewInfo(3000);
6+
it('should accept a number for port', () => {
7+
const previewInfo = PreviewInfo.parse(3000);
78

89
expect(previewInfo.port).toBe(3000);
10+
expect(previewInfo.title).toBe(undefined);
11+
expect(previewInfo.pathname).toBe(undefined);
12+
});
13+
14+
it('should accept a string for port and pathname', () => {
15+
const previewInfo = PreviewInfo.parse('3000/some/nested/path');
16+
17+
expect(previewInfo.port).toBe(3000);
18+
expect(previewInfo.pathname).toBe('some/nested/path');
19+
expect(previewInfo.title).toBe(undefined);
920
});
1021

1122
it('should accept a tuple of [port, title]', () => {
12-
const previewInfo = new PreviewInfo([3000, 'Local server']);
23+
const previewInfo = PreviewInfo.parse([3000, 'Local server']);
1324

1425
expect(previewInfo.port).toBe(3000);
1526
expect(previewInfo.title).toBe('Local server');
27+
expect(previewInfo.pathname).toBe(undefined);
28+
});
29+
30+
it('should accept a tuple of [port, title, pathname]', () => {
31+
const previewInfo = PreviewInfo.parse([3000, 'Local server', '/docs']);
32+
33+
expect(previewInfo.port).toBe(3000);
34+
expect(previewInfo.title).toBe('Local server');
35+
expect(previewInfo.pathname).toBe('/docs');
1636
});
1737

1838
it('should accept an object with { port, title }', () => {
19-
const previewInfo = new PreviewInfo({ port: 3000, title: 'Local server' });
39+
const previewInfo = PreviewInfo.parse({ port: 3000, title: 'Local server' });
2040

2141
expect(previewInfo.port).toBe(3000);
2242
expect(previewInfo.title).toBe('Local server');
43+
expect(previewInfo.pathname).toBe(undefined);
44+
});
45+
46+
it('should accept an object with { port, title, pathname }', () => {
47+
const previewInfo = PreviewInfo.parse({ port: 3000, title: 'Local server', pathname: '/docs' });
48+
49+
expect(previewInfo.port).toBe(3000);
50+
expect(previewInfo.title).toBe('Local server');
51+
expect(previewInfo.pathname).toBe('/docs');
2352
});
2453

2554
it('should not be ready by default', () => {
26-
const previewInfo = new PreviewInfo(3000);
55+
const previewInfo = new PreviewInfo({}, new PortInfo(3000));
2756

2857
expect(previewInfo.ready).toBe(false);
2958
});
3059

3160
it('should be ready if explicitly set', () => {
32-
const previewInfo = new PreviewInfo(3000, true);
61+
const previewInfo = new PreviewInfo({}, new PortInfo(3000, undefined, true));
3362

3463
expect(previewInfo.ready).toBe(true);
3564
});
3665

3766
it('should not be ready if explicitly set', () => {
38-
const previewInfo = new PreviewInfo(3000, false);
67+
const previewInfo = new PreviewInfo({}, new PortInfo(3000, undefined, false));
3968

4069
expect(previewInfo.ready).toBe(false);
4170
});
4271

4372
it('should have a url with a custom pathname and baseUrl', () => {
44-
const previewInfo = new PreviewInfo(3000);
45-
previewInfo.baseUrl = 'https://example.com';
46-
previewInfo.pathname = '/foo';
73+
const parsed = PreviewInfo.parse('3000/foo');
74+
const previewInfo = new PreviewInfo(parsed, new PortInfo(parsed.port));
75+
previewInfo.portInfo.origin = 'https://example.com';
4776

4877
expect(previewInfo.url).toBe('https://example.com/foo');
4978
});
5079

5180
it('should be equal to another preview info with the same port and title', () => {
52-
const a = new PreviewInfo(3000);
53-
const b = new PreviewInfo(3000);
81+
const a = new PreviewInfo({}, new PortInfo(3000));
82+
const b = new PreviewInfo({}, new PortInfo(3000));
5483

5584
expect(PreviewInfo.equals(a, b)).toBe(true);
5685
});
5786

5887
it('should not be equal to another preview info with a different port', () => {
59-
const a = new PreviewInfo(3000);
60-
const b = new PreviewInfo(4000);
88+
const a = new PreviewInfo({}, new PortInfo(3000));
89+
const b = new PreviewInfo({}, new PortInfo(4000));
6190

6291
expect(PreviewInfo.equals(a, b)).toBe(false);
6392
});
6493

6594
it('should not be equal to another preview info with a different title', () => {
66-
const a = new PreviewInfo([3000, 'Local server']);
67-
const b = new PreviewInfo([3000, 'Remote server']);
95+
const parsed = {
96+
a: PreviewInfo.parse([3000, 'Local server']),
97+
b: PreviewInfo.parse([3000, 'Remote server']),
98+
};
99+
100+
const a = new PreviewInfo(parsed.a, new PortInfo(parsed.a.port));
101+
const b = new PreviewInfo(parsed.b, new PortInfo(parsed.b.port));
68102

69103
expect(PreviewInfo.equals(a, b)).toBe(false);
70104
});
71105

72106
it('should not be equal to another preview info with a different pathname', () => {
73-
const a = new PreviewInfo(3000);
74-
const b = new PreviewInfo(3000);
107+
const parsed = {
108+
a: PreviewInfo.parse(3000),
109+
b: PreviewInfo.parse('3000/b'),
110+
c: PreviewInfo.parse('3000/c'),
111+
};
75112

76-
a.pathname = '/foo';
113+
const a = new PreviewInfo(parsed.a, new PortInfo(parsed.a.port));
114+
const b = new PreviewInfo(parsed.b, new PortInfo(parsed.b.port));
115+
const c = new PreviewInfo(parsed.c, new PortInfo(parsed.c.port));
77116

78117
expect(PreviewInfo.equals(a, b)).toBe(false);
118+
expect(PreviewInfo.equals(b, c)).toBe(false);
79119
});
80120
});

0 commit comments

Comments
 (0)