@vercel/sandbox 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +16 -0
- package/dist/api-client/api-client.d.ts +13 -8
- package/dist/api-client/api-client.js +25 -4
- package/dist/api-client/api-error.d.ts +0 -1
- package/dist/api-client/base-client.d.ts +1 -2
- package/dist/api-client/base-client.js +1 -5
- package/dist/api-client/validators.d.ts +42 -29
- package/dist/api-client/validators.js +1 -0
- package/dist/api-client/with-retry.d.ts +0 -1
- package/dist/command.d.ts +27 -8
- package/dist/command.js +26 -5
- package/dist/sandbox.d.ts +12 -5
- package/dist/sandbox.js +10 -0
- package/dist/utils/normalizePath.d.ts +17 -0
- package/dist/utils/normalizePath.js +31 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -4
- package/src/api-client/api-client.ts +32 -5
- package/src/api-client/api-error.ts +0 -2
- package/src/api-client/base-client.ts +1 -2
- package/src/api-client/validators.ts +2 -1
- package/src/api-client/with-retry.ts +0 -1
- package/src/command.ts +29 -7
- package/src/sandbox.test.ts +42 -1
- package/src/sandbox.ts +17 -5
- package/src/utils/normalizePath.test.ts +114 -0
- package/src/utils/normalizePath.ts +33 -0
- package/src/version.ts +1 -1
package/dist/command.js
CHANGED
|
@@ -5,8 +5,11 @@ const resolveSignal_1 = require("./utils/resolveSignal");
|
|
|
5
5
|
/**
|
|
6
6
|
* A command executed in a Sandbox.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* For detached commands, you can {@link wait} to get a {@link CommandFinished} instance
|
|
9
|
+
* with the populated exit code. For non-detached commands, {@link Sandbox.runCommand}
|
|
10
|
+
* automatically waits and returns a {@link CommandFinished} instance.
|
|
11
|
+
*
|
|
12
|
+
* You can iterate over command output with {@link logs}.
|
|
10
13
|
*
|
|
11
14
|
* @see {@link Sandbox.runCommand} to start a command.
|
|
12
15
|
*
|
|
@@ -64,9 +67,14 @@ class Command {
|
|
|
64
67
|
/**
|
|
65
68
|
* Wait for a command to exit and populate its exit code.
|
|
66
69
|
*
|
|
70
|
+
* This method is useful for detached commands where you need to wait
|
|
71
|
+
* for completion. For non-detached commands, {@link Sandbox.runCommand}
|
|
72
|
+
* automatically waits and returns a {@link CommandFinished} instance.
|
|
73
|
+
*
|
|
67
74
|
* ```
|
|
68
|
-
* await
|
|
69
|
-
*
|
|
75
|
+
* const detachedCmd = await sandbox.runCommand({ cmd: 'sleep', args: ['5'], detached: true });
|
|
76
|
+
* const result = await detachedCmd.wait();
|
|
77
|
+
* if (result.exitCode !== 0) {
|
|
70
78
|
* console.error("Something went wrong...")
|
|
71
79
|
* }
|
|
72
80
|
* ```
|
|
@@ -145,7 +153,9 @@ exports.Command = Command;
|
|
|
145
153
|
/**
|
|
146
154
|
* A command that has finished executing.
|
|
147
155
|
*
|
|
148
|
-
*
|
|
156
|
+
* The exit code is immediately available and populated upon creation.
|
|
157
|
+
* Unlike {@link Command}, you don't need to call wait() - the command
|
|
158
|
+
* has already completed execution.
|
|
149
159
|
*
|
|
150
160
|
* @hideconstructor
|
|
151
161
|
*/
|
|
@@ -161,5 +171,16 @@ class CommandFinished extends Command {
|
|
|
161
171
|
super({ ...params });
|
|
162
172
|
this.exitCode = params.exitCode;
|
|
163
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* The wait method is not needed for CommandFinished instances since
|
|
176
|
+
* the command has already completed and exitCode is populated.
|
|
177
|
+
*
|
|
178
|
+
* @deprecated This method is redundant for CommandFinished instances.
|
|
179
|
+
* The exitCode is already available.
|
|
180
|
+
* @returns This CommandFinished instance.
|
|
181
|
+
*/
|
|
182
|
+
async wait() {
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
164
185
|
}
|
|
165
186
|
exports.CommandFinished = CommandFinished;
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SandboxMetaData, SandboxRouteData } from "./api-client";
|
|
2
2
|
import type { Writable } from "stream";
|
|
3
3
|
import { APIClient } from "./api-client";
|
|
4
4
|
import { Command, type CommandFinished } from "./command";
|
|
@@ -31,7 +31,8 @@ export interface CreateSandboxParams {
|
|
|
31
31
|
url: string;
|
|
32
32
|
};
|
|
33
33
|
/**
|
|
34
|
-
* Array of port numbers to expose from the sandbox.
|
|
34
|
+
* Array of port numbers to expose from the sandbox. Sandboxes can
|
|
35
|
+
* expose up to 4 ports.
|
|
35
36
|
*/
|
|
36
37
|
ports?: number[];
|
|
37
38
|
/**
|
|
@@ -113,7 +114,11 @@ export declare class Sandbox {
|
|
|
113
114
|
*/
|
|
114
115
|
get sandboxId(): string;
|
|
115
116
|
/**
|
|
116
|
-
*
|
|
117
|
+
* The status of the sandbox.
|
|
118
|
+
*/
|
|
119
|
+
get status(): SandboxMetaData["status"];
|
|
120
|
+
/**
|
|
121
|
+
* Internal metadata about this sandbox.
|
|
117
122
|
*/
|
|
118
123
|
private readonly sandbox;
|
|
119
124
|
/**
|
|
@@ -140,7 +145,7 @@ export declare class Sandbox {
|
|
|
140
145
|
constructor({ client, routes, sandbox, }: {
|
|
141
146
|
client: APIClient;
|
|
142
147
|
routes: SandboxRouteData[];
|
|
143
|
-
sandbox:
|
|
148
|
+
sandbox: SandboxMetaData;
|
|
144
149
|
});
|
|
145
150
|
/**
|
|
146
151
|
* Get a previously run command by its ID.
|
|
@@ -180,7 +185,7 @@ export declare class Sandbox {
|
|
|
180
185
|
* @returns A {@link Command} or {@link CommandFinished}, depending on `detached`.
|
|
181
186
|
* @internal
|
|
182
187
|
*/
|
|
183
|
-
_runCommand(params: RunCommandParams): Promise<
|
|
188
|
+
_runCommand(params: RunCommandParams): Promise<Command | CommandFinished>;
|
|
184
189
|
/**
|
|
185
190
|
* Create a directory in the filesystem of this sandbox.
|
|
186
191
|
*
|
|
@@ -199,6 +204,8 @@ export declare class Sandbox {
|
|
|
199
204
|
}): Promise<NodeJS.ReadableStream | null>;
|
|
200
205
|
/**
|
|
201
206
|
* Write files to the filesystem of this sandbox.
|
|
207
|
+
* Defaults to writing to /vercel/sandbox unless an absolute path is specified.
|
|
208
|
+
* Writes files using the `vercel-sandbox` user.
|
|
202
209
|
*
|
|
203
210
|
* @param files - Array of files with path and stream/buffer contents
|
|
204
211
|
* @returns A promise that resolves when the files are written
|
package/dist/sandbox.js
CHANGED
|
@@ -17,6 +17,12 @@ class Sandbox {
|
|
|
17
17
|
get sandboxId() {
|
|
18
18
|
return this.sandbox.id;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* The status of the sandbox.
|
|
22
|
+
*/
|
|
23
|
+
get status() {
|
|
24
|
+
return this.sandbox.status;
|
|
25
|
+
}
|
|
20
26
|
/**
|
|
21
27
|
* Create a new sandbox.
|
|
22
28
|
*
|
|
@@ -159,6 +165,8 @@ class Sandbox {
|
|
|
159
165
|
}
|
|
160
166
|
/**
|
|
161
167
|
* Write files to the filesystem of this sandbox.
|
|
168
|
+
* Defaults to writing to /vercel/sandbox unless an absolute path is specified.
|
|
169
|
+
* Writes files using the `vercel-sandbox` user.
|
|
162
170
|
*
|
|
163
171
|
* @param files - Array of files with path and stream/buffer contents
|
|
164
172
|
* @returns A promise that resolves when the files are written
|
|
@@ -166,6 +174,8 @@ class Sandbox {
|
|
|
166
174
|
async writeFiles(files) {
|
|
167
175
|
return this.client.writeFiles({
|
|
168
176
|
sandboxId: this.sandbox.id,
|
|
177
|
+
cwd: this.sandbox.cwd,
|
|
178
|
+
extractDir: "/",
|
|
169
179
|
files: files,
|
|
170
180
|
});
|
|
171
181
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a path and make it relative to `params.extractDir` for inclusion
|
|
3
|
+
* in our tar archives.
|
|
4
|
+
*
|
|
5
|
+
* Relative paths are first resolved to `params.cwd`.
|
|
6
|
+
* Absolute paths are normalized and resolved relative to `params.extractDir`.
|
|
7
|
+
*
|
|
8
|
+
* In addition, paths are normalized so consecutive slashes are removed and
|
|
9
|
+
* stuff like `../..` is resolved appropriately.
|
|
10
|
+
*
|
|
11
|
+
* This function always returns a path relative to `params.extractDir`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizePath(params: {
|
|
14
|
+
filePath: string;
|
|
15
|
+
cwd: string;
|
|
16
|
+
extractDir: string;
|
|
17
|
+
}): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.normalizePath = normalizePath;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a path and make it relative to `params.extractDir` for inclusion
|
|
10
|
+
* in our tar archives.
|
|
11
|
+
*
|
|
12
|
+
* Relative paths are first resolved to `params.cwd`.
|
|
13
|
+
* Absolute paths are normalized and resolved relative to `params.extractDir`.
|
|
14
|
+
*
|
|
15
|
+
* In addition, paths are normalized so consecutive slashes are removed and
|
|
16
|
+
* stuff like `../..` is resolved appropriately.
|
|
17
|
+
*
|
|
18
|
+
* This function always returns a path relative to `params.extractDir`.
|
|
19
|
+
*/
|
|
20
|
+
function normalizePath(params) {
|
|
21
|
+
if (!path_1.default.posix.isAbsolute(params.cwd)) {
|
|
22
|
+
throw new Error("cwd dir must be absolute");
|
|
23
|
+
}
|
|
24
|
+
if (!path_1.default.posix.isAbsolute(params.extractDir)) {
|
|
25
|
+
throw new Error("extractDir must be absolute");
|
|
26
|
+
}
|
|
27
|
+
const basePath = path_1.default.posix.isAbsolute(params.filePath)
|
|
28
|
+
? path_1.default.posix.normalize(params.filePath)
|
|
29
|
+
: path_1.default.posix.join(params.cwd, params.filePath);
|
|
30
|
+
return path_1.default.posix.relative(params.extractDir, basePath);
|
|
31
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.0.
|
|
1
|
+
export declare const VERSION = "0.0.17";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/sandbox",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -11,10 +11,8 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@vercel/oidc": "^2.0.0",
|
|
13
13
|
"async-retry": "1.3.3",
|
|
14
|
-
"form-data": "3.0.0",
|
|
15
14
|
"jsonlines": "0.1.1",
|
|
16
15
|
"ms": "2.1.3",
|
|
17
|
-
"node-fetch": "2.6.11",
|
|
18
16
|
"tar-stream": "3.1.7",
|
|
19
17
|
"zod": "3.24.4"
|
|
20
18
|
},
|
|
@@ -23,7 +21,6 @@
|
|
|
23
21
|
"@types/jsonlines": "0.1.5",
|
|
24
22
|
"@types/ms": "2.1.0",
|
|
25
23
|
"@types/node": "22.15.12",
|
|
26
|
-
"@types/node-fetch": "2.6.12",
|
|
27
24
|
"@types/tar-stream": "3.1.4",
|
|
28
25
|
"dotenv": "16.5.0",
|
|
29
26
|
"typedoc": "0.28.5",
|
|
@@ -19,6 +19,8 @@ import { consumeReadable } from "../utils/consume-readable";
|
|
|
19
19
|
import { z } from "zod";
|
|
20
20
|
import jsonlines from "jsonlines";
|
|
21
21
|
import os from "os";
|
|
22
|
+
import { Readable } from "stream";
|
|
23
|
+
import { normalizePath } from "../utils/normalizePath";
|
|
22
24
|
|
|
23
25
|
export class APIClient extends BaseClient {
|
|
24
26
|
private teamId: string;
|
|
@@ -149,13 +151,16 @@ export class APIClient extends BaseClient {
|
|
|
149
151
|
);
|
|
150
152
|
}
|
|
151
153
|
|
|
152
|
-
getFileWriter(params: { sandboxId: string }) {
|
|
154
|
+
getFileWriter(params: { sandboxId: string; extractDir: string }) {
|
|
153
155
|
const writer = new FileWriter();
|
|
154
156
|
return {
|
|
155
157
|
response: (async () => {
|
|
156
158
|
return this.request(`/v1/sandboxes/${params.sandboxId}/fs/write`, {
|
|
157
159
|
method: "POST",
|
|
158
|
-
headers: {
|
|
160
|
+
headers: {
|
|
161
|
+
"content-type": "application/gzip",
|
|
162
|
+
"x-cwd": params.extractDir,
|
|
163
|
+
},
|
|
159
164
|
body: await consumeReadable(writer.readable),
|
|
160
165
|
});
|
|
161
166
|
})(),
|
|
@@ -165,14 +170,24 @@ export class APIClient extends BaseClient {
|
|
|
165
170
|
|
|
166
171
|
async writeFiles(params: {
|
|
167
172
|
sandboxId: string;
|
|
173
|
+
cwd: string;
|
|
168
174
|
files: { path: string; content: Buffer }[];
|
|
175
|
+
extractDir: string;
|
|
169
176
|
}) {
|
|
170
177
|
const { writer, response } = this.getFileWriter({
|
|
171
178
|
sandboxId: params.sandboxId,
|
|
179
|
+
extractDir: params.extractDir,
|
|
172
180
|
});
|
|
173
181
|
|
|
174
182
|
for (const file of params.files) {
|
|
175
|
-
await writer.addFile({
|
|
183
|
+
await writer.addFile({
|
|
184
|
+
name: normalizePath({
|
|
185
|
+
filePath: file.path,
|
|
186
|
+
extractDir: params.extractDir,
|
|
187
|
+
cwd: params.cwd,
|
|
188
|
+
}),
|
|
189
|
+
content: file.content,
|
|
190
|
+
});
|
|
176
191
|
}
|
|
177
192
|
|
|
178
193
|
writer.end();
|
|
@@ -196,7 +211,11 @@ export class APIClient extends BaseClient {
|
|
|
196
211
|
return null;
|
|
197
212
|
}
|
|
198
213
|
|
|
199
|
-
|
|
214
|
+
if (response.body === null) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return Readable.fromWeb(response.body);
|
|
200
219
|
}
|
|
201
220
|
|
|
202
221
|
async killCommand(params: {
|
|
@@ -228,7 +247,15 @@ export class APIClient extends BaseClient {
|
|
|
228
247
|
});
|
|
229
248
|
}
|
|
230
249
|
|
|
231
|
-
|
|
250
|
+
if (response.body === null) {
|
|
251
|
+
throw new APIError(response, {
|
|
252
|
+
message: "No response body",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for await (const chunk of Readable.fromWeb(response.body).pipe(
|
|
257
|
+
jsonlines.parse(),
|
|
258
|
+
)) {
|
|
232
259
|
yield LogLine.parse(chunk);
|
|
233
260
|
}
|
|
234
261
|
}
|
|
@@ -3,7 +3,6 @@ import { APIError } from "./api-error";
|
|
|
3
3
|
import { ZodType } from "zod";
|
|
4
4
|
import { array } from "../utils/array";
|
|
5
5
|
import { withRetry, type RequestOptions } from "./with-retry";
|
|
6
|
-
import nodeFetch, { type Response, type RequestInit } from "node-fetch";
|
|
7
6
|
|
|
8
7
|
export interface RequestParams extends RequestInit {
|
|
9
8
|
headers?: Record<string, string>;
|
|
@@ -25,7 +24,7 @@ export class BaseClient {
|
|
|
25
24
|
private host: string;
|
|
26
25
|
|
|
27
26
|
constructor(params: { debug?: boolean; host: string; token?: string }) {
|
|
28
|
-
this.fetch = withRetry(
|
|
27
|
+
this.fetch = withRetry(globalThis.fetch);
|
|
29
28
|
this.host = params.host;
|
|
30
29
|
this.debug = params.debug ?? process.env.DEBUG_FETCH === "true";
|
|
31
30
|
this.token = params.token;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
export type
|
|
3
|
+
export type SandboxMetaData = z.infer<typeof Sandbox>;
|
|
4
4
|
|
|
5
5
|
export const Sandbox = z.object({
|
|
6
6
|
id: z.string(),
|
|
@@ -16,6 +16,7 @@ export const Sandbox = z.object({
|
|
|
16
16
|
stoppedAt: z.number().optional(),
|
|
17
17
|
duration: z.number().optional(),
|
|
18
18
|
createdAt: z.number(),
|
|
19
|
+
cwd: z.string(),
|
|
19
20
|
updatedAt: z.number(),
|
|
20
21
|
});
|
|
21
22
|
|
package/src/command.ts
CHANGED
|
@@ -4,8 +4,11 @@ import { Signal, resolveSignal } from "./utils/resolveSignal";
|
|
|
4
4
|
/**
|
|
5
5
|
* A command executed in a Sandbox.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* For detached commands, you can {@link wait} to get a {@link CommandFinished} instance
|
|
8
|
+
* with the populated exit code. For non-detached commands, {@link Sandbox.runCommand}
|
|
9
|
+
* automatically waits and returns a {@link CommandFinished} instance.
|
|
10
|
+
*
|
|
11
|
+
* You can iterate over command output with {@link logs}.
|
|
9
12
|
*
|
|
10
13
|
* @see {@link Sandbox.runCommand} to start a command.
|
|
11
14
|
*
|
|
@@ -94,9 +97,14 @@ export class Command {
|
|
|
94
97
|
/**
|
|
95
98
|
* Wait for a command to exit and populate its exit code.
|
|
96
99
|
*
|
|
100
|
+
* This method is useful for detached commands where you need to wait
|
|
101
|
+
* for completion. For non-detached commands, {@link Sandbox.runCommand}
|
|
102
|
+
* automatically waits and returns a {@link CommandFinished} instance.
|
|
103
|
+
*
|
|
97
104
|
* ```
|
|
98
|
-
* await
|
|
99
|
-
*
|
|
105
|
+
* const detachedCmd = await sandbox.runCommand({ cmd: 'sleep', args: ['5'], detached: true });
|
|
106
|
+
* const result = await detachedCmd.wait();
|
|
107
|
+
* if (result.exitCode !== 0) {
|
|
100
108
|
* console.error("Something went wrong...")
|
|
101
109
|
* }
|
|
102
110
|
* ```
|
|
@@ -180,14 +188,16 @@ export class Command {
|
|
|
180
188
|
/**
|
|
181
189
|
* A command that has finished executing.
|
|
182
190
|
*
|
|
183
|
-
*
|
|
191
|
+
* The exit code is immediately available and populated upon creation.
|
|
192
|
+
* Unlike {@link Command}, you don't need to call wait() - the command
|
|
193
|
+
* has already completed execution.
|
|
184
194
|
*
|
|
185
195
|
* @hideconstructor
|
|
186
196
|
*/
|
|
187
197
|
export class CommandFinished extends Command {
|
|
188
198
|
/**
|
|
189
|
-
* The exit code of the command
|
|
190
|
-
*
|
|
199
|
+
* The exit code of the command. This is always populated for
|
|
200
|
+
* CommandFinished instances.
|
|
191
201
|
*/
|
|
192
202
|
public exitCode: number;
|
|
193
203
|
|
|
@@ -207,4 +217,16 @@ export class CommandFinished extends Command {
|
|
|
207
217
|
super({ ...params });
|
|
208
218
|
this.exitCode = params.exitCode;
|
|
209
219
|
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The wait method is not needed for CommandFinished instances since
|
|
223
|
+
* the command has already completed and exitCode is populated.
|
|
224
|
+
*
|
|
225
|
+
* @deprecated This method is redundant for CommandFinished instances.
|
|
226
|
+
* The exitCode is already available.
|
|
227
|
+
* @returns This CommandFinished instance.
|
|
228
|
+
*/
|
|
229
|
+
async wait(): Promise<CommandFinished> {
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
210
232
|
}
|
package/src/sandbox.test.ts
CHANGED
|
@@ -2,10 +2,11 @@ import { it, beforeEach, afterEach, expect } from "vitest";
|
|
|
2
2
|
import { consumeReadable } from "./utils/consume-readable";
|
|
3
3
|
import { Sandbox } from "./sandbox";
|
|
4
4
|
|
|
5
|
+
const PORTS = [3000, 4000];
|
|
5
6
|
let sandbox: Sandbox;
|
|
6
7
|
|
|
7
8
|
beforeEach(async () => {
|
|
8
|
-
sandbox = await Sandbox.create();
|
|
9
|
+
sandbox = await Sandbox.create({ ports: PORTS });
|
|
9
10
|
});
|
|
10
11
|
|
|
11
12
|
afterEach(async () => {
|
|
@@ -23,3 +24,43 @@ it("allows to write files and then read them", async () => {
|
|
|
23
24
|
expect((await consumeReadable(content1!)).toString()).toBe("Hello 1");
|
|
24
25
|
expect((await consumeReadable(content2!)).toString()).toBe("Hello 2");
|
|
25
26
|
});
|
|
27
|
+
|
|
28
|
+
it("verifies port forwarding works correctly", async () => {
|
|
29
|
+
const serverScript = `
|
|
30
|
+
const http = require('http');
|
|
31
|
+
const ports = process.argv.slice(2);
|
|
32
|
+
|
|
33
|
+
for (const port of ports) {
|
|
34
|
+
const server = http.createServer((req, res) => {
|
|
35
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
36
|
+
res.end(\`hello port \${port}\`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
server.listen(port, () => {
|
|
40
|
+
console.log(\`Server running on port \${port}\`);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
await sandbox.writeFiles([
|
|
46
|
+
{ path: "server.js", content: Buffer.from(serverScript) },
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const server = await sandbox.runCommand({
|
|
50
|
+
cmd: "node",
|
|
51
|
+
args: ["server.js", ...PORTS.map(String)],
|
|
52
|
+
detached: true,
|
|
53
|
+
stdout: process.stdout,
|
|
54
|
+
stderr: process.stderr,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
58
|
+
|
|
59
|
+
for (const port of PORTS) {
|
|
60
|
+
const response = await fetch(sandbox.domain(port));
|
|
61
|
+
const text = await response.text();
|
|
62
|
+
expect(text).toBe(`hello port ${port}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await server.kill();
|
|
66
|
+
});
|
package/src/sandbox.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SandboxMetaData, SandboxRouteData } from "./api-client";
|
|
2
2
|
import type { Writable } from "stream";
|
|
3
3
|
import { APIClient } from "./api-client";
|
|
4
4
|
import { Command, type CommandFinished } from "./command";
|
|
@@ -32,7 +32,8 @@ export interface CreateSandboxParams {
|
|
|
32
32
|
}
|
|
33
33
|
| { type: "tarball"; url: string };
|
|
34
34
|
/**
|
|
35
|
-
* Array of port numbers to expose from the sandbox.
|
|
35
|
+
* Array of port numbers to expose from the sandbox. Sandboxes can
|
|
36
|
+
* expose up to 4 ports.
|
|
36
37
|
*/
|
|
37
38
|
ports?: number[];
|
|
38
39
|
/**
|
|
@@ -121,9 +122,16 @@ export class Sandbox {
|
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
/**
|
|
124
|
-
*
|
|
125
|
+
* The status of the sandbox.
|
|
125
126
|
*/
|
|
126
|
-
|
|
127
|
+
public get status(): SandboxMetaData["status"] {
|
|
128
|
+
return this.sandbox.status;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Internal metadata about this sandbox.
|
|
133
|
+
*/
|
|
134
|
+
private readonly sandbox: SandboxMetaData;
|
|
127
135
|
|
|
128
136
|
/**
|
|
129
137
|
* Create a new sandbox.
|
|
@@ -196,7 +204,7 @@ export class Sandbox {
|
|
|
196
204
|
}: {
|
|
197
205
|
client: APIClient;
|
|
198
206
|
routes: SandboxRouteData[];
|
|
199
|
-
sandbox:
|
|
207
|
+
sandbox: SandboxMetaData;
|
|
200
208
|
}) {
|
|
201
209
|
this.client = client;
|
|
202
210
|
this.routes = routes;
|
|
@@ -324,6 +332,8 @@ export class Sandbox {
|
|
|
324
332
|
|
|
325
333
|
/**
|
|
326
334
|
* Write files to the filesystem of this sandbox.
|
|
335
|
+
* Defaults to writing to /vercel/sandbox unless an absolute path is specified.
|
|
336
|
+
* Writes files using the `vercel-sandbox` user.
|
|
327
337
|
*
|
|
328
338
|
* @param files - Array of files with path and stream/buffer contents
|
|
329
339
|
* @returns A promise that resolves when the files are written
|
|
@@ -331,6 +341,8 @@ export class Sandbox {
|
|
|
331
341
|
async writeFiles(files: { path: string; content: Buffer }[]) {
|
|
332
342
|
return this.client.writeFiles({
|
|
333
343
|
sandboxId: this.sandbox.id,
|
|
344
|
+
cwd: this.sandbox.cwd,
|
|
345
|
+
extractDir: "/",
|
|
334
346
|
files: files,
|
|
335
347
|
});
|
|
336
348
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { normalizePath } from "./normalizePath";
|
|
3
|
+
|
|
4
|
+
describe("normalizePath", () => {
|
|
5
|
+
it("handles base cases", () => {
|
|
6
|
+
expect(
|
|
7
|
+
normalizePath({
|
|
8
|
+
filePath: "foo.txt",
|
|
9
|
+
cwd: "/vercel/sandbox",
|
|
10
|
+
extractDir: "/",
|
|
11
|
+
}),
|
|
12
|
+
).toBe("vercel/sandbox/foo.txt");
|
|
13
|
+
expect(
|
|
14
|
+
normalizePath({
|
|
15
|
+
filePath: "foo/bar/baz.txt",
|
|
16
|
+
cwd: "/vercel/sandbox",
|
|
17
|
+
extractDir: "/",
|
|
18
|
+
}),
|
|
19
|
+
).toBe("vercel/sandbox/foo/bar/baz.txt");
|
|
20
|
+
expect(
|
|
21
|
+
normalizePath({ filePath: "bar.txt", cwd: "/", extractDir: "/" }),
|
|
22
|
+
).toBe("bar.txt");
|
|
23
|
+
expect(
|
|
24
|
+
normalizePath({
|
|
25
|
+
filePath: "/some/other/dir/foo.txt",
|
|
26
|
+
cwd: "/bar",
|
|
27
|
+
extractDir: "/",
|
|
28
|
+
}),
|
|
29
|
+
).toBe("some/other/dir/foo.txt");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("handles base cases (extract dir)", () => {
|
|
33
|
+
expect(
|
|
34
|
+
normalizePath({
|
|
35
|
+
filePath: "foo.txt",
|
|
36
|
+
cwd: "/vercel/sandbox",
|
|
37
|
+
extractDir: "/vercel",
|
|
38
|
+
}),
|
|
39
|
+
).toBe("sandbox/foo.txt");
|
|
40
|
+
expect(
|
|
41
|
+
normalizePath({
|
|
42
|
+
filePath: "foo/bar/baz.txt",
|
|
43
|
+
cwd: "/vercel/sandbox",
|
|
44
|
+
extractDir: "/vercel",
|
|
45
|
+
}),
|
|
46
|
+
).toBe("sandbox/foo/bar/baz.txt");
|
|
47
|
+
|
|
48
|
+
// TODO: Should this be allowed?
|
|
49
|
+
expect(
|
|
50
|
+
normalizePath({ filePath: "bar.txt", cwd: "/", extractDir: "/vercel" }),
|
|
51
|
+
).toBe("../bar.txt");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("handles normalization", () => {
|
|
55
|
+
expect(
|
|
56
|
+
normalizePath({
|
|
57
|
+
filePath: "/resolves/../this/stuff/foo.txt",
|
|
58
|
+
cwd: "/bar",
|
|
59
|
+
extractDir: "/",
|
|
60
|
+
}),
|
|
61
|
+
).toBe("this/stuff/foo.txt");
|
|
62
|
+
expect(
|
|
63
|
+
normalizePath({
|
|
64
|
+
filePath: "/handles//extra-slashes",
|
|
65
|
+
cwd: "/",
|
|
66
|
+
extractDir: "/",
|
|
67
|
+
}),
|
|
68
|
+
).toBe("handles/extra-slashes");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("resolves relative paths", () => {
|
|
72
|
+
expect(
|
|
73
|
+
normalizePath({
|
|
74
|
+
filePath: "/../../../foo.txt",
|
|
75
|
+
cwd: "/",
|
|
76
|
+
extractDir: "/",
|
|
77
|
+
}),
|
|
78
|
+
).toBe("foo.txt");
|
|
79
|
+
expect(
|
|
80
|
+
normalizePath({
|
|
81
|
+
filePath: "../../../../foo.txt",
|
|
82
|
+
cwd: "/vercel/sandbox",
|
|
83
|
+
extractDir: "/",
|
|
84
|
+
}),
|
|
85
|
+
).toBe("foo.txt");
|
|
86
|
+
expect(
|
|
87
|
+
normalizePath({
|
|
88
|
+
filePath: "../foo.txt",
|
|
89
|
+
cwd: "/vercel/sandbox",
|
|
90
|
+
extractDir: "/",
|
|
91
|
+
}),
|
|
92
|
+
).toBe("vercel/foo.txt");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("validates the cwd", () => {
|
|
96
|
+
expect(() => {
|
|
97
|
+
normalizePath({
|
|
98
|
+
filePath: "doesn't matter",
|
|
99
|
+
cwd: "relative/root",
|
|
100
|
+
extractDir: "/",
|
|
101
|
+
});
|
|
102
|
+
}).toThrow("cwd dir must be absolute");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("validates the cwd", () => {
|
|
106
|
+
expect(() => {
|
|
107
|
+
normalizePath({
|
|
108
|
+
filePath: "doesn't matter",
|
|
109
|
+
cwd: "/",
|
|
110
|
+
extractDir: "some/relative/path",
|
|
111
|
+
});
|
|
112
|
+
}).toThrow("extractDir must be absolute");
|
|
113
|
+
});
|
|
114
|
+
});
|