diff --git a/.changeset/write-files-string-content.md b/.changeset/write-files-string-content.md new file mode 100644 index 00000000..4afef09e --- /dev/null +++ b/.changeset/write-files-string-content.md @@ -0,0 +1,5 @@ +--- +"@vercel/sandbox": patch +--- + +Accept `string` and `Uint8Array` in `writeFiles()` content, not just `Buffer`. diff --git a/packages/vercel-sandbox/src/api-client/api-client.ts b/packages/vercel-sandbox/src/api-client/api-client.ts index e87c392b..1f226a3e 100644 --- a/packages/vercel-sandbox/src/api-client/api-client.ts +++ b/packages/vercel-sandbox/src/api-client/api-client.ts @@ -460,7 +460,11 @@ export class APIClient extends BaseClient { async writeFiles(params: { sandboxId: string; cwd: string; - files: { path: string; content: Buffer; mode?: number }[]; + files: { + path: string; + content: string | Uint8Array; + mode?: number; + }[]; extractDir: string; signal?: AbortSignal; }) { diff --git a/packages/vercel-sandbox/src/api-client/file-writer.test.ts b/packages/vercel-sandbox/src/api-client/file-writer.test.ts index f293d117..facefbe7 100644 --- a/packages/vercel-sandbox/src/api-client/file-writer.test.ts +++ b/packages/vercel-sandbox/src/api-client/file-writer.test.ts @@ -29,9 +29,10 @@ describe("FileWriter", () => { name: "hello.txt", content: Buffer.from("Hello world"), }); - writer.end(); + const end = writer.end(); const files = await extractFiles(writer.readable); + await end; expect(files.get("hello.txt")?.toString()).toBe("Hello world"); }); @@ -42,9 +43,10 @@ describe("FileWriter", () => { name: "utf8.txt", content: Buffer.from(content), }); - writer.end(); + const end = writer.end(); const files = await extractFiles(writer.readable); + await end; expect(files.get("utf8.txt")?.toString()).toBe(content); }); @@ -58,14 +60,71 @@ describe("FileWriter", () => { name: "b.txt", content: Buffer.from("file b"), }); - writer.end(); + const end = writer.end(); const files = await extractFiles(writer.readable); + await end; expect(files.size).toBe(2); expect(files.get("a.txt")?.toString()).toBe("file a"); expect(files.get("b.txt")?.toString()).toBe("file b"); }); + it("writes string content", async () => { + const writer = new FileWriter(); + await writer.addFile({ + name: "hello.txt", + content: "Hello world", + }); + const end = writer.end(); + + const files = await extractFiles(writer.readable); + await end; + expect(files.get("hello.txt")?.toString()).toBe("Hello world"); + }); + + it("writes multi-byte UTF-8 string content", async () => { + const writer = new FileWriter(); + const content = "café ☕ — Grüße aus München 🌍 日本語テスト"; + await writer.addFile({ + name: "utf8.txt", + content, + }); + const end = writer.end(); + + const files = await extractFiles(writer.readable); + await end; + expect(files.get("utf8.txt")?.toString()).toBe(content); + }); + + it("writes Uint8Array content", async () => { + const writer = new FileWriter(); + const content = new TextEncoder().encode("Hello world"); + await writer.addFile({ + name: "hello.txt", + content, + }); + const end = writer.end(); + + const files = await extractFiles(writer.readable); + await end; + expect(files.get("hello.txt")?.toString()).toBe("Hello world"); + }); + + it("writes multi-byte UTF-8 Uint8Array content", async () => { + const writer = new FileWriter(); + const text = "café ☕ — Grüße aus München 🌍 日本語テスト"; + const content = new TextEncoder().encode(text); + await writer.addFile({ + name: "utf8.txt", + content, + }); + const end = writer.end(); + + const files = await extractFiles(writer.readable); + await end; + expect(files.get("utf8.txt")?.toString()).toBe(text); + }); + it("writes stream content with explicit size", async () => { const content = Buffer.from("streamed content"); const writer = new FileWriter(); @@ -74,9 +133,10 @@ describe("FileWriter", () => { content: Readable.from(content), size: content.length, }); - writer.end(); + const end = writer.end(); const files = await extractFiles(writer.readable); + await end; expect(files.get("stream.txt")?.toString()).toBe("streamed content"); }); }); diff --git a/packages/vercel-sandbox/src/api-client/file-writer.ts b/packages/vercel-sandbox/src/api-client/file-writer.ts index 65e94d42..adbc2b4e 100644 --- a/packages/vercel-sandbox/src/api-client/file-writer.ts +++ b/packages/vercel-sandbox/src/api-client/file-writer.ts @@ -2,15 +2,15 @@ import zlib from "zlib"; import tar, { type Pack } from "tar-stream"; import { Readable } from "stream"; -interface FileBuffer { +interface FileData { /** * The name (path) of the file to write. */ name: string; /** - * The content of the file as a Buffer. + * The content of the file. */ - content: Buffer; + content: string | Uint8Array; /** * The file mode (permissions) to set on the file. * For example, 0o755 for executable files. @@ -62,12 +62,16 @@ export class FileWriter { * Returns a Promise resolved once the file is written in the * stream. */ - async addFile(file: FileBuffer | FileStream) { + async addFile(file: FileData | FileStream) { return new Promise((resolve, reject) => { const entry = this.pack.entry( "size" in file ? { name: file.name, size: file.size, mode: file.mode } - : { name: file.name, size: Buffer.byteLength(file.content), mode: file.mode }, + : { + name: file.name, + size: Buffer.byteLength(file.content), + mode: file.mode, + }, (error) => { if (error) { return reject(error); diff --git a/packages/vercel-sandbox/src/sandbox.ts b/packages/vercel-sandbox/src/sandbox.ts index caec91ce..2a895c50 100644 --- a/packages/vercel-sandbox/src/sandbox.ts +++ b/packages/vercel-sandbox/src/sandbox.ts @@ -703,11 +703,15 @@ export class Sandbox { * @example * // Write an executable script * await sandbox.writeFiles([ - * { path: "/usr/local/bin/myscript", content: Buffer.from("#!/bin/bash\necho hello"), mode: 0o755 } + * { path: "/usr/local/bin/myscript", content: "#!/bin/bash\necho hello", mode: 0o755 } * ]); */ async writeFiles( - files: { path: string; content: Buffer; mode?: number }[], + files: { + path: string; + content: string | Uint8Array; + mode?: number; + }[], opts?: { signal?: AbortSignal }, ) { "use step";