Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@webcontainer/api": "^1.6.1"
"@webcontainer/api": "^1.6.1",
"@webcontainer/snapshot": "^0.1.0"
},
"peerDependencies": {
"@vitest/browser": "^3.1",
Expand Down
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 5 additions & 54 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { readdir, readFile, stat } from "node:fs/promises";
import { relative, resolve, sep } from "node:path";
import { FileSystemTree } from "@webcontainer/api";
import { resolve } from "node:path";
import { snapshot } from "@webcontainer/snapshot";
import type { BrowserCommand } from "vitest/node";

// custom command to read directories from browser context. Used for mounting directories inside webcontainer
export const readDirectory: BrowserCommand<[directory: string]> = async (
context,
directory,
): Promise<FileSystemTree> => {
): Promise<string> => {
const root = context.project.config.root;
const resolved = resolve(root, directory);

Expand All @@ -21,55 +20,7 @@ export const readDirectory: BrowserCommand<[directory: string]> = async (
);
}

const files = await recursiveRead(directory);
const tree: FileSystemTree = {};
const tree = await snapshot(directory);

for (const { filePath, contents } of files) {
const segments = filePath.split("/");

let currentTree: FileSystemTree = tree;

for (let i = 0; i < segments.length; i++) {
const name = segments[i];

if (i === segments.length - 1) {
currentTree[name] = { file: { contents } };
} else {
let folder = currentTree[name];

if (!folder || !("directory" in folder)) {
folder = { directory: {} };
currentTree[name] = folder;
}

currentTree = folder.directory;
}
}
}

return tree;

async function recursiveRead(
dir: string,
): Promise<{ filePath: string; contents: string }[]> {
const files = await readdir(dir);

const output = await Promise.all(
files.map(async (file) => {
const filePath = resolve(dir, file);
const fileStat = await stat(filePath);

if (fileStat.isDirectory()) {
return recursiveRead(filePath);
}

return {
filePath: relative(directory, filePath).replaceAll(sep, "/"),
contents: await readFile(filePath, "utf-8"),
};
}),
);

return output.flat();
}
return tree.toString("base64");
};
7 changes: 5 additions & 2 deletions src/fixtures/file-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ export class FileSystem {
*/
async mount(filesOrPath: string | FileSystemTree) {
if (typeof filesOrPath === "string") {
filesOrPath = await commands.readDirectory(filesOrPath);
const tree = await commands.readDirectory(filesOrPath);
const binary = Uint8Array.from(atob(tree), (c) => c.charCodeAt(0));

return await this._instance.mount(binary);
}

return await this._instance.mount(filesOrPath as FileSystemTree);
return await this._instance.mount(filesOrPath);
}

/**
Expand Down
45 changes: 35 additions & 10 deletions src/fixtures/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,48 @@ export class ProcessWrap {
isDone: Promise<void>;

constructor(promise: Promise<WebContainerProcess>) {
let isExitted = false;
let setDone: () => void = () => undefined;
this.isDone = new Promise((resolve) => (setDone = resolve));

this.isDone = new Promise((resolve) => {
setDone = () => {
resolve();
isExitted = true;
};
});

this._isReady = promise.then((webcontainerProcess) => {
this._webcontainerProcess = webcontainerProcess;
this._writer = webcontainerProcess.input.getWriter();

webcontainerProcess.exit.then(() => setDone());
webcontainerProcess.exit.then(setDone);

const reader = this._webcontainerProcess.output.getReader();

const read = async () => {
while (true) {
const { done, value } = await reader.read();

if (isExitted && !done) {
console.warn(
`[webcontainer-test]: Closed process keeps writing to output. Closing reader forcefully. \n` +
` Received: "${value}".`,
);
await reader.cancel();
break;
}

// webcontainer process never reaches here, but for future-proofing let's exit
if (done) {
break;
}

this._output += value;
this._listeners.forEach((fn) => fn(value));
}
};

this._webcontainerProcess.output.pipeTo(
new WritableStream({
write: (data) => {
this._output += data;
this._listeners.forEach((fn) => fn(data));
},
}),
);
void read();
});
}

Expand Down
3 changes: 1 addition & 2 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type {} from "@vitest/browser/context";
import { FileSystemTree } from "@webcontainer/api";

declare module "@vitest/browser/context" {
interface BrowserCommands {
readDirectory: (directory: string) => Promise<FileSystemTree>;
readDirectory: (directory: string) => Promise<string>;
}
}
Binary file added test/fixtures/mount-example/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 25 additions & 17 deletions test/mount.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
import { expect } from "vitest";
import { test } from "../src";

test("user can mount directories from file-system to webcontainer", async ({
webcontainer,
}) => {
await webcontainer.mount("test/fixtures/mount-example");
test(
"user can mount directories from file-system to webcontainer",
{ retry: 3 },
async ({ webcontainer }) => {
await webcontainer.mount("test/fixtures/mount-example");

const ls = await webcontainer.runCommand("ls");
expect(ls).toMatchInlineSnapshot(`"file-1.ts nested"`);
const ls = await webcontainer.runCommand("ls");
expect(ls).toMatchInlineSnapshot(`"file-1.ts image.png nested"`);

const lsNested = await webcontainer.runCommand("ls", ["nested"]);
expect(lsNested).toMatchInlineSnapshot(`"file-2.ts"`);
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 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";"`,
);
});
const catNestedFile = await webcontainer.runCommand("cat", [
"nested/file-2.ts",
]);
expect(catNestedFile).toMatchInlineSnapshot(
`"export default "Hello from nested file";"`,
);

// TODO: Once WebcontainerProcess output resolving is visible, assert whole png content
const pngFile = await webcontainer.runCommand("xxd", ["image.png"]);
expect(pngFile).toContain(
"00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG........IHDR",
);
},
);

test("user can mount inlined FileSystemTree to webcontainer", async ({
webcontainer,
Expand Down