Skip to content

Commit 00f514a

Browse files
committed
feat: sync new files from WebContainer to editor
1 parent 431145a commit 00f514a

File tree

12 files changed

+109
-23
lines changed

12 files changed

+109
-23
lines changed

e2e/src/components/ButtonWriteToFile.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ export function ButtonWriteToFile({ filePath, newContent, access = 'store', test
1616
case 'webcontainer': {
1717
const webcontainerInstance = await webcontainer;
1818

19+
const folderPath = filePath.split('/').slice(0, -1).join('/');
20+
21+
if (folderPath) {
22+
await webcontainerInstance.fs.mkdir(folderPath, { recursive: true });
23+
}
24+
1925
await webcontainerInstance.fs.writeFile(filePath, newContent);
2026

2127
return;

e2e/src/content/tutorial/tests/filesystem/no-watch/content.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ import { ButtonWriteToFile } from '@components/ButtonWriteToFile';
99
# Watch filesystem test
1010

1111
<ButtonWriteToFile client:load access="webcontainer" filePath="/bar.txt" newContent='Something else' />
12+
<ButtonWriteToFile client:load access="webcontainer" filePath="/src/new.txt" newContent='New' testId='write-new-file' />
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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
type: lesson
3+
title: Watch Glob
4+
focus: /bar.txt
5+
filesystem:
6+
watch: ['/*', '/a/**/*', '/src/**/*']
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' />
15+
<ButtonWriteToFile client:load access="webcontainer" filePath="/src/new.txt" newContent='New' testId='write-new-file' />
16+
<ButtonWriteToFile client:load access="webcontainer" filePath="/unknown/other.txt" newContent='Ignore this' testId='write-new-ignored-file' />

e2e/src/content/tutorial/tests/filesystem/watch/content.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ import { ButtonWriteToFile } from '@components/ButtonWriteToFile';
1212

1313
<ButtonWriteToFile client:load access="webcontainer" filePath="/bar.txt" newContent='Something else' />
1414
<ButtonWriteToFile client:load access="webcontainer" filePath="/a/b/baz.txt" newContent='Foo' testId='write-to-file-in-subfolder' />
15+
<ButtonWriteToFile client:load access="webcontainer" filePath="/unknown/other.txt" newContent='Ignore this' testId='write-new-ignored-file' />

e2e/test/filesystem.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ test('editor should reflect changes made from webcontainer', async ({ page }) =>
1717
});
1818
});
1919

20-
test('editor should reflect changes made from webcontainer in file in nested folder', async ({ page }) => {
20+
test('editor should reflect changes made from webcontainer in file in nested folder and not add new files', async ({ page }) => {
2121
const testCase = 'watch';
2222
await page.goto(`${BASE_URL}/${testCase}`);
2323

24+
await page.getByTestId('write-new-ignored-file').click();
2425
await page.getByRole('button', { name: 'baz.txt' }).click();
2526

2627
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Baz', {
@@ -32,6 +33,37 @@ test('editor should reflect changes made from webcontainer in file in nested fol
3233
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Foo', {
3334
useInnerText: true,
3435
});
36+
expect(await page.getByRole('button', { name: 'other.txt' }).count()).toEqual(0);
37+
});
38+
39+
test('editor should reflect changes made from webcontainer in specified paths', async ({ page }) => {
40+
const testCase = 'watch-glob';
41+
await page.goto(`${BASE_URL}/${testCase}`);
42+
43+
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Initial content\n', {
44+
useInnerText: true,
45+
});
46+
47+
await page.getByTestId('write-to-file').click();
48+
49+
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Something else', {
50+
useInnerText: true,
51+
});
52+
});
53+
54+
test('editor should reflect new files added in specified paths in webcontainer', async ({ page }) => {
55+
const testCase = 'watch-glob';
56+
await page.goto(`${BASE_URL}/${testCase}`);
57+
58+
await page.getByTestId('write-new-ignored-file').click();
59+
await page.getByTestId('write-new-file').click();
60+
61+
await page.getByRole('button', { name: 'new.txt' }).click();
62+
expect(await page.getByRole('button', { name: 'other.txt' }).count()).toEqual(0);
63+
64+
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('New', {
65+
useInnerText: true,
66+
});
3567
});
3668

3769
test('editor should not reflect changes made from webcontainer if watch is not set', async ({ page }) => {

packages/runtime/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@
3535
"dependencies": {
3636
"@tutorialkit/types": "workspace:*",
3737
"@webcontainer/api": "1.2.4",
38-
"nanostores": "^0.10.3"
38+
"nanostores": "^0.10.3",
39+
"picomatch": "^4.0.2"
3940
},
4041
"devDependencies": {
42+
"@types/picomatch": "^3.0.1",
4143
"typescript": "^5.4.5",
4244
"vite": "^5.3.1",
4345
"vite-tsconfig-paths": "^4.3.2",

packages/runtime/src/store/editor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ export class EditorStore {
9999

100100
addFileOrFolder(file: FileDescriptor) {
101101
// when adding file or folder to empty folder, remove the empty folder from documents
102-
const emptyFolder = this.files.get().find((f) => f.type === 'folder' && file.path.startsWith(f.path));
102+
const emptyFolder = this.files.get().find((f) => file.path.startsWith(f.path));
103103

104-
if (emptyFolder && emptyFolder.type === 'folder') {
104+
if (emptyFolder) {
105105
this.documents.setKey(emptyFolder.path, undefined);
106106
}
107107

packages/runtime/src/store/tutorial-runner.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { CommandsSchema, Files } from '@tutorialkit/types';
22
import type { IFSWatcher, WebContainer, WebContainerProcess } from '@webcontainer/api';
3+
import picomatch from 'picomatch';
34
import { newTask, type Task, type TaskCancelled } from '../tasks.js';
45
import { MultiCounter } from '../utils/multi-counter.js';
56
import { clearTerminal, escapeCodes, type ITerminal } from '../utils/terminal.js';
@@ -65,7 +66,7 @@ export class TutorialRunner {
6566

6667
private _ignoreFileEvents = new MultiCounter();
6768
private _watcher: IFSWatcher | undefined;
68-
private _watchContentFromWebContainer = false;
69+
private _watchContentFromWebContainer: string[] | boolean = false;
6970
private _readyToWatch = false;
7071

7172
private _packageJsonDirty = false;
@@ -82,7 +83,7 @@ export class TutorialRunner {
8283
private _stepController: StepsController,
8384
) {}
8485

85-
setWatchFromWebContainer(value: boolean) {
86+
setWatchFromWebContainer(value: boolean | string[]) {
8687
this._watchContentFromWebContainer = value;
8788

8889
if (this._readyToWatch && this._watchContentFromWebContainer) {
@@ -654,19 +655,39 @@ export class TutorialRunner {
654655
return;
655656
}
656657

657-
// for now we only care about 'change' event
658-
if (eventType !== 'change') {
658+
if (
659+
Array.isArray(this._watchContentFromWebContainer) &&
660+
!this._watchContentFromWebContainer.some((pattern) => picomatch.isMatch(filePath, pattern))
661+
) {
659662
return;
660663
}
661664

662-
// we ignore all paths that aren't exposed in the `_editorStore`
663-
const file = this._editorStore.documents.get()[filePath];
665+
if (eventType === 'change') {
666+
// we ignore all paths that aren't exposed in the `_editorStore`
667+
const file = this._editorStore.documents.get()[filePath];
664668

665-
if (!file) {
666-
return;
667-
}
669+
if (!file) {
670+
return;
671+
}
672+
673+
scheduleReadFor(filePath, typeof file.value === 'string' ? 'utf-8' : null);
674+
} else if (eventType === 'rename' && Array.isArray(this._watchContentFromWebContainer)) {
675+
const segments = filePath.split('/');
676+
segments.forEach((_, index) => {
677+
if (index == segments.length - 1) {
678+
return;
679+
}
668680

669-
scheduleReadFor(filePath, typeof file.value === 'string' ? 'utf-8' : null);
681+
const folderPath = segments.slice(0, index + 1).join('/');
682+
683+
if (!this._editorStore.documents.get()[folderPath]) {
684+
this._editorStore.addFileOrFolder({ path: folderPath, type: 'folder' });
685+
}
686+
});
687+
this._editorStore.addFileOrFolder({ path: filePath, type: 'file' });
688+
this._updateCurrentFiles({ [filePath]: 'test' });
689+
scheduleReadFor(filePath, 'utf-8');
690+
}
670691
});
671692
}
672693

0 commit comments

Comments
 (0)