Skip to content

Commit f256113

Browse files
committed
chore: account for file deletion
1 parent 67d585d commit f256113

File tree

7 files changed

+110
-18
lines changed

7 files changed

+110
-18
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { webcontainer } from 'tutorialkit:core';
2+
3+
interface Props {
4+
filePath: string;
5+
newContent: string;
6+
7+
// default to 'store'
8+
access?: 'store' | 'webcontainer';
9+
testId?: string;
10+
}
11+
12+
export function ButtonDeleteFile({ filePath, access = 'webcontainer', testId = 'delete-file' }: Props) {
13+
async function deleteFile() {
14+
switch (access) {
15+
case 'webcontainer': {
16+
const webcontainerInstance = await webcontainer;
17+
18+
await webcontainerInstance.fs.rm(filePath);
19+
20+
return;
21+
}
22+
case 'store': {
23+
throw new Error('Delete from store not implemented');
24+
return;
25+
}
26+
}
27+
}
28+
29+
return (
30+
<button data-testid={testId} onClick={deleteFile}>
31+
Delete File
32+
</button>
33+
);
34+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ type: lesson
33
title: Watch Glob
44
focus: /bar.txt
55
filesystem:
6-
watch: ['/*', '/a/**/*', '/src/**/*']
6+
watch: ['/*.txt', '/a/**/*', '/src/**/*']
77
---
88

99
import { ButtonWriteToFile } from '@components/ButtonWriteToFile';
10+
import { ButtonDeleteFile } from '@components/ButtonDeleteFile';
1011

1112
# Watch filesystem test
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' />
1516
<ButtonWriteToFile client:load access="webcontainer" filePath="/src/new.txt" newContent='New' testId='write-new-file' />
1617
<ButtonWriteToFile client:load access="webcontainer" filePath="/unknown/other.txt" newContent='Ignore this' testId='write-new-ignored-file' />
18+
19+
<ButtonDeleteFile client:load access="webcontainer" filePath="/bar.txt" testId='delete-file' />

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ filesystem:
77
---
88

99
import { ButtonWriteToFile } from '@components/ButtonWriteToFile';
10+
import { ButtonDeleteFile } from '@components/ButtonDeleteFile';
1011

1112
# Watch filesystem test
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' />
1516
<ButtonWriteToFile client:load access="webcontainer" filePath="/unknown/other.txt" newContent='Ignore this' testId='write-new-ignored-file' />
17+
18+
<ButtonDeleteFile client:load access="webcontainer" filePath="/bar.txt" testId='delete-file' />

e2e/test/filesystem.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ test('editor should reflect changes made from webcontainer in file in nested fol
2121
const testCase = 'watch';
2222
await page.goto(`${BASE_URL}/${testCase}`);
2323

24+
// set up actions that shouldn't do anything
2425
await page.getByTestId('write-new-ignored-file').click();
26+
await page.getByTestId('delete-file').click();
27+
2528
await page.getByRole('button', { name: 'baz.txt' }).click();
2629

2730
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Baz', {
@@ -33,7 +36,10 @@ test('editor should reflect changes made from webcontainer in file in nested fol
3336
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Foo', {
3437
useInnerText: true,
3538
});
39+
40+
// test that ignored actions are ignored
3641
expect(await page.getByRole('button', { name: 'other.txt' }).count()).toEqual(0);
42+
expect(await page.getByRole('button', { name: 'bar.txt' }).count()).toEqual(1);
3743
});
3844

3945
test('editor should reflect changes made from webcontainer in specified paths', async ({ page }) => {
@@ -59,13 +65,27 @@ test('editor should reflect new files added in specified paths in webcontainer',
5965
await page.getByTestId('write-new-file').click();
6066

6167
await page.getByRole('button', { name: 'new.txt' }).click();
62-
expect(await page.getByRole('button', { name: 'other.txt' }).count()).toEqual(0);
68+
await expect(async () => {
69+
expect(await page.getByRole('button', { name: 'unknown' }).count()).toEqual(0);
70+
expect(await page.getByRole('button', { name: 'other.txt' }).count()).toEqual(0);
71+
}).toPass();
6372

6473
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('New', {
6574
useInnerText: true,
6675
});
6776
});
6877

78+
test('editor should remove deleted files in specified paths in webcontainer', async ({ page }) => {
79+
const testCase = 'watch-glob';
80+
await page.goto(`${BASE_URL}/${testCase}`);
81+
82+
await page.getByTestId('delete-file').click();
83+
84+
await expect(async () => {
85+
expect(await page.getByRole('button', { name: 'bar.txt' }).count()).toEqual(0);
86+
}).toPass();
87+
});
88+
6989
test('editor should not reflect changes made from webcontainer if watch is not set', async ({ page }) => {
7090
const testCase = 'no-watch';
7191
await page.goto(`${BASE_URL}/${testCase}`);

packages/runtime/src/store/editor.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,18 @@ export class EditorStore {
133133
return contentChanged;
134134
}
135135

136+
deleteFile(filePath: string): boolean {
137+
const documentState = this.documents.get()[filePath];
138+
139+
if (!documentState) {
140+
return false;
141+
}
142+
143+
this.documents.setKey(filePath, undefined);
144+
145+
return true;
146+
}
147+
136148
onDocumentChanged(filePath: string, callback: (document: Readonly<EditorDocument>) => void) {
137149
const unsubscribeFromCurrentDocument = this.currentDocument.subscribe((document) => {
138150
if (document?.filePath === filePath) {

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

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { CommandsSchema, Files } from '@tutorialkit/types';
22
import type { IFSWatcher, WebContainer, WebContainerProcess } from '@webcontainer/api';
3-
import picomatch from 'picomatch';
3+
import picomatch from 'picomatch/posix.js';
44
import { newTask, type Task, type TaskCancelled } from '../tasks.js';
55
import { MultiCounter } from '../utils/multi-counter.js';
66
import { clearTerminal, escapeCodes, type ITerminal } from '../utils/terminal.js';
@@ -641,6 +641,23 @@ export class TutorialRunner {
641641
* cleanup the allocated buffers.
642642
*/
643643
const scheduleReadFor = (filePath: string, encoding: 'utf-8' | null) => {
644+
const segments = filePath.split('/');
645+
segments.forEach((_, index) => {
646+
if (index == segments.length - 1) {
647+
return;
648+
}
649+
650+
const folderPath = segments.slice(0, index + 1).join('/');
651+
652+
if (!this._editorStore.documents.get()[folderPath]) {
653+
this._editorStore.addFileOrFolder({ path: folderPath, type: 'folder' });
654+
}
655+
});
656+
657+
if (!this._editorStore.documents.get()[filePath]) {
658+
this._editorStore.addFileOrFolder({ path: filePath, type: 'file' });
659+
}
660+
644661
filesToRead.set(filePath, encoding);
645662

646663
clearTimeout(timeoutId);
@@ -663,7 +680,10 @@ export class TutorialRunner {
663680
}
664681

665682
if (eventType === 'change') {
666-
// we ignore all paths that aren't exposed in the `_editorStore`
683+
/**
684+
* Update file
685+
* we ignore all paths that aren't exposed in the `_editorStore`
686+
*/
667687
const file = this._editorStore.documents.get()[filePath];
668688

669689
if (!file) {
@@ -672,21 +692,16 @@ export class TutorialRunner {
672692

673693
scheduleReadFor(filePath, typeof file.value === 'string' ? 'utf-8' : null);
674694
} 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-
}
680-
681-
const folderPath = segments.slice(0, index + 1).join('/');
695+
const file = this._editorStore.documents.get()[filePath];
682696

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]: '' });
689-
scheduleReadFor(filePath, 'utf-8');
697+
if (file) {
698+
// remove file
699+
this._editorStore.deleteFile(filePath);
700+
} else {
701+
// add file
702+
this._updateCurrentFiles({ [filePath]: '' });
703+
scheduleReadFor(filePath, 'utf-8');
704+
}
690705
}
691706
});
692707
}

packages/runtime/src/types.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
/// <reference types="vite/client" />
2+
3+
// https://github.com/micromatch/picomatch?tab=readme-ov-file#api
4+
declare module 'picomatch/posix.js' {
5+
export { default } from 'picomatch';
6+
}

0 commit comments

Comments
 (0)