From 00f514a747f8fd3370dc6b932ef31aabac2c80a1 Mon Sep 17 00:00:00 2001 From: Isaac Mann Date: Tue, 29 Oct 2024 15:01:09 -0400 Subject: [PATCH 01/10] feat: sync new files from WebContainer to editor --- e2e/src/components/ButtonWriteToFile.tsx | 6 +++ .../tests/filesystem/no-watch/content.mdx | 1 + .../filesystem/watch-glob/_files/a/b/baz.txt | 1 + .../filesystem/watch-glob/_files/bar.txt | 1 + .../tests/filesystem/watch-glob/content.mdx | 16 ++++++++ .../tests/filesystem/watch/content.mdx | 1 + e2e/test/filesystem.test.ts | 34 ++++++++++++++- packages/runtime/package.json | 4 +- packages/runtime/src/store/editor.ts | 4 +- packages/runtime/src/store/tutorial-runner.ts | 41 ++++++++++++++----- packages/types/src/schemas/common.ts | 8 ++-- pnpm-lock.yaml | 15 ++++--- 12 files changed, 109 insertions(+), 23 deletions(-) create mode 100644 e2e/src/content/tutorial/tests/filesystem/watch-glob/_files/a/b/baz.txt create mode 100644 e2e/src/content/tutorial/tests/filesystem/watch-glob/_files/bar.txt create mode 100644 e2e/src/content/tutorial/tests/filesystem/watch-glob/content.mdx diff --git a/e2e/src/components/ButtonWriteToFile.tsx b/e2e/src/components/ButtonWriteToFile.tsx index e0403e43a..3d41c38d6 100644 --- a/e2e/src/components/ButtonWriteToFile.tsx +++ b/e2e/src/components/ButtonWriteToFile.tsx @@ -16,6 +16,12 @@ export function ButtonWriteToFile({ filePath, newContent, access = 'store', test case 'webcontainer': { const webcontainerInstance = await webcontainer; + const folderPath = filePath.split('/').slice(0, -1).join('/'); + + if (folderPath) { + await webcontainerInstance.fs.mkdir(folderPath, { recursive: true }); + } + await webcontainerInstance.fs.writeFile(filePath, newContent); return; diff --git a/e2e/src/content/tutorial/tests/filesystem/no-watch/content.mdx b/e2e/src/content/tutorial/tests/filesystem/no-watch/content.mdx index aeac65eb4..2ed52b5fb 100644 --- a/e2e/src/content/tutorial/tests/filesystem/no-watch/content.mdx +++ b/e2e/src/content/tutorial/tests/filesystem/no-watch/content.mdx @@ -9,3 +9,4 @@ import { ButtonWriteToFile } from '@components/ButtonWriteToFile'; # Watch filesystem test + diff --git a/e2e/src/content/tutorial/tests/filesystem/watch-glob/_files/a/b/baz.txt b/e2e/src/content/tutorial/tests/filesystem/watch-glob/_files/a/b/baz.txt new file mode 100644 index 000000000..a68818270 --- /dev/null +++ b/e2e/src/content/tutorial/tests/filesystem/watch-glob/_files/a/b/baz.txt @@ -0,0 +1 @@ +Baz diff --git a/e2e/src/content/tutorial/tests/filesystem/watch-glob/_files/bar.txt b/e2e/src/content/tutorial/tests/filesystem/watch-glob/_files/bar.txt new file mode 100644 index 000000000..8430408a5 --- /dev/null +++ b/e2e/src/content/tutorial/tests/filesystem/watch-glob/_files/bar.txt @@ -0,0 +1 @@ +Initial content diff --git a/e2e/src/content/tutorial/tests/filesystem/watch-glob/content.mdx b/e2e/src/content/tutorial/tests/filesystem/watch-glob/content.mdx new file mode 100644 index 000000000..66b771ce6 --- /dev/null +++ b/e2e/src/content/tutorial/tests/filesystem/watch-glob/content.mdx @@ -0,0 +1,16 @@ +--- +type: lesson +title: Watch Glob +focus: /bar.txt +filesystem: + watch: ['/*', '/a/**/*', '/src/**/*'] +--- + +import { ButtonWriteToFile } from '@components/ButtonWriteToFile'; + +# Watch filesystem test + + + + + diff --git a/e2e/src/content/tutorial/tests/filesystem/watch/content.mdx b/e2e/src/content/tutorial/tests/filesystem/watch/content.mdx index 46a0ed3b7..797287805 100644 --- a/e2e/src/content/tutorial/tests/filesystem/watch/content.mdx +++ b/e2e/src/content/tutorial/tests/filesystem/watch/content.mdx @@ -12,3 +12,4 @@ import { ButtonWriteToFile } from '@components/ButtonWriteToFile'; + diff --git a/e2e/test/filesystem.test.ts b/e2e/test/filesystem.test.ts index be14fef7f..046c3a39e 100644 --- a/e2e/test/filesystem.test.ts +++ b/e2e/test/filesystem.test.ts @@ -17,10 +17,11 @@ test('editor should reflect changes made from webcontainer', async ({ page }) => }); }); -test('editor should reflect changes made from webcontainer in file in nested folder', async ({ page }) => { +test('editor should reflect changes made from webcontainer in file in nested folder and not add new files', async ({ page }) => { const testCase = 'watch'; await page.goto(`${BASE_URL}/${testCase}`); + await page.getByTestId('write-new-ignored-file').click(); await page.getByRole('button', { name: 'baz.txt' }).click(); 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 await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Foo', { useInnerText: true, }); + expect(await page.getByRole('button', { name: 'other.txt' }).count()).toEqual(0); +}); + +test('editor should reflect changes made from webcontainer in specified paths', async ({ page }) => { + const testCase = 'watch-glob'; + await page.goto(`${BASE_URL}/${testCase}`); + + await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Initial content\n', { + useInnerText: true, + }); + + await page.getByTestId('write-to-file').click(); + + await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Something else', { + useInnerText: true, + }); +}); + +test('editor should reflect new files added in specified paths in webcontainer', async ({ page }) => { + const testCase = 'watch-glob'; + await page.goto(`${BASE_URL}/${testCase}`); + + await page.getByTestId('write-new-ignored-file').click(); + await page.getByTestId('write-new-file').click(); + + await page.getByRole('button', { name: 'new.txt' }).click(); + expect(await page.getByRole('button', { name: 'other.txt' }).count()).toEqual(0); + + await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('New', { + useInnerText: true, + }); }); test('editor should not reflect changes made from webcontainer if watch is not set', async ({ page }) => { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index ad984f28a..d28c5649a 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -35,9 +35,11 @@ "dependencies": { "@tutorialkit/types": "workspace:*", "@webcontainer/api": "1.2.4", - "nanostores": "^0.10.3" + "nanostores": "^0.10.3", + "picomatch": "^4.0.2" }, "devDependencies": { + "@types/picomatch": "^3.0.1", "typescript": "^5.4.5", "vite": "^5.3.1", "vite-tsconfig-paths": "^4.3.2", diff --git a/packages/runtime/src/store/editor.ts b/packages/runtime/src/store/editor.ts index a0f305d63..5fb2af52f 100644 --- a/packages/runtime/src/store/editor.ts +++ b/packages/runtime/src/store/editor.ts @@ -99,9 +99,9 @@ export class EditorStore { addFileOrFolder(file: FileDescriptor) { // when adding file or folder to empty folder, remove the empty folder from documents - const emptyFolder = this.files.get().find((f) => f.type === 'folder' && file.path.startsWith(f.path)); + const emptyFolder = this.files.get().find((f) => file.path.startsWith(f.path)); - if (emptyFolder && emptyFolder.type === 'folder') { + if (emptyFolder) { this.documents.setKey(emptyFolder.path, undefined); } diff --git a/packages/runtime/src/store/tutorial-runner.ts b/packages/runtime/src/store/tutorial-runner.ts index e1df25d7c..4c32a9219 100644 --- a/packages/runtime/src/store/tutorial-runner.ts +++ b/packages/runtime/src/store/tutorial-runner.ts @@ -1,5 +1,6 @@ import type { CommandsSchema, Files } from '@tutorialkit/types'; import type { IFSWatcher, WebContainer, WebContainerProcess } from '@webcontainer/api'; +import picomatch from 'picomatch'; import { newTask, type Task, type TaskCancelled } from '../tasks.js'; import { MultiCounter } from '../utils/multi-counter.js'; import { clearTerminal, escapeCodes, type ITerminal } from '../utils/terminal.js'; @@ -65,7 +66,7 @@ export class TutorialRunner { private _ignoreFileEvents = new MultiCounter(); private _watcher: IFSWatcher | undefined; - private _watchContentFromWebContainer = false; + private _watchContentFromWebContainer: string[] | boolean = false; private _readyToWatch = false; private _packageJsonDirty = false; @@ -82,7 +83,7 @@ export class TutorialRunner { private _stepController: StepsController, ) {} - setWatchFromWebContainer(value: boolean) { + setWatchFromWebContainer(value: boolean | string[]) { this._watchContentFromWebContainer = value; if (this._readyToWatch && this._watchContentFromWebContainer) { @@ -654,19 +655,39 @@ export class TutorialRunner { return; } - // for now we only care about 'change' event - if (eventType !== 'change') { + if ( + Array.isArray(this._watchContentFromWebContainer) && + !this._watchContentFromWebContainer.some((pattern) => picomatch.isMatch(filePath, pattern)) + ) { return; } - // we ignore all paths that aren't exposed in the `_editorStore` - const file = this._editorStore.documents.get()[filePath]; + if (eventType === 'change') { + // we ignore all paths that aren't exposed in the `_editorStore` + const file = this._editorStore.documents.get()[filePath]; - if (!file) { - return; - } + if (!file) { + return; + } + + scheduleReadFor(filePath, typeof file.value === 'string' ? 'utf-8' : null); + } else if (eventType === 'rename' && Array.isArray(this._watchContentFromWebContainer)) { + const segments = filePath.split('/'); + segments.forEach((_, index) => { + if (index == segments.length - 1) { + return; + } - scheduleReadFor(filePath, typeof file.value === 'string' ? 'utf-8' : null); + const folderPath = segments.slice(0, index + 1).join('/'); + + if (!this._editorStore.documents.get()[folderPath]) { + this._editorStore.addFileOrFolder({ path: folderPath, type: 'folder' }); + } + }); + this._editorStore.addFileOrFolder({ path: filePath, type: 'file' }); + this._updateCurrentFiles({ [filePath]: 'test' }); + scheduleReadFor(filePath, 'utf-8'); + } }); } diff --git a/packages/types/src/schemas/common.ts b/packages/types/src/schemas/common.ts index 1cabc31b2..4f3972a40 100644 --- a/packages/types/src/schemas/common.ts +++ b/packages/types/src/schemas/common.ts @@ -58,9 +58,11 @@ export type PreviewSchema = z.infer; export const fileSystemSchema = z.object({ watch: z - .boolean() - .optional() - .describe('When set to true, file changes in WebContainer are updated in the editor as well.'), + .union([z.boolean(), z.array(z.string())]) + .describe( + 'When set to true, file changes in WebContainer are updated in the editor as well. When set to an array, file changes or new files in the matching paths are updated in the editor.', + ) + .optional(), }); export type FileSystemSchema = z.infer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 152a529cf..46bfbeabb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -627,7 +627,13 @@ importers: nanostores: specifier: ^0.10.3 version: 0.10.3 + picomatch: + specifier: ^4.0.2 + version: 4.0.2 devDependencies: + '@types/picomatch': + specifier: ^3.0.1 + version: 3.0.1 typescript: specifier: ^5.4.5 version: 5.5.3 @@ -4012,7 +4018,7 @@ packages: '@unocss/core': 0.59.4 '@unocss/reset': 0.59.4 '@unocss/vite': 0.59.4(vite@5.4.2) - vite: 5.4.2(@types/node@22.4.2) + vite: 5.4.2(@types/node@22.4.2)(sass@1.77.6) transitivePeerDependencies: - rollup @@ -4211,7 +4217,7 @@ packages: chokidar: 3.6.0 fast-glob: 3.3.2 magic-string: 0.30.11 - vite: 5.4.2(@types/node@22.4.2) + vite: 5.4.2(@types/node@22.4.2)(sass@1.77.6) transitivePeerDependencies: - rollup @@ -6742,7 +6748,6 @@ packages: /immutable@4.3.6: resolution: {integrity: sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==} - dev: true /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} @@ -8835,7 +8840,6 @@ packages: chokidar: 3.6.0 immutable: 4.3.6 source-map-js: 1.2.0 - dev: true /sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} @@ -9644,7 +9648,7 @@ packages: '@unocss/transformer-directives': 0.59.4 '@unocss/transformer-variant-group': 0.59.4 '@unocss/vite': 0.59.4(vite@5.4.2) - vite: 5.4.2(@types/node@22.4.2) + vite: 5.4.2(@types/node@22.4.2)(sass@1.77.6) transitivePeerDependencies: - postcss - rollup @@ -9990,7 +9994,6 @@ packages: sass: 1.77.6 optionalDependencies: fsevents: 2.3.3 - dev: true /vitefu@0.2.5(vite@5.4.2): resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} From 67d585de3a8d7bfee9385b1158af725bacd61aa6 Mon Sep 17 00:00:00 2001 From: Isaac Mann Date: Tue, 29 Oct 2024 15:54:55 -0400 Subject: [PATCH 02/10] chore: fixes from review --- packages/runtime/src/store/tutorial-runner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/src/store/tutorial-runner.ts b/packages/runtime/src/store/tutorial-runner.ts index 4c32a9219..d4961fe5e 100644 --- a/packages/runtime/src/store/tutorial-runner.ts +++ b/packages/runtime/src/store/tutorial-runner.ts @@ -685,7 +685,7 @@ export class TutorialRunner { } }); this._editorStore.addFileOrFolder({ path: filePath, type: 'file' }); - this._updateCurrentFiles({ [filePath]: 'test' }); + this._updateCurrentFiles({ [filePath]: '' }); scheduleReadFor(filePath, 'utf-8'); } }); From f256113bacd998e4c790c762df4e5a2126ea1a1f Mon Sep 17 00:00:00 2001 From: Isaac Mann Date: Wed, 30 Oct 2024 10:59:10 -0400 Subject: [PATCH 03/10] chore: account for file deletion --- e2e/src/components/ButtonDeleteFile.tsx | 34 ++++++++++++++ .../tests/filesystem/watch-glob/content.mdx | 5 +- .../tests/filesystem/watch/content.mdx | 3 ++ e2e/test/filesystem.test.ts | 22 ++++++++- packages/runtime/src/store/editor.ts | 12 +++++ packages/runtime/src/store/tutorial-runner.ts | 47 ++++++++++++------- packages/runtime/src/types.d.ts | 5 ++ 7 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 e2e/src/components/ButtonDeleteFile.tsx diff --git a/e2e/src/components/ButtonDeleteFile.tsx b/e2e/src/components/ButtonDeleteFile.tsx new file mode 100644 index 000000000..d673fc161 --- /dev/null +++ b/e2e/src/components/ButtonDeleteFile.tsx @@ -0,0 +1,34 @@ +import { webcontainer } from 'tutorialkit:core'; + +interface Props { + filePath: string; + newContent: string; + + // default to 'store' + access?: 'store' | 'webcontainer'; + testId?: string; +} + +export function ButtonDeleteFile({ filePath, access = 'webcontainer', testId = 'delete-file' }: Props) { + async function deleteFile() { + switch (access) { + case 'webcontainer': { + const webcontainerInstance = await webcontainer; + + await webcontainerInstance.fs.rm(filePath); + + return; + } + case 'store': { + throw new Error('Delete from store not implemented'); + return; + } + } + } + + return ( + + ); +} diff --git a/e2e/src/content/tutorial/tests/filesystem/watch-glob/content.mdx b/e2e/src/content/tutorial/tests/filesystem/watch-glob/content.mdx index 66b771ce6..5054653d3 100644 --- a/e2e/src/content/tutorial/tests/filesystem/watch-glob/content.mdx +++ b/e2e/src/content/tutorial/tests/filesystem/watch-glob/content.mdx @@ -3,10 +3,11 @@ type: lesson title: Watch Glob focus: /bar.txt filesystem: - watch: ['/*', '/a/**/*', '/src/**/*'] + watch: ['/*.txt', '/a/**/*', '/src/**/*'] --- import { ButtonWriteToFile } from '@components/ButtonWriteToFile'; +import { ButtonDeleteFile } from '@components/ButtonDeleteFile'; # Watch filesystem test @@ -14,3 +15,5 @@ import { ButtonWriteToFile } from '@components/ButtonWriteToFile'; + + diff --git a/e2e/src/content/tutorial/tests/filesystem/watch/content.mdx b/e2e/src/content/tutorial/tests/filesystem/watch/content.mdx index 797287805..4498c7297 100644 --- a/e2e/src/content/tutorial/tests/filesystem/watch/content.mdx +++ b/e2e/src/content/tutorial/tests/filesystem/watch/content.mdx @@ -7,9 +7,12 @@ filesystem: --- import { ButtonWriteToFile } from '@components/ButtonWriteToFile'; +import { ButtonDeleteFile } from '@components/ButtonDeleteFile'; # Watch filesystem test + + diff --git a/e2e/test/filesystem.test.ts b/e2e/test/filesystem.test.ts index 046c3a39e..38b26e429 100644 --- a/e2e/test/filesystem.test.ts +++ b/e2e/test/filesystem.test.ts @@ -21,7 +21,10 @@ test('editor should reflect changes made from webcontainer in file in nested fol const testCase = 'watch'; await page.goto(`${BASE_URL}/${testCase}`); + // set up actions that shouldn't do anything await page.getByTestId('write-new-ignored-file').click(); + await page.getByTestId('delete-file').click(); + await page.getByRole('button', { name: 'baz.txt' }).click(); 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 await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Foo', { useInnerText: true, }); + + // test that ignored actions are ignored expect(await page.getByRole('button', { name: 'other.txt' }).count()).toEqual(0); + expect(await page.getByRole('button', { name: 'bar.txt' }).count()).toEqual(1); }); 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', await page.getByTestId('write-new-file').click(); await page.getByRole('button', { name: 'new.txt' }).click(); - expect(await page.getByRole('button', { name: 'other.txt' }).count()).toEqual(0); + await expect(async () => { + expect(await page.getByRole('button', { name: 'unknown' }).count()).toEqual(0); + expect(await page.getByRole('button', { name: 'other.txt' }).count()).toEqual(0); + }).toPass(); await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('New', { useInnerText: true, }); }); +test('editor should remove deleted files in specified paths in webcontainer', async ({ page }) => { + const testCase = 'watch-glob'; + await page.goto(`${BASE_URL}/${testCase}`); + + await page.getByTestId('delete-file').click(); + + await expect(async () => { + expect(await page.getByRole('button', { name: 'bar.txt' }).count()).toEqual(0); + }).toPass(); +}); + test('editor should not reflect changes made from webcontainer if watch is not set', async ({ page }) => { const testCase = 'no-watch'; await page.goto(`${BASE_URL}/${testCase}`); diff --git a/packages/runtime/src/store/editor.ts b/packages/runtime/src/store/editor.ts index 5fb2af52f..d215bdece 100644 --- a/packages/runtime/src/store/editor.ts +++ b/packages/runtime/src/store/editor.ts @@ -133,6 +133,18 @@ export class EditorStore { return contentChanged; } + deleteFile(filePath: string): boolean { + const documentState = this.documents.get()[filePath]; + + if (!documentState) { + return false; + } + + this.documents.setKey(filePath, undefined); + + return true; + } + onDocumentChanged(filePath: string, callback: (document: Readonly) => void) { const unsubscribeFromCurrentDocument = this.currentDocument.subscribe((document) => { if (document?.filePath === filePath) { diff --git a/packages/runtime/src/store/tutorial-runner.ts b/packages/runtime/src/store/tutorial-runner.ts index d4961fe5e..8910d7db3 100644 --- a/packages/runtime/src/store/tutorial-runner.ts +++ b/packages/runtime/src/store/tutorial-runner.ts @@ -1,6 +1,6 @@ import type { CommandsSchema, Files } from '@tutorialkit/types'; import type { IFSWatcher, WebContainer, WebContainerProcess } from '@webcontainer/api'; -import picomatch from 'picomatch'; +import picomatch from 'picomatch/posix.js'; import { newTask, type Task, type TaskCancelled } from '../tasks.js'; import { MultiCounter } from '../utils/multi-counter.js'; import { clearTerminal, escapeCodes, type ITerminal } from '../utils/terminal.js'; @@ -641,6 +641,23 @@ export class TutorialRunner { * cleanup the allocated buffers. */ const scheduleReadFor = (filePath: string, encoding: 'utf-8' | null) => { + const segments = filePath.split('/'); + segments.forEach((_, index) => { + if (index == segments.length - 1) { + return; + } + + const folderPath = segments.slice(0, index + 1).join('/'); + + if (!this._editorStore.documents.get()[folderPath]) { + this._editorStore.addFileOrFolder({ path: folderPath, type: 'folder' }); + } + }); + + if (!this._editorStore.documents.get()[filePath]) { + this._editorStore.addFileOrFolder({ path: filePath, type: 'file' }); + } + filesToRead.set(filePath, encoding); clearTimeout(timeoutId); @@ -663,7 +680,10 @@ export class TutorialRunner { } if (eventType === 'change') { - // we ignore all paths that aren't exposed in the `_editorStore` + /** + * Update file + * we ignore all paths that aren't exposed in the `_editorStore` + */ const file = this._editorStore.documents.get()[filePath]; if (!file) { @@ -672,21 +692,16 @@ export class TutorialRunner { scheduleReadFor(filePath, typeof file.value === 'string' ? 'utf-8' : null); } else if (eventType === 'rename' && Array.isArray(this._watchContentFromWebContainer)) { - const segments = filePath.split('/'); - segments.forEach((_, index) => { - if (index == segments.length - 1) { - return; - } - - const folderPath = segments.slice(0, index + 1).join('/'); + const file = this._editorStore.documents.get()[filePath]; - if (!this._editorStore.documents.get()[folderPath]) { - this._editorStore.addFileOrFolder({ path: folderPath, type: 'folder' }); - } - }); - this._editorStore.addFileOrFolder({ path: filePath, type: 'file' }); - this._updateCurrentFiles({ [filePath]: '' }); - scheduleReadFor(filePath, 'utf-8'); + if (file) { + // remove file + this._editorStore.deleteFile(filePath); + } else { + // add file + this._updateCurrentFiles({ [filePath]: '' }); + scheduleReadFor(filePath, 'utf-8'); + } } }); } diff --git a/packages/runtime/src/types.d.ts b/packages/runtime/src/types.d.ts index 11f02fe2a..b0698638d 100644 --- a/packages/runtime/src/types.d.ts +++ b/packages/runtime/src/types.d.ts @@ -1 +1,6 @@ /// + +// https://github.com/micromatch/picomatch?tab=readme-ov-file#api +declare module 'picomatch/posix.js' { + export { default } from 'picomatch'; +} From 69f7cd36a2c6b0edb139710eb3272df99aa6885e Mon Sep 17 00:00:00 2001 From: Isaac Mann Date: Thu, 31 Oct 2024 07:19:26 -0400 Subject: [PATCH 04/10] Update e2e/src/components/ButtonDeleteFile.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jòan --- e2e/src/components/ButtonDeleteFile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/src/components/ButtonDeleteFile.tsx b/e2e/src/components/ButtonDeleteFile.tsx index d673fc161..503a92b2f 100644 --- a/e2e/src/components/ButtonDeleteFile.tsx +++ b/e2e/src/components/ButtonDeleteFile.tsx @@ -4,7 +4,7 @@ interface Props { filePath: string; newContent: string; - // default to 'store' + // default to 'webcontainer' access?: 'store' | 'webcontainer'; testId?: string; } From 8d7ffc49176499451ef2f24beb38952bea45135e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Fri, 1 Nov 2024 11:03:34 +0200 Subject: [PATCH 05/10] test: prefer `toBeVisible` selectors --- e2e/test/filesystem.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/test/filesystem.test.ts b/e2e/test/filesystem.test.ts index 38b26e429..ba4b11bc2 100644 --- a/e2e/test/filesystem.test.ts +++ b/e2e/test/filesystem.test.ts @@ -38,8 +38,8 @@ test('editor should reflect changes made from webcontainer in file in nested fol }); // test that ignored actions are ignored - expect(await page.getByRole('button', { name: 'other.txt' }).count()).toEqual(0); - expect(await page.getByRole('button', { name: 'bar.txt' }).count()).toEqual(1); + await expect(page.getByRole('button', { name: 'other.txt' })).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'bar.txt' })).toBeVisible(); }); test('editor should reflect changes made from webcontainer in specified paths', async ({ page }) => { From f2403a99e7117bee23682d3d60454ea8db4b6316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Fri, 1 Nov 2024 11:03:46 +0200 Subject: [PATCH 06/10] test: prefer `toBeVisible` selectors --- e2e/test/filesystem.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/test/filesystem.test.ts b/e2e/test/filesystem.test.ts index ba4b11bc2..45c03d44e 100644 --- a/e2e/test/filesystem.test.ts +++ b/e2e/test/filesystem.test.ts @@ -66,8 +66,8 @@ test('editor should reflect new files added in specified paths in webcontainer', await page.getByRole('button', { name: 'new.txt' }).click(); await expect(async () => { - expect(await page.getByRole('button', { name: 'unknown' }).count()).toEqual(0); - expect(await page.getByRole('button', { name: 'other.txt' }).count()).toEqual(0); + await expect(page.getByRole('button', { name: 'unknown' })).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'other.txt' })).not.toBeVisible(); }).toPass(); await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('New', { From 49c941c56575d2c9b4de1ab461a2f1c9fc9a07ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Fri, 1 Nov 2024 11:03:55 +0200 Subject: [PATCH 07/10] test: prefer `toBeVisible` selectors --- e2e/test/filesystem.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/test/filesystem.test.ts b/e2e/test/filesystem.test.ts index 45c03d44e..8db15888b 100644 --- a/e2e/test/filesystem.test.ts +++ b/e2e/test/filesystem.test.ts @@ -82,7 +82,7 @@ test('editor should remove deleted files in specified paths in webcontainer', as await page.getByTestId('delete-file').click(); await expect(async () => { - expect(await page.getByRole('button', { name: 'bar.txt' }).count()).toEqual(0); + await expect(page.getByRole('button', { name: 'bar.txt' })).not.toBeVisible(); }).toPass(); }); From 060abf49441f1dacd39c5d9ec50e3d2593dca115 Mon Sep 17 00:00:00 2001 From: Isaac Mann Date: Mon, 4 Nov 2024 22:58:13 -0500 Subject: [PATCH 08/10] feat: sync new files from WebContainer to editor --- packages/runtime/src/store/tutorial-runner.ts | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/runtime/src/store/tutorial-runner.ts b/packages/runtime/src/store/tutorial-runner.ts index 8910d7db3..5546c490b 100644 --- a/packages/runtime/src/store/tutorial-runner.ts +++ b/packages/runtime/src/store/tutorial-runner.ts @@ -641,23 +641,6 @@ export class TutorialRunner { * cleanup the allocated buffers. */ const scheduleReadFor = (filePath: string, encoding: 'utf-8' | null) => { - const segments = filePath.split('/'); - segments.forEach((_, index) => { - if (index == segments.length - 1) { - return; - } - - const folderPath = segments.slice(0, index + 1).join('/'); - - if (!this._editorStore.documents.get()[folderPath]) { - this._editorStore.addFileOrFolder({ path: folderPath, type: 'folder' }); - } - }); - - if (!this._editorStore.documents.get()[filePath]) { - this._editorStore.addFileOrFolder({ path: filePath, type: 'file' }); - } - filesToRead.set(filePath, encoding); clearTimeout(timeoutId); @@ -679,6 +662,8 @@ export class TutorialRunner { return; } + console.log(eventType, filename); + if (eventType === 'change') { /** * Update file @@ -699,6 +684,23 @@ export class TutorialRunner { this._editorStore.deleteFile(filePath); } else { // add file + const segments = filePath.split('/'); + segments.forEach((_, index) => { + if (index == segments.length - 1) { + return; + } + + const folderPath = segments.slice(0, index + 1).join('/'); + + if (!this._editorStore.documents.get()[folderPath]) { + this._editorStore.addFileOrFolder({ path: folderPath, type: 'folder' }); + } + }); + + if (!this._editorStore.documents.get()[filePath]) { + this._editorStore.addFileOrFolder({ path: filePath, type: 'file' }); + } + this._updateCurrentFiles({ [filePath]: '' }); scheduleReadFor(filePath, 'utf-8'); } From c79bafe7ac325722b05ba3e03df9e2c609079847 Mon Sep 17 00:00:00 2001 From: Isaac Mann Date: Mon, 4 Nov 2024 23:02:52 -0500 Subject: [PATCH 09/10] chore: remove console log --- packages/runtime/src/store/tutorial-runner.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/runtime/src/store/tutorial-runner.ts b/packages/runtime/src/store/tutorial-runner.ts index 5546c490b..09aec75c7 100644 --- a/packages/runtime/src/store/tutorial-runner.ts +++ b/packages/runtime/src/store/tutorial-runner.ts @@ -662,8 +662,6 @@ export class TutorialRunner { return; } - console.log(eventType, filename); - if (eventType === 'change') { /** * Update file From ffa57ef3563fc905a59072ffa18c38f68df4c501 Mon Sep 17 00:00:00 2001 From: Isaac Mann Date: Mon, 4 Nov 2024 23:43:54 -0500 Subject: [PATCH 10/10] docs: document filesystem watch globs --- .../src/content/docs/reference/configuration.mdx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx b/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx index 89c1ab12a..31707ca94 100644 --- a/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx +++ b/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx @@ -284,13 +284,15 @@ An example use case is when a user runs a command that modifies a file. For inst 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`. +If you would like files to be added or removed from the editor automatically, you need to specify an array of globs that will determine which folders and files to watch for changes. + The `FileSystem` type has the following shape: ```ts type FileSystem = { - watch: boolean + watch: boolean | string[] } ``` @@ -299,10 +301,13 @@ Example values: ```yaml filesystem: - watch: true # Filesystem changes are reflected in the editor + watch: true # Filesystem changes to files already in the editor are reflected in the editor filesystem: watch: false # Or if it's omitted, the default value is false + +filesystem: + watch: ['/*.json', '/src/**/*'] # Files changed, added or deleted that match one of the globs are updated in the editor ```