From 81558e315c5d5239f612600b2d5f58ed1e7a4c6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Mon, 14 Apr 2025 15:54:57 +0300 Subject: [PATCH] docs: readme and jsdocs --- README.md | 65 ++++++++++++++++++++++++++++++++++++ src/commands/index.ts | 2 +- src/fixtures/index.ts | 20 +++++++++++ src/fixtures/preview.ts | 18 ++++++++++ src/fixtures/webcontainer.ts | 32 +++++++++++++++++- src/plugin.ts | 1 - test/mount.test.ts | 41 +++++++++++++++++++++-- tsconfig.json | 3 +- 8 files changed, 175 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 691bbb3..9a5706d 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,14 @@ Vitest's [`getByText`](https://vitest.dev/guide/browser/locators.html#getbytext) await preview.getByText("Hello Vite!"); ``` +##### `locator` + +Vitest's [`locator`](https://vitest.dev/guide/browser/locators.html) of the preview window. + +```ts +await preview.locator.hover(); +``` + #### `webcontainer` ##### `mount` @@ -90,6 +98,15 @@ Accepts a path that is relative to the [project root](https://vitest.dev/config/ ```ts await webcontainer.mount("/path/to/project"); + +await webcontainer.mount({ + "package.json": { file: { contents: '{ "name": "example-project" }' } }, + src: { + directory: { + "index.ts": { file: { contents: "export default 'Hello!';" } }, + }, + }, +}); ``` ##### `runCommand` @@ -102,5 +119,53 @@ await webcontainer.runCommand("npm", ["install"]); const files = await webcontainer.runCommand("ls", ["-l"]); ``` +##### `readFile` + +WebContainer's [`readFile`](https://webcontainers.io/guides/working-with-the-file-system#readfile) method. + +```ts +const content = await webcontainer.readFile("/package.json"); +``` + +##### `writeFile` + +WebContainer's [`writeFile`](https://webcontainers.io/guides/working-with-the-file-system#writefile) method. + +```ts +await webcontainer.writeFile("/main.ts", "console.log('Hello world!')"); +``` + +##### `rename` + +WebContainer's [`rename`](https://webcontainers.io/guides/working-with-the-file-system#rename) method. + +```ts +await webcontainer.rename("/before.ts", "/after.ts"); +``` + +##### `mkdir` + +WebContainer's [`mkdir`](https://webcontainers.io/guides/working-with-the-file-system#mkdir) method. + +```ts +await webcontainer.mkdir("/src/components"); +``` + +##### `readdir` + +WebContainer's [`readdir`](https://webcontainers.io/guides/working-with-the-file-system#readdir) method. + +```ts +const contents = await webcontainer.readdir("/src"); +``` + +##### `rm` + +WebContainer's [`rm`](https://webcontainers.io/guides/working-with-the-file-system#rm) method. + +```ts +await webcontainer.rm("/node_modules"); +``` + [version-badge]: https://img.shields.io/npm/v/@webcontainer/test [npm-url]: https://www.npmjs.com/package/@webcontainer/test diff --git a/src/commands/index.ts b/src/commands/index.ts index a462dac..d1fa1cd 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -13,7 +13,7 @@ export const readDirectory: BrowserCommand<[directory: string]> = async ( if (!resolved.startsWith(root)) { throw new Error( - `[vitest:webcontainers] Cannot read files outside project root: \n${JSON.stringify( + `[vitest:webcontainers] Cannot read files outside project root:\n${JSON.stringify( { directory, resolved }, null, 2, diff --git a/src/fixtures/index.ts b/src/fixtures/index.ts index 0f481e2..e5751de 100644 --- a/src/fixtures/index.ts +++ b/src/fixtures/index.ts @@ -3,6 +3,26 @@ import { test as base } from "vitest"; import { Preview } from "./preview"; import { WebContainer } from "./webcontainer"; +/** + * Pre-defined [`test()` function](https://vitest.dev/guide/test-context.html#extend-test-context) with WebContainer fixtures. + * + * @example + * ```ts + * import { test } from "@webcontainer/test"; + * + * test("run development server inside webcontainer", async ({ + * webcontainer, + * preview, + * }) => { + * await webcontainer.mount("path/to/project"); + * + * await webcontainer.runCommand("npm", ["install"]); + * webcontainer.runCommand("npm", ["run", "dev"]); + * + * await preview.getByRole("heading", { level: 1, name: "Hello Vite!" }); + * }); + * ``` + */ export const test = base.extend<{ preview: Preview; webcontainer: WebContainer; diff --git a/src/fixtures/preview.ts b/src/fixtures/preview.ts index 0d4f220..21cfc7d 100644 --- a/src/fixtures/preview.ts +++ b/src/fixtures/preview.ts @@ -3,13 +3,17 @@ import { type Locator, page } from "@vitest/browser/context"; const TEST_ID = "webcontainers-iframe"; export class Preview { + /** @internal */ private _preview: Locator; + + /** @internal */ private _iframe?: HTMLIFrameElement; constructor() { this._preview = page.getByTestId(TEST_ID); } + /** @internal */ async setup(url: string) { const iframe = document.createElement("iframe"); iframe.setAttribute("src", url); @@ -21,17 +25,31 @@ export class Preview { this._iframe = iframe; } + /** @internal */ async teardown() { if (this._iframe) { document.body.removeChild(this._iframe); } } + /** + * Vitest's [`getByRole`](https://vitest.dev/guide/browser/locators.html#getbyrole) that's scoped to the preview window. + */ async getByRole(...options: Parameters) { return this._preview.getByRole(...options); } + /** + * Vitest's [`getByText`](https://vitest.dev/guide/browser/locators.html#getbytext) that's scoped to the preview window. + */ async getByText(...options: Parameters) { return this._preview.getByText(...options); } + + /** + * Vitest's [`locator`](https://vitest.dev/guide/browser/locators.html) of the preview window. + */ + get locator() { + return this._preview; + } } diff --git a/src/fixtures/webcontainer.ts b/src/fixtures/webcontainer.ts index 9b97512..712913c 100644 --- a/src/fixtures/webcontainer.ts +++ b/src/fixtures/webcontainer.ts @@ -6,8 +6,13 @@ import { } from "@webcontainer/api"; export class WebContainer { + /** @internal */ private _instancePromise?: WebContainerApi; + + /** @internal */ private _isReady: Promise; + + /** @internal */ private _onExit: (() => Promise)[] = []; constructor() { @@ -26,10 +31,12 @@ export class WebContainer { return this._instancePromise; } + /** @internal */ async wait() { await this._isReady; } + /** @internal */ onServerReady(callback: (options: { port: number; url: string }) => void) { this._instance.on("server-ready", (port, url) => { callback({ port, url }); @@ -37,7 +44,7 @@ export class WebContainer { } /** - * Mount file directory into webcontainer. + * Mount file directory into WebContainer. * `string` arguments are considered paths that are relative to [`root`](https://vitest.dev/config/#root) */ async mount(filesOrPath: string | FileSystemTree) { @@ -48,6 +55,7 @@ export class WebContainer { return await this._instance.mount(filesOrPath as FileSystemTree); } + /** @internal */ async teardown() { await Promise.all(this._onExit.map((fn) => fn())); @@ -58,6 +66,10 @@ export class WebContainer { this._instancePromise = undefined; } + /** + * Run command inside WebContainer. + * Returns the output of the command. + */ async runCommand(command: string, args: string[] = []) { let output = ""; @@ -86,26 +98,44 @@ export class WebContainer { return output.trim(); } + /** + * WebContainer's [`readFile`](https://webcontainers.io/guides/working-with-the-file-system#readfile) method. + */ async readFile(path: string, encoding: BufferEncoding = "utf8") { return this._instance.fs.readFile(path, encoding); } + /** + * WebContainer's [`writeFile`](https://webcontainers.io/guides/working-with-the-file-system#writefile) method. + */ async writeFile(path: string, data: string, encoding = "utf8") { return this._instance.fs.writeFile(path, data, { encoding }); } + /** + * WebContainer's [`rename`](https://webcontainers.io/guides/working-with-the-file-system#rename) method. + */ async rename(oldPath: string, newPath: string) { return this._instance.fs.rename(oldPath, newPath); } + /** + * WebContainer's [`mkdir`](https://webcontainers.io/guides/working-with-the-file-system#mkdir) method. + */ async mkdir(path: string) { return this._instance.fs.mkdir(path); } + /** + * WebContainer's [`readdir`](https://webcontainers.io/guides/working-with-the-file-system#readdir) method. + */ async readdir(path: string) { return this._instance.fs.readdir(path); } + /** + * WebContainer's [`rm`](https://webcontainers.io/guides/working-with-the-file-system#rm) method. + */ async rm(path: string) { return this._instance.fs.rm(path); } diff --git a/src/plugin.ts b/src/plugin.ts index 846e42e..9eee4cc 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,6 +1,5 @@ import type { Vite } from "vitest/node"; import { readDirectory } from "./commands"; -import "./types.d.ts"; const COEP = "Cross-Origin-Embedder-Policy"; const COOP = "Cross-Origin-Opener-Policy"; diff --git a/test/mount.test.ts b/test/mount.test.ts index f72a8ee..2e9e7c5 100644 --- a/test/mount.test.ts +++ b/test/mount.test.ts @@ -1,7 +1,9 @@ import { expect } from "vitest"; import { test } from "../src"; -test("user can mount directories to webcontainer", async ({ webcontainer }) => { +test("user can mount directories from file-system to webcontainer", async ({ + webcontainer, +}) => { await webcontainer.mount("test/fixtures/mount-example"); const ls = await webcontainer.runCommand("ls"); @@ -21,12 +23,45 @@ test("user can mount directories to webcontainer", async ({ webcontainer }) => { ); }); +test("user can mount inlined FileSystemTree to webcontainer", async ({ + webcontainer, +}) => { + await webcontainer.mount({ + "file-1.ts": { + file: { contents: 'export default "Hello world";' }, + }, + nested: { + directory: { + "file-2.ts": { + file: { contents: 'export default "Hello from nested file";' }, + }, + }, + }, + }); + + const ls = await webcontainer.runCommand("ls"); + expect(ls).toMatchInlineSnapshot(`"file-1.ts nested"`); + + const lsNested = await webcontainer.runCommand("ls", ["nested"]); + expect(lsNested).toMatchInlineSnapshot(`"file-2.ts"`); + + const catFile = await webcontainer.runCommand("cat", ["file-1.ts"]); + expect(catFile).toMatchInlineSnapshot(`"export default "Hello world";"`); + + const catNestedFile = await webcontainer.runCommand("cat", [ + "nested/file-2.ts", + ]); + expect(catNestedFile).toMatchInlineSnapshot( + `"export default "Hello from nested file";"`, + ); +}); + test("user should see error when attemping to mount files outside project root", async ({ webcontainer, }) => { await expect(() => webcontainer.mount("/home/non-existing")).rejects .toThrowErrorMatchingInlineSnapshot(` - [Error: [vitest:webcontainers] Cannot read files outside project root: + [Error: [vitest:webcontainers] Cannot read files outside project root: { "directory": "/home/non-existing", "resolved": "/home/non-existing" @@ -35,7 +70,7 @@ test("user should see error when attemping to mount files outside project root", await expect(() => webcontainer.mount("/../../non-existing")).rejects .toThrowErrorMatchingInlineSnapshot(` - [Error: [vitest:webcontainers] Cannot read files outside project root: + [Error: [vitest:webcontainers] Cannot read files outside project root: { "directory": "/../../non-existing", "resolved": "/non-existing" diff --git a/tsconfig.json b/tsconfig.json index fefe855..3a2d260 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,8 @@ "module": "ESNext", "target": "ESNext", "moduleResolution": "bundler", - "skipLibCheck": true + "skipLibCheck": true, + "stripInternal": true }, "include": ["src", "test"] }