Skip to content

Commit b920c77

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

File tree

8 files changed

+66
-18
lines changed

8 files changed

+66
-18
lines changed

e2e/src/components/ButtonWriteToFile.tsx

+6
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

+1
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' />

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

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ type: lesson
33
title: Watch
44
focus: /bar.txt
55
filesystem:
6+
addNewFilesInPaths: ['src']
67
watch: true
78
---
89

@@ -12,3 +13,5 @@ import { ButtonWriteToFile } from '@components/ButtonWriteToFile';
1213

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

e2e/test/filesystem.test.ts

+15
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@ test('editor should reflect changes made from webcontainer in file in nested fol
3434
});
3535
});
3636

37+
test('editor should reflect new files added in specified paths in webcontainer', async ({ page }) => {
38+
const testCase = 'watch';
39+
await page.goto(`${BASE_URL}/${testCase}`);
40+
41+
await page.getByTestId('write-new-ignored-file').click();
42+
await page.getByTestId('write-new-file').click();
43+
44+
await page.getByRole('button', { name: 'new.txt' }).click();
45+
expect(await page.getByRole('button', { name: 'other.txt' }).count()).toEqual(0);
46+
47+
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('New', {
48+
useInnerText: true,
49+
});
50+
});
51+
3752
test('editor should not reflect changes made from webcontainer if watch is not set', async ({ page }) => {
3853
const testCase = 'no-watch';
3954
await page.goto(`${BASE_URL}/${testCase}`);

packages/runtime/src/store/editor.ts

+2-2
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/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export class TutorialStore {
151151
return;
152152
}
153153

154-
this._runner.setWatchFromWebContainer(lesson.data.filesystem?.watch ?? false);
154+
this._runner.setWatchFromWebContainer(lesson.data.filesystem);
155155

156156
this._lessonTask = newTask(
157157
async (signal) => {

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

+32-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CommandsSchema, Files } from '@tutorialkit/types';
1+
import type { CommandsSchema, Files, FileSystemSchema } from '@tutorialkit/types';
22
import type { IFSWatcher, WebContainer, WebContainerProcess } from '@webcontainer/api';
33
import { newTask, type Task, type TaskCancelled } from '../tasks.js';
44
import { MultiCounter } from '../utils/multi-counter.js';
@@ -66,6 +66,7 @@ export class TutorialRunner {
6666
private _ignoreFileEvents = new MultiCounter();
6767
private _watcher: IFSWatcher | undefined;
6868
private _watchContentFromWebContainer = false;
69+
private _watcherAddNewFilesInPaths: string[] | undefined = undefined;
6970
private _readyToWatch = false;
7071

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

85-
setWatchFromWebContainer(value: boolean) {
86-
this._watchContentFromWebContainer = value;
86+
setWatchFromWebContainer(fileSystemConfig?: FileSystemSchema) {
87+
this._watchContentFromWebContainer = fileSystemConfig?.watch || false;
88+
this._watcherAddNewFilesInPaths = fileSystemConfig?.addNewFilesInPaths;
8789

8890
if (this._readyToWatch && this._watchContentFromWebContainer) {
8991
this._webcontainer.then((webcontainer) => this._setupWatcher(webcontainer));
@@ -654,19 +656,36 @@ export class TutorialRunner {
654656
return;
655657
}
656658

657-
// for now we only care about 'change' event
658-
if (eventType !== 'change') {
659-
return;
660-
}
659+
if (eventType === 'change') {
660+
// we ignore all paths that aren't exposed in the `_editorStore`
661+
const file = this._editorStore.documents.get()[filePath];
661662

662-
// we ignore all paths that aren't exposed in the `_editorStore`
663-
const file = this._editorStore.documents.get()[filePath];
663+
if (!file) {
664+
return;
665+
}
664666

665-
if (!file) {
666-
return;
667-
}
667+
scheduleReadFor(filePath, typeof file.value === 'string' ? 'utf-8' : null);
668+
} else if (eventType === 'rename' && this._watcherAddNewFilesInPaths) {
669+
if (!this._watcherAddNewFilesInPaths.some((path) => filePath.startsWith('/' + path))) {
670+
return;
671+
}
668672

669-
scheduleReadFor(filePath, typeof file.value === 'string' ? 'utf-8' : null);
673+
const segments = filePath.split('/');
674+
segments.forEach((_, index) => {
675+
if (index == segments.length - 1) {
676+
return;
677+
}
678+
679+
const folderPath = segments.slice(0, index + 1).join('/');
680+
681+
if (!this._editorStore.documents.get()[folderPath]) {
682+
this._editorStore.addFileOrFolder({ path: folderPath, type: 'folder' });
683+
}
684+
});
685+
this._editorStore.addFileOrFolder({ path: filePath, type: 'file' });
686+
this._updateCurrentFiles({ [filePath]: 'test' });
687+
scheduleReadFor(filePath, 'utf-8');
688+
}
670689
});
671690
}
672691

packages/types/src/schemas/common.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,14 @@ export const previewSchema = z.union([
5757
export type PreviewSchema = z.infer<typeof previewSchema>;
5858

5959
export const fileSystemSchema = z.object({
60+
addNewFilesInPaths: z
61+
.array(z.string())
62+
.describe('Can specify an array of file paths to watch for new files')
63+
.optional(),
6064
watch: z
6165
.boolean()
62-
.optional()
63-
.describe('When set to true, file changes in WebContainer are updated in the editor as well.'),
66+
.describe('When set to true, file changes in WebContainer are updated in the editor as well.')
67+
.optional(),
6468
});
6569

6670
export type FileSystemSchema = z.infer<typeof fileSystemSchema>;

0 commit comments

Comments
 (0)