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 e0403e43..3d41c38d 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 aeac65eb..2ed52b5f 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 00000000..a6881827 --- /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 00000000..8430408a --- /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 00000000..66b771ce --- /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 46a0ed3b..79728780 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 be14fef7..046c3a39 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 ad984f28..d28c5649 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 a0f305d6..5fb2af52 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 e1df25d7..4c32a921 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 1cabc31b..4f3972a4 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 152a529c..46bfbeab 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 4c32a921..d4961fe5 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 00000000..d673fc16 --- /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 66b771ce..5054653d 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 79728780..4498c729 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 046c3a39..38b26e42 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 5fb2af52..d215bdec 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 d4961fe5..8910d7db 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 11f02fe2..b0698638 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 d673fc16..503a92b2 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 38b26e42..ba4b11bc 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 ba4b11bc..45c03d44 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 45c03d44..8db15888 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 8910d7db..5546c490 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 5546c490..09aec75c 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 89c1ab12..31707ca9 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 ```