Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: sync files from WebContainer to editor #334

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Nemikolh merged 12 commits into main from joan/sync-files-from-fs
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
12 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,35 @@ type Command = string

```

##### `filesystem`
Configures how changes such as files being modified or added in WebContainer should be reflected in the editor when they weren't caused by the user directly. By default, the editor will not reflect these changes.

An example use case is when a user runs a command that modifies a file. For instance when a `package.json` is modified by doing an `npm install <xyz>`. If `watch` is set to `true`, the file will be updated in the editor. If set to `false`, the file will not be updated.

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`.

<PropertyTable inherited type={'FileSystem'} />

The `FileSystem` type has the following shape:

```ts
type FileSystem = {
watch: boolean
}

```

Example values:

```yaml
filesystem:
watch: true # Filesystem changes are reflected in the editor

filesystem:
watch: false # Or if it's omitted, the default value is false
```


##### `terminal`
Configures one or more terminals. TutorialKit provides two types of terminals: read-only, called `output`, and interactive, called `terminal`. Note, that there can be only one `output` terminal.

Expand Down Expand Up @@ -319,7 +348,7 @@ Navigating to a lesson that specifies `autoReload` will always reload the previe
Specifies which folder from the `src/templates/` directory should be used as the basis for the code. See the "[Code templates](/guides/creating-content/#code-templates)" guide for a detailed explainer.
<PropertyTable inherited type="string" />

#### `editPageLink`
##### `editPageLink`
Display a link in lesson for editing the page content.
The value is a URL pattern where `${path}` is replaced with the lesson's location relative to the `src/content/tutorial`.

Expand All @@ -346,7 +375,7 @@ You can instruct Github to show the source code instead by adding `plain=1` quer

:::

### `openInStackBlitz`
##### `openInStackBlitz`
Display a link for opening current lesson in StackBlitz.
<PropertyTable inherited type="OpenInStackBlitz" />

Expand Down
26 changes: 17 additions & 9 deletions e2e/src/components/ButtonWriteToFile.tsx
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import tutorialStore from 'tutorialkit:store';
import { webcontainer } from 'tutorialkit:core';

interface Props {
filePath: string;
newContent: string;

// default to 'store'
access?: 'store' | 'webcontainer';
testId?: string;
}

export function ButtonWriteToFile({ filePath, newContent, testId = 'write-to-file' }: Props) {
export function ButtonWriteToFile({ filePath, newContent, access = 'store', testId = 'write-to-file' }: Props) {
async function writeFile() {
await new Promise<void>((resolve) => {
tutorialStore.lessonFullyLoaded.subscribe((value) => {
if (value) {
resolve();
}
});
});
switch (access) {
case 'webcontainer': {
const webcontainerInstance = await webcontainer;

await webcontainerInstance.fs.writeFile(filePath, newContent);

tutorialStore.updateFile(filePath, newContent);
return;
}
case 'store': {
tutorialStore.updateFile(filePath, newContent);
return;
}
}
}

return (
Expand Down
4 changes: 4 additions & 0 deletions e2e/src/content/tutorial/tests/filesystem/meta.md
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
type: chapter
title: filesystem
---
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Initial content
11 changes: 11 additions & 0 deletions e2e/src/content/tutorial/tests/filesystem/no-watch/content.mdx
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
type: lesson
title: No watch
focus: /bar.txt
---

import { ButtonWriteToFile } from '@components/ButtonWriteToFile';

# Watch filesystem test

<ButtonWriteToFile client:load access="webcontainer" filePath="/bar.txt" newContent='Something else' />
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Baz
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Initial content
14 changes: 14 additions & 0 deletions e2e/src/content/tutorial/tests/filesystem/watch/content.mdx
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
type: lesson
title: Watch
focus: /bar.txt
filesystem:
watch: true
---

import { ButtonWriteToFile } from '@components/ButtonWriteToFile';

# Watch filesystem test

<ButtonWriteToFile client:load access="webcontainer" filePath="/bar.txt" newContent='Something else' />
<ButtonWriteToFile client:load access="webcontainer" filePath="/a/b/baz.txt" newContent='Foo' testId='write-to-file-in-subfolder' />
52 changes: 52 additions & 0 deletions e2e/test/filesystem.test.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { test, expect } from '@playwright/test';

const BASE_URL = '/tests/filesystem';

test('editor should reflect changes made from webcontainer', async ({ page }) => {
const testCase = 'watch';
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 changes made from webcontainer in file in nested folder', async ({ page }) => {
const testCase = 'watch';
await page.goto(`${BASE_URL}/${testCase}`);

await page.getByRole('button', { name: 'baz.txt' }).click();

await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Baz', {
useInnerText: true,
});

await page.getByTestId('write-to-file-in-subfolder').click();

await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Foo', {
useInnerText: true,
});
});

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}`);

await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Initial content\n', {
useInnerText: true,
});

await page.getByTestId('write-to-file').click();

await page.waitForTimeout(1_000);

await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText('Initial content\n', {
useInnerText: true,
});
});
1 change: 1 addition & 0 deletions packages/astro/src/default/utils/content.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ export async function getTutorial(): Promise<Tutorial> {
'i18n',
'editPageLink',
'openInStackBlitz',
'filesystem',
],
),
};
Expand Down
5 changes: 2 additions & 3 deletions packages/runtime/README.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ A wrapper around the **[WebContainer API][webcontainer-api]** focused on providi

The runtime exposes the following:

- `lessonFilesFetcher`: A singleton that lets you fetch the contents of the lesson files
- `TutorialRunner`: The API to manage your tutorial content in WebContainer
- `TutorialStore`: A store to manage your tutorial content in WebContainer and in your components.

Only a single instance of `TutorialRunner` should be created in your application and its lifetime is bound by the lifetime of the WebContainer instance.
Only a single instance of `TutorialStore` should be created in your application and its lifetime is bound by the lifetime of the WebContainer instance.

## License

Expand Down
2 changes: 0 additions & 2 deletions packages/runtime/src/index.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
export { LessonFilesFetcher } from './lesson-files.js';
export { TutorialRunner } from './tutorial-runner.js';
export type { Command, Commands, PreviewInfo, Step, Steps } from './webcontainer/index.js';
export { safeBoot } from './webcontainer/index.js';
export { TutorialStore } from './store/index.js';
2 changes: 1 addition & 1 deletion packages/runtime/src/store/editor.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export class EditorStore {
});
}

updateFile(filePath: string, content: string): boolean {
updateFile(filePath: string, content: string | Uint8Array): boolean {
const documentState = this.documents.get()[filePath];

if (!documentState) {
Expand Down
6 changes: 4 additions & 2 deletions packages/runtime/src/store/index.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { WebContainer } from '@webcontainer/api';
import { atom, type ReadableAtom } from 'nanostores';
import { LessonFilesFetcher } from '../lesson-files.js';
import { newTask, type Task } from '../tasks.js';
import { TutorialRunner } from '../tutorial-runner.js';
import type { ITerminal } from '../utils/terminal.js';
import type { EditorConfig } from '../webcontainer/editor-config.js';
import { bootStatus, unblockBoot, type BootStatus } from '../webcontainer/on-demand-boot.js';
Expand All @@ -13,6 +12,7 @@ import type { TerminalConfig } from '../webcontainer/terminal-config.js';
import { EditorStore, type EditorDocument, type EditorDocuments, type ScrollPosition } from './editor.js';
import { PreviewsStore } from './previews.js';
import { TerminalStore } from './terminal.js';
import { TutorialRunner } from './tutorial-runner.js';

interface StoreOptions {
webcontainer: Promise<WebContainer>;
Expand Down Expand Up @@ -59,7 +59,7 @@ export class TutorialStore {
this._lessonFilesFetcher = new LessonFilesFetcher(basePathname);
this._previewsStore = new PreviewsStore(this._webcontainer);
this._terminalStore = new TerminalStore(this._webcontainer, useAuth);
this._runner = new TutorialRunner(this._webcontainer, this._terminalStore, this._stepController);
this._runner = new TutorialRunner(this._webcontainer, this._terminalStore, this._editorStore, this._stepController);

/**
* By having this code under `import.meta.hot`, it gets:
Expand Down Expand Up @@ -150,6 +150,8 @@ export class TutorialStore {
return;
}

this._runner.setWatchFromWebContainer(lesson.data.filesystem?.watch ?? false);

this._lessonTask = newTask(
async (signal) => {
const templatePromise = this._lessonFilesFetcher.getLessonTemplate(lesson);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { resetProcessFactory, setProcessFactory } from '@tutorialkit/test-utils'
import type { MockedWebContainer } from '@tutorialkit/test-utils';
import { WebContainer } from '@webcontainer/api';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { TerminalStore } from './store/terminal.js';
import { withResolvers } from '../utils/promises.js';
import { StepsController } from '../webcontainer/steps.js';
import { EditorStore } from './editor.js';
import { TerminalStore } from './terminal.js';
import { TutorialRunner } from './tutorial-runner.js';
import { withResolvers } from './utils/promises.js';
import { StepsController } from './webcontainer/steps.js';

beforeEach(() => {
resetProcessFactory();
Expand All @@ -17,7 +18,12 @@ describe('TutorialRunner', () => {
test('prepareFiles should mount files to WebContainer', async () => {
const webcontainer = WebContainer.boot();
const mock = (await webcontainer) as MockedWebContainer;
const runner = new TutorialRunner(webcontainer, new TerminalStore(webcontainer, false), new StepsController());
const runner = new TutorialRunner(
webcontainer,
new TerminalStore(webcontainer, false),
new EditorStore(),
new StepsController(),
);

await runner.prepareFiles({
files: {
Expand Down Expand Up @@ -72,7 +78,12 @@ describe('TutorialRunner', () => {

setProcessFactory(processFactory);

const runner = new TutorialRunner(webcontainer, new TerminalStore(webcontainer, false), new StepsController());
const runner = new TutorialRunner(
webcontainer,
new TerminalStore(webcontainer, false),
new EditorStore(),
new StepsController(),
);

runner.setCommands({
mainCommand: 'some command',
Expand Down
Loading

AltStyle によって変換されたページ (->オリジナル) /