Skip to content

Commit 6a9f164

Browse files
authored
fix: mount binary files correctly (#14)
1 parent 9784b75 commit 6a9f164

File tree

8 files changed

+90
-86
lines changed

8 files changed

+90
-86
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
"typecheck": "tsc --noEmit"
4646
},
4747
"dependencies": {
48-
"@webcontainer/api": "^1.6.1"
48+
"@webcontainer/api": "^1.6.1",
49+
"@webcontainer/snapshot": "^0.1.0"
4950
},
5051
"peerDependencies": {
5152
"@vitest/browser": "^3.1",

pnpm-lock.yaml

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/index.ts

Lines changed: 5 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import { readdir, readFile, stat } from "node:fs/promises";
2-
import { relative, resolve, sep } from "node:path";
3-
import { FileSystemTree } from "@webcontainer/api";
1+
import { resolve } from "node:path";
2+
import { snapshot } from "@webcontainer/snapshot";
43
import type { BrowserCommand } from "vitest/node";
54

65
// custom command to read directories from browser context. Used for mounting directories inside webcontainer
76
export const readDirectory: BrowserCommand<[directory: string]> = async (
87
context,
98
directory,
10-
): Promise<FileSystemTree> => {
9+
): Promise<string> => {
1110
const root = context.project.config.root;
1211
const resolved = resolve(root, directory);
1312

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

24-
const files = await recursiveRead(directory);
25-
const tree: FileSystemTree = {};
23+
const tree = await snapshot(directory);
2624

27-
for (const { filePath, contents } of files) {
28-
const segments = filePath.split("/");
29-
30-
let currentTree: FileSystemTree = tree;
31-
32-
for (let i = 0; i < segments.length; i++) {
33-
const name = segments[i];
34-
35-
if (i === segments.length - 1) {
36-
currentTree[name] = { file: { contents } };
37-
} else {
38-
let folder = currentTree[name];
39-
40-
if (!folder || !("directory" in folder)) {
41-
folder = { directory: {} };
42-
currentTree[name] = folder;
43-
}
44-
45-
currentTree = folder.directory;
46-
}
47-
}
48-
}
49-
50-
return tree;
51-
52-
async function recursiveRead(
53-
dir: string,
54-
): Promise<{ filePath: string; contents: string }[]> {
55-
const files = await readdir(dir);
56-
57-
const output = await Promise.all(
58-
files.map(async (file) => {
59-
const filePath = resolve(dir, file);
60-
const fileStat = await stat(filePath);
61-
62-
if (fileStat.isDirectory()) {
63-
return recursiveRead(filePath);
64-
}
65-
66-
return {
67-
filePath: relative(directory, filePath).replaceAll(sep, "/"),
68-
contents: await readFile(filePath, "utf-8"),
69-
};
70-
}),
71-
);
72-
73-
return output.flat();
74-
}
25+
return tree.toString("base64");
7526
};

src/fixtures/file-system.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ export class FileSystem {
1717
*/
1818
async mount(filesOrPath: string | FileSystemTree) {
1919
if (typeof filesOrPath === "string") {
20-
filesOrPath = await commands.readDirectory(filesOrPath);
20+
const tree = await commands.readDirectory(filesOrPath);
21+
const binary = Uint8Array.from(atob(tree), (c) => c.charCodeAt(0));
22+
23+
return await this._instance.mount(binary);
2124
}
2225

23-
return await this._instance.mount(filesOrPath as FileSystemTree);
26+
return await this._instance.mount(filesOrPath);
2427
}
2528

2629
/**

src/fixtures/process.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,48 @@ export class ProcessWrap {
2222
isDone: Promise<void>;
2323

2424
constructor(promise: Promise<WebContainerProcess>) {
25+
let isExitted = false;
2526
let setDone: () => void = () => undefined;
26-
this.isDone = new Promise((resolve) => (setDone = resolve));
27+
28+
this.isDone = new Promise((resolve) => {
29+
setDone = () => {
30+
resolve();
31+
isExitted = true;
32+
};
33+
});
2734

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

32-
webcontainerProcess.exit.then(() => setDone());
39+
webcontainerProcess.exit.then(setDone);
40+
41+
const reader = this._webcontainerProcess.output.getReader();
42+
43+
const read = async () => {
44+
while (true) {
45+
const { done, value } = await reader.read();
46+
47+
if (isExitted && !done) {
48+
console.warn(
49+
`[webcontainer-test]: Closed process keeps writing to output. Closing reader forcefully. \n` +
50+
` Received: "${value}".`,
51+
);
52+
await reader.cancel();
53+
break;
54+
}
55+
56+
// webcontainer process never reaches here, but for future-proofing let's exit
57+
if (done) {
58+
break;
59+
}
60+
61+
this._output += value;
62+
this._listeners.forEach((fn) => fn(value));
63+
}
64+
};
3365

34-
this._webcontainerProcess.output.pipeTo(
35-
new WritableStream({
36-
write: (data) => {
37-
this._output += data;
38-
this._listeners.forEach((fn) => fn(data));
39-
},
40-
}),
41-
);
66+
void read();
4267
});
4368
}
4469

src/types.d.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type {} from "@vitest/browser/context";
2-
import { FileSystemTree } from "@webcontainer/api";
32

43
declare module "@vitest/browser/context" {
54
interface BrowserCommands {
6-
readDirectory: (directory: string) => Promise<FileSystemTree>;
5+
readDirectory: (directory: string) => Promise<string>;
76
}
87
}
116 Bytes
Loading

test/mount.test.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,35 @@
11
import { expect } from "vitest";
22
import { test } from "../src";
33

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

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

12-
const lsNested = await webcontainer.runCommand("ls", ["nested"]);
13-
expect(lsNested).toMatchInlineSnapshot(`"file-2.ts"`);
13+
const lsNested = await webcontainer.runCommand("ls", ["nested"]);
14+
expect(lsNested).toMatchInlineSnapshot(`"file-2.ts"`);
1415

15-
const catFile = await webcontainer.runCommand("cat", ["file-1.ts"]);
16-
expect(catFile).toMatchInlineSnapshot(`"export default "Hello world";"`);
16+
const catFile = await webcontainer.runCommand("cat", ["file-1.ts"]);
17+
expect(catFile).toMatchInlineSnapshot(`"export default "Hello world";"`);
1718

18-
const catNestedFile = await webcontainer.runCommand("cat", [
19-
"nested/file-2.ts",
20-
]);
21-
expect(catNestedFile).toMatchInlineSnapshot(
22-
`"export default "Hello from nested file";"`,
23-
);
24-
});
19+
const catNestedFile = await webcontainer.runCommand("cat", [
20+
"nested/file-2.ts",
21+
]);
22+
expect(catNestedFile).toMatchInlineSnapshot(
23+
`"export default "Hello from nested file";"`,
24+
);
25+
26+
// TODO: Once WebcontainerProcess output resolving is visible, assert whole png content
27+
const pngFile = await webcontainer.runCommand("xxd", ["image.png"]);
28+
expect(pngFile).toContain(
29+
"00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG........IHDR",
30+
);
31+
},
32+
);
2533

2634
test("user can mount inlined FileSystemTree to webcontainer", async ({
2735
webcontainer,

0 commit comments

Comments
 (0)