Skip to content

Commit 5c1de69

Browse files
authored
feat: sync files from WebContainer to editor (#334)
1 parent c1a59f5 commit 5c1de69

File tree

20 files changed

+337
-34
lines changed

20 files changed

+337
-34
lines changed

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

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,35 @@ type Command = string
260260
261261
```
262262

263+
##### `filesystem`
264+
Configures how changes such as files being modified or added in WebContainer should be reflected in the editor when they weren't caused by the user directly. By default, the editor will not reflect these changes.
265+
266+
An example use case is when a user runs a command that modifies a file. For instance when a `package.json` is modified by doing an `npm install <xyz>`. If `watch` is set to `true`, the file will be updated in the editor. If set to `false`, the file will not be updated.
267+
268+
This property is by default set to `false` as it can impact performance. If you are creating a lesson where the user is expected to modify files outside the editor, you may want to keep this to `false`.
269+
270+
<PropertyTable inherited type={'FileSystem'} />
271+
272+
The `FileSystem` type has the following shape:
273+
274+
```ts
275+
type FileSystem = {
276+
watch: boolean
277+
}
278+
279+
```
280+
281+
Example values:
282+
283+
```yaml
284+
filesystem:
285+
watch: true # Filesystem changes are reflected in the editor
286+
287+
filesystem:
288+
watch: false # Or if it's omitted, the default value is false
289+
```
290+
291+
263292
##### `terminal`
264293
Configures one or more terminals. TutorialKit provides two types of terminals: read-only, called `output`, and interactive, called `terminal`. Note, that there can be only one `output` terminal.
265294

@@ -319,7 +348,7 @@ Navigating to a lesson that specifies `autoReload` will always reload the previe
319348
Specifies which folder from the `src/templates/` directory should be used as the basis for the code. See the "[Code templates](/guides/creating-content/#code-templates)" guide for a detailed explainer.
320349
<PropertyTable inherited type="string" />
321350

322-
#### `editPageLink`
351+
##### `editPageLink`
323352
Display a link in lesson for editing the page content.
324353
The value is a URL pattern where `${path}` is replaced with the lesson's location relative to the `src/content/tutorial`.
325354

@@ -346,7 +375,7 @@ You can instruct Github to show the source code instead by adding `plain=1` quer
346375

347376
:::
348377

349-
### `openInStackBlitz`
378+
##### `openInStackBlitz`
350379
Display a link for opening current lesson in StackBlitz.
351380
<PropertyTable inherited type="OpenInStackBlitz" />
352381

e2e/src/components/ButtonWriteToFile.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
import tutorialStore from 'tutorialkit:store';
2+
import { webcontainer } from 'tutorialkit:core';
23

34
interface Props {
45
filePath: string;
56
newContent: string;
7+
8+
// default to 'store'
9+
access?: 'store' | 'webcontainer';
610
testId?: string;
711
}
812

9-
export function ButtonWriteToFile({ filePath, newContent, testId = 'write-to-file' }: Props) {
13+
export function ButtonWriteToFile({ filePath, newContent, access = 'store', testId = 'write-to-file' }: Props) {
1014
async function writeFile() {
11-
await new Promise<void>((resolve) => {
12-
tutorialStore.lessonFullyLoaded.subscribe((value) => {
13-
if (value) {
14-
resolve();
15-
}
16-
});
17-
});
15+
switch (access) {
16+
case 'webcontainer': {
17+
const webcontainerInstance = await webcontainer;
18+
19+
await webcontainerInstance.fs.writeFile(filePath, newContent);
1820

19-
tutorialStore.updateFile(filePath, newContent);
21+
return;
22+
}
23+
case 'store': {
24+
tutorialStore.updateFile(filePath, newContent);
25+
return;
26+
}
27+
}
2028
}
2129

2230
return (
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
type: chapter
3+
title: filesystem
4+
---
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Initial content
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
type: lesson
3+
title: No watch
4+
focus: /bar.txt
5+
---
6+
7+
import { ButtonWriteToFile } from '@components/ButtonWriteToFile';
8+
9+
# Watch filesystem test
10+
11+
<ButtonWriteToFile client:load access="webcontainer" filePath="/bar.txt" newContent='Something else' />
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Baz
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Initial content
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
type: lesson
3+
title: Watch
4+
focus: /bar.txt
5+
filesystem:
6+
watch: true
7+
---
8+
9+
import { ButtonWriteToFile } from '@components/ButtonWriteToFile';
10+
11+
# Watch filesystem test
12+
13+
<ButtonWriteToFile client:load access="webcontainer" filePath="/bar.txt" newContent='Something else' />
14+
<ButtonWriteToFile client:load access="webcontainer" filePath="/a/b/baz.txt" newContent='Foo' testId='write-to-file-in-subfolder' />

e2e/test/filesystem.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
const BASE_URL = '/tests/filesystem';
4+
5+
test('editor should reflect changes made from webcontainer', async ({ page }) => {
6+
const testCase = 'watch';
7+
await page.goto(`${BASE_URL}/${testCase}`);
8+
9+
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Initial content\n', {
10+
useInnerText: true,
11+
});
12+
13+
await page.getByTestId('write-to-file').click();
14+
15+
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Something else', {
16+
useInnerText: true,
17+
});
18+
});
19+
20+
test('editor should reflect changes made from webcontainer in file in nested folder', async ({ page }) => {
21+
const testCase = 'watch';
22+
await page.goto(`${BASE_URL}/${testCase}`);
23+
24+
await page.getByRole('button', { name: 'baz.txt' }).click();
25+
26+
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Baz', {
27+
useInnerText: true,
28+
});
29+
30+
await page.getByTestId('write-to-file-in-subfolder').click();
31+
32+
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Foo', {
33+
useInnerText: true,
34+
});
35+
});
36+
37+
test('editor should not reflect changes made from webcontainer if watch is not set', async ({ page }) => {
38+
const testCase = 'no-watch';
39+
await page.goto(`${BASE_URL}/${testCase}`);
40+
41+
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Initial content\n', {
42+
useInnerText: true,
43+
});
44+
45+
await page.getByTestId('write-to-file').click();
46+
47+
await page.waitForTimeout(1_000);
48+
49+
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Initial content\n', {
50+
useInnerText: true,
51+
});
52+
});

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ export async function getTutorial(): Promise<Tutorial> {
247247
'i18n',
248248
'editPageLink',
249249
'openInStackBlitz',
250+
'filesystem',
250251
],
251252
),
252253
};

0 commit comments

Comments
 (0)