@prisma/compute-sdk 0.1.0

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.
Files changed (52) hide show
  1. package/README.md +638 -0
  2. package/dist/api-client.d.ts +181 -0
  3. package/dist/api-client.d.ts.map +1 -0
  4. package/dist/api-client.js +193 -0
  5. package/dist/api-client.js.map +1 -0
  6. package/dist/archive.d.ts +11 -0
  7. package/dist/archive.d.ts.map +1 -0
  8. package/dist/archive.js +80 -0
  9. package/dist/archive.js.map +1 -0
  10. package/dist/build-strategy.d.ts +34 -0
  11. package/dist/build-strategy.d.ts.map +1 -0
  12. package/dist/build-strategy.js +33 -0
  13. package/dist/build-strategy.js.map +1 -0
  14. package/dist/bun-build.d.ts +14 -0
  15. package/dist/bun-build.d.ts.map +1 -0
  16. package/dist/bun-build.js +128 -0
  17. package/dist/bun-build.js.map +1 -0
  18. package/dist/callbacks.d.ts +44 -0
  19. package/dist/callbacks.d.ts.map +1 -0
  20. package/dist/callbacks.js +2 -0
  21. package/dist/callbacks.js.map +1 -0
  22. package/dist/compute-client.d.ts +136 -0
  23. package/dist/compute-client.d.ts.map +1 -0
  24. package/dist/compute-client.js +562 -0
  25. package/dist/compute-client.js.map +1 -0
  26. package/dist/errors.d.ts +101 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +56 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/index.d.ts +12 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +10 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/polling.d.ts +22 -0
  35. package/dist/polling.d.ts.map +1 -0
  36. package/dist/polling.js +64 -0
  37. package/dist/polling.js.map +1 -0
  38. package/dist/types.d.ts +46 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +17 -0
  41. package/dist/types.js.map +1 -0
  42. package/package.json +43 -0
  43. package/src/api-client.ts +330 -0
  44. package/src/archive.ts +111 -0
  45. package/src/build-strategy.ts +66 -0
  46. package/src/bun-build.ts +184 -0
  47. package/src/callbacks.ts +50 -0
  48. package/src/compute-client.ts +985 -0
  49. package/src/errors.ts +148 -0
  50. package/src/index.ts +75 -0
  51. package/src/polling.ts +108 -0
  52. package/src/types.ts +65 -0
@@ -0,0 +1,330 @@
1
+ import type { ManagementApiClient, paths } from "@prisma/management-api-sdk";
2
+ import { Result } from "better-result";
3
+ import { ApiError, AuthenticationError } from "./errors.ts";
4
+
5
+ export type ApiClientError = AuthenticationError | ApiError;
6
+
7
+ /**
8
+ * Lean internal wrapper around ManagementApiClient.
9
+ *
10
+ * Provides typed, domain-specific methods with structured error handling.
11
+ * Has no knowledge of OAuth, service tokens, or token storage — it simply
12
+ * uses the ManagementApiClient it was given.
13
+ */
14
+ export class InternalApiClient {
15
+ readonly #client: ManagementApiClient;
16
+
17
+ constructor(client: ManagementApiClient) {
18
+ this.#client = client;
19
+ }
20
+
21
+ async listProjects(signal?: AbortSignal) {
22
+ return this.#requestPaginated(
23
+ (cursor) =>
24
+ this.#client.GET("/v1/projects", {
25
+ params: { query: cursor ? { cursor } : {} },
26
+ signal,
27
+ }),
28
+ "/v1/projects",
29
+ );
30
+ }
31
+
32
+ async getProject(id: string, signal?: AbortSignal) {
33
+ return this.#request(
34
+ this.#client.GET("/v1/projects/{id}", {
35
+ params: { path: { id } },
36
+ signal,
37
+ }),
38
+ "/v1/projects/{id}",
39
+ ).then((r) => r.map((body) => body.data));
40
+ }
41
+
42
+ async listProjectServices(projectId: string, signal?: AbortSignal) {
43
+ return this.#requestPaginated(
44
+ (cursor) =>
45
+ this.#client.GET("/v1/projects/{projectId}/compute-services", {
46
+ params: { path: { projectId }, query: cursor ? { cursor } : {} },
47
+ signal,
48
+ }),
49
+ "/v1/projects/{projectId}/compute-services",
50
+ );
51
+ }
52
+
53
+ async createProjectService(
54
+ projectId: string,
55
+ body: CreateProjectServiceBody,
56
+ signal?: AbortSignal,
57
+ ) {
58
+ return this.#request(
59
+ this.#client.POST("/v1/projects/{projectId}/compute-services", {
60
+ params: { path: { projectId } },
61
+ body: body as SdkCreateProjectServiceBody,
62
+ signal,
63
+ }),
64
+ "/v1/projects/{projectId}/compute-services",
65
+ ).then((r) => r.map((body) => body.data));
66
+ }
67
+
68
+ async getService(computeServiceId: string, signal?: AbortSignal) {
69
+ return this.#request(
70
+ this.#client.GET("/v1/compute-services/{computeServiceId}", {
71
+ params: { path: { computeServiceId } },
72
+ signal,
73
+ }),
74
+ "/v1/compute-services/{computeServiceId}",
75
+ ).then((r) => r.map((body) => body.data));
76
+ }
77
+
78
+ async deleteService(
79
+ computeServiceId: string,
80
+ signal?: AbortSignal,
81
+ ): Promise<Result<void, ApiClientError>> {
82
+ return this.#unwrapVoid(
83
+ this.#client.DELETE("/v1/compute-services/{computeServiceId}", {
84
+ params: { path: { computeServiceId } },
85
+ signal,
86
+ }),
87
+ );
88
+ }
89
+
90
+ async listServiceVersions(computeServiceId: string, signal?: AbortSignal) {
91
+ return this.#requestPaginated(
92
+ (cursor) =>
93
+ this.#client.GET("/v1/compute-services/{computeServiceId}/versions", {
94
+ params: {
95
+ path: { computeServiceId },
96
+ query: cursor ? { cursor } : {},
97
+ },
98
+ signal,
99
+ }),
100
+ "/v1/compute-services/{computeServiceId}/versions",
101
+ );
102
+ }
103
+
104
+ async createServiceVersion(
105
+ computeServiceId: string,
106
+ body?: CreateServiceVersionBody,
107
+ signal?: AbortSignal,
108
+ ) {
109
+ return this.#request(
110
+ this.#client.POST("/v1/compute-services/{computeServiceId}/versions", {
111
+ params: { path: { computeServiceId } },
112
+ body,
113
+ signal,
114
+ }),
115
+ "/v1/compute-services/{computeServiceId}/versions",
116
+ ).then((r) => r.map((body) => body.data));
117
+ }
118
+
119
+ async getVersion(versionId: string, signal?: AbortSignal) {
120
+ return this.#request(
121
+ this.#client.GET("/v1/compute-services/versions/{versionId}", {
122
+ params: { path: { versionId } },
123
+ signal,
124
+ }),
125
+ "/v1/compute-services/versions/{versionId}",
126
+ ).then((r) => r.map((body) => body.data));
127
+ }
128
+
129
+ async startVersion(versionId: string, signal?: AbortSignal) {
130
+ return this.#request(
131
+ this.#client.POST("/v1/compute-services/versions/{versionId}/start", {
132
+ params: { path: { versionId } },
133
+ signal,
134
+ }),
135
+ "/v1/compute-services/versions/{versionId}/start",
136
+ ).then((r) => r.map((body) => body.data));
137
+ }
138
+
139
+ async stopVersion(
140
+ versionId: string,
141
+ signal?: AbortSignal,
142
+ ): Promise<Result<void, ApiClientError>> {
143
+ return this.#unwrapVoid(
144
+ this.#client.POST("/v1/compute-services/versions/{versionId}/stop", {
145
+ params: { path: { versionId } },
146
+ signal,
147
+ }),
148
+ );
149
+ }
150
+
151
+ async deleteVersion(
152
+ versionId: string,
153
+ signal?: AbortSignal,
154
+ ): Promise<Result<void, ApiClientError>> {
155
+ return this.#unwrapVoid(
156
+ this.#client.DELETE("/v1/compute-services/versions/{versionId}", {
157
+ params: { path: { versionId } },
158
+ signal,
159
+ }),
160
+ );
161
+ }
162
+
163
+ async #request<TData>(
164
+ request: Promise<RequestResult<TData>>,
165
+ endpoint: string,
166
+ ): Promise<Result<NonNullable<TData>, ApiClientError>> {
167
+ const result = await request;
168
+ if (result.error) {
169
+ return Result.err(parseApiError(result.error, result.response));
170
+ }
171
+ const body = result.data;
172
+ if (body == null) {
173
+ return Result.err(
174
+ parseApiError(
175
+ undefined,
176
+ undefined,
177
+ `Management API returned an empty response body for ${endpoint}`,
178
+ ),
179
+ );
180
+ }
181
+ return Result.ok(body);
182
+ }
183
+
184
+ async #requestPaginated<TItem>(
185
+ makeRequest: (cursor: string | undefined) => Promise<
186
+ RequestResult<{
187
+ data: TItem[];
188
+ pagination: { nextCursor: string | null; hasMore: boolean };
189
+ }>
190
+ >,
191
+ endpoint: string,
192
+ ): Promise<Result<TItem[], ApiClientError>> {
193
+ const allItems: TItem[] = [];
194
+ let cursor: string | undefined;
195
+
196
+ for (;;) {
197
+ const result = await this.#request(makeRequest(cursor), endpoint);
198
+ if (result.isErr()) return result;
199
+
200
+ allItems.push(...result.value.data);
201
+
202
+ const nextCursor = result.value.pagination?.nextCursor;
203
+ if (!nextCursor) break;
204
+ cursor = nextCursor;
205
+ }
206
+
207
+ return Result.ok(allItems);
208
+ }
209
+
210
+ async #unwrapVoid(
211
+ request: Promise<RequestResult<unknown>>,
212
+ ): Promise<Result<void, ApiClientError>> {
213
+ const result = await request;
214
+ if (result.error) {
215
+ return Result.err(parseApiError(result.error, result.response));
216
+ }
217
+ return Result.ok(undefined);
218
+ }
219
+ }
220
+
221
+ type RequestResult<TData> =
222
+ | { data: TData; error?: never; response: Response }
223
+ | { data?: never; error: unknown; response: Response };
224
+
225
+ type SdkCreateProjectServiceBody = NonNullable<
226
+ paths["/v1/projects/{projectId}/compute-services"]["post"]["requestBody"]
227
+ >["content"]["application/json"];
228
+
229
+ type CreateProjectServiceBody = Omit<
230
+ SdkCreateProjectServiceBody,
231
+ "regionId"
232
+ > & {
233
+ regionId?: string;
234
+ };
235
+
236
+ export type CreateServiceVersionBody = NonNullable<
237
+ paths["/v1/compute-services/{computeServiceId}/versions"]["post"]["requestBody"]
238
+ >["content"]["application/json"];
239
+
240
+ const TRACE_HEADER_NAMES = [
241
+ "x-request-id",
242
+ "x-correlation-id",
243
+ "x-amzn-requestid",
244
+ "x-amz-request-id",
245
+ "traceparent",
246
+ "cf-ray",
247
+ "cf-placement",
248
+ ] as const;
249
+
250
+ function extractTraceHeaders(response?: Response): Record<string, string> {
251
+ const headers: Record<string, string> = {};
252
+ if (!response) return headers;
253
+
254
+ for (const name of TRACE_HEADER_NAMES) {
255
+ const value = response.headers.get(name);
256
+ if (value) {
257
+ headers[name] = value;
258
+ }
259
+ }
260
+
261
+ return headers;
262
+ }
263
+
264
+ function parseApiError(
265
+ error: unknown,
266
+ response?: Response,
267
+ fallbackMessage?: string,
268
+ ): AuthenticationError | ApiError {
269
+ const traceHeaders = extractTraceHeaders(response);
270
+
271
+ if (response?.status === 401) {
272
+ return new AuthenticationError({
273
+ statusCode: 401,
274
+ message: "Authentication failed (HTTP 401)",
275
+ });
276
+ }
277
+
278
+ const err = error as
279
+ | {
280
+ error?: { message?: string; code?: string; hint?: string };
281
+ message?: string;
282
+ code?: string;
283
+ hint?: string;
284
+ }
285
+ | undefined;
286
+
287
+ const message =
288
+ err?.error?.message ??
289
+ err?.message ??
290
+ fallbackMessage ??
291
+ `Request failed with HTTP ${response?.status ?? "unknown"}`;
292
+
293
+ const code = err?.error?.code ?? err?.code;
294
+ const hint = err?.error?.hint ?? err?.hint;
295
+
296
+ return new ApiError({
297
+ statusCode: response?.status ?? 0,
298
+ statusText: response?.statusText ?? "",
299
+ code,
300
+ message,
301
+ hint,
302
+ traceHeaders,
303
+ });
304
+ }
305
+
306
+ /** Extract the upload URL from a version creation response. */
307
+ export function readUploadUrl(data: {
308
+ uploadUrl?: string | null;
309
+ artifactUploadUrl?: string | null;
310
+ }): string | null {
311
+ if (typeof data.uploadUrl === "string" && data.uploadUrl.length > 0) {
312
+ return data.uploadUrl;
313
+ }
314
+ if (
315
+ typeof data.artifactUploadUrl === "string" &&
316
+ data.artifactUploadUrl.length > 0
317
+ ) {
318
+ return data.artifactUploadUrl;
319
+ }
320
+ return null;
321
+ }
322
+
323
+ /** Normalize a preview domain to a clickable https URL. */
324
+ export function toDeploymentUrl(value: string): string {
325
+ const normalized = value.trim();
326
+ if (normalized.length === 0) return normalized;
327
+ if (normalized.startsWith("//")) return `https:${normalized}`;
328
+ if (/^https?:\/\//i.test(normalized)) return normalized;
329
+ return `https://${normalized}`;
330
+ }
package/src/archive.ts ADDED
@@ -0,0 +1,111 @@
1
+ import { lstat, readdir, readFile, readlink, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createGzip } from "node:zlib";
4
+ import { type Pack, pack } from "tar-stream";
5
+
6
+ const COMPUTE_MANIFEST_VERSION = "1" as const;
7
+
8
+ /**
9
+ * Create a tar.gz archive in memory from a build artifact directory.
10
+ *
11
+ * - Files are added under the `bundle/` prefix.
12
+ * - Symlinks are followed (dereferenced).
13
+ * - A synthetic `compute.manifest.json` entry is injected at the tar root.
14
+ *
15
+ * Returns the gzipped archive as a Uint8Array.
16
+ */
17
+ export async function createArchive(
18
+ directory: string,
19
+ entrypoint: string,
20
+ ): Promise<Uint8Array> {
21
+ const packer = pack();
22
+
23
+ // Walk the directory and add all files
24
+ await addDirectory(packer, directory, "bundle");
25
+
26
+ // Inject the manifest as a synthetic entry
27
+ const manifest = JSON.stringify(
28
+ {
29
+ manifestVersion: COMPUTE_MANIFEST_VERSION,
30
+ entrypoint: `bundle/${entrypoint}`,
31
+ },
32
+ null,
33
+ 2,
34
+ );
35
+ packer.entry({ name: "compute.manifest.json" }, manifest);
36
+
37
+ // Signal end of archive
38
+ packer.finalize();
39
+
40
+ // Pipe through gzip and collect into buffer
41
+ return await collectGzipped(packer);
42
+ }
43
+
44
+ async function addDirectory(
45
+ packer: Pack,
46
+ fsPath: string,
47
+ tarPrefix: string,
48
+ ): Promise<void> {
49
+ const entries = await readdir(fsPath, { withFileTypes: true });
50
+
51
+ for (const entry of entries) {
52
+ const fullPath = path.join(fsPath, entry.name);
53
+ const tarPath = `${tarPrefix}/${entry.name}`;
54
+
55
+ if (entry.isSymbolicLink()) {
56
+ // Dereference symlinks: stat the target and add accordingly
57
+ const target = await readlink(fullPath);
58
+ const resolvedPath = path.resolve(fsPath, target);
59
+ const resolvedStat = await stat(resolvedPath);
60
+
61
+ if (resolvedStat.isDirectory()) {
62
+ await addDirectory(packer, resolvedPath, tarPath);
63
+ } else if (resolvedStat.isFile()) {
64
+ await addFile(packer, resolvedPath, tarPath, resolvedStat);
65
+ }
66
+ } else if (entry.isDirectory()) {
67
+ await addDirectory(packer, fullPath, tarPath);
68
+ } else if (entry.isFile()) {
69
+ const fileStat = await lstat(fullPath);
70
+ await addFile(packer, fullPath, tarPath, fileStat);
71
+ }
72
+ }
73
+ }
74
+
75
+ async function addFile(
76
+ packer: Pack,
77
+ fsPath: string,
78
+ tarPath: string,
79
+ fileStat: { size: number; mode: number; mtime: Date },
80
+ ): Promise<void> {
81
+ const content = await readFile(fsPath);
82
+ packer.entry(
83
+ {
84
+ name: tarPath,
85
+ size: content.length,
86
+ mode: fileStat.mode,
87
+ mtime: fileStat.mtime,
88
+ },
89
+ content,
90
+ );
91
+ }
92
+
93
+ function collectGzipped(packer: Pack): Promise<Uint8Array> {
94
+ return new Promise<Uint8Array>((resolve, reject) => {
95
+ const chunks: Buffer[] = [];
96
+ const gzip = createGzip();
97
+
98
+ packer.pipe(gzip);
99
+
100
+ gzip.on("data", (chunk: Buffer) => {
101
+ chunks.push(chunk);
102
+ });
103
+
104
+ gzip.on("end", () => {
105
+ resolve(Buffer.concat(chunks));
106
+ });
107
+
108
+ gzip.on("error", reject);
109
+ packer.on("error", reject);
110
+ });
111
+ }
@@ -0,0 +1,66 @@
1
+ import { stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * The result of executing a build strategy.
6
+ */
7
+ export interface BuildArtifact {
8
+ /** Absolute path to the directory containing the built files. */
9
+ directory: string;
10
+
11
+ /** Entrypoint file path, relative to `directory`, using posix separators. */
12
+ entrypoint: string;
13
+
14
+ /**
15
+ * Optional cleanup callback. Called by the core after the archive has been
16
+ * created and the artifact directory is no longer needed. Strategies that
17
+ * create temporary output directories (e.g., BunBuild) use this to clean up.
18
+ */
19
+ cleanup?(): Promise<void>;
20
+ }
21
+
22
+ /**
23
+ * A build strategy produces a deployable artifact.
24
+ */
25
+ export interface BuildStrategy {
26
+ execute(): Promise<BuildArtifact>;
27
+ }
28
+
29
+ /**
30
+ * Reads directly from a pre-built application directory without copying.
31
+ * Validates the entrypoint exists.
32
+ */
33
+ export class PreBuilt implements BuildStrategy {
34
+ readonly #appPath: string;
35
+ readonly #entrypoint: string;
36
+
37
+ constructor(options: { appPath: string; entrypoint: string }) {
38
+ this.#appPath = options.appPath;
39
+ this.#entrypoint = options.entrypoint;
40
+ }
41
+
42
+ async execute(): Promise<BuildArtifact> {
43
+ const normalized = path.normalize(this.#entrypoint);
44
+
45
+ if (path.isAbsolute(normalized)) {
46
+ throw new Error("Entrypoint must be a relative path");
47
+ }
48
+
49
+ if (normalized.startsWith("..")) {
50
+ throw new Error("Entrypoint must not escape the application directory");
51
+ }
52
+
53
+ const fullPath = path.join(this.#appPath, normalized);
54
+ const stats = await stat(fullPath).catch(() => null);
55
+ if (!stats?.isFile()) {
56
+ throw new Error(
57
+ `Entrypoint not found in pre-built artifact: ${this.#entrypoint}`,
58
+ );
59
+ }
60
+
61
+ return {
62
+ directory: this.#appPath,
63
+ entrypoint: normalized.split(path.sep).join("/"),
64
+ };
65
+ }
66
+ }
@@ -0,0 +1,184 @@
1
+ import { execFile } from "node:child_process";
2
+ import {
3
+ access,
4
+ mkdir,
5
+ mkdtemp,
6
+ readdir,
7
+ readFile,
8
+ rm,
9
+ } from "node:fs/promises";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import type { BuildArtifact, BuildStrategy } from "./build-strategy.ts";
13
+
14
+ /**
15
+ * Build strategy that runs `bun build` CLI and manages its own temp output directory.
16
+ * Owns entrypoint resolution from explicit arg or package.json main field.
17
+ */
18
+ export class BunBuild implements BuildStrategy {
19
+ readonly #appPath: string;
20
+ readonly #entrypoint?: string;
21
+
22
+ constructor(options: { appPath: string; entrypoint?: string }) {
23
+ this.#appPath = options.appPath;
24
+ this.#entrypoint = options.entrypoint;
25
+ }
26
+
27
+ async execute(): Promise<BuildArtifact> {
28
+ const entrypoint = await this.#resolveEntrypoint();
29
+
30
+ const outDir = await mkdtemp(path.join(os.tmpdir(), "compute-build-"));
31
+ const bundleDir = path.join(outDir, "bundle");
32
+ await mkdir(bundleDir, { recursive: true });
33
+
34
+ const absoluteEntrypoint = path.join(this.#appPath, entrypoint);
35
+
36
+ try {
37
+ await this.#runBuild(absoluteEntrypoint, bundleDir);
38
+ } catch (error) {
39
+ await rm(outDir, { recursive: true, force: true });
40
+ throw error;
41
+ }
42
+
43
+ const outputFiles = await readdir(bundleDir);
44
+ const jsOutputs = outputFiles
45
+ .filter((f) => f.endsWith(".js"))
46
+ .map((f) => path.join(bundleDir, f));
47
+
48
+ const runtimeEntrypoint = this.#selectRuntimeEntrypoint(
49
+ jsOutputs,
50
+ bundleDir,
51
+ absoluteEntrypoint,
52
+ );
53
+
54
+ return {
55
+ directory: bundleDir,
56
+ entrypoint: runtimeEntrypoint,
57
+ cleanup: () => rm(outDir, { recursive: true, force: true }),
58
+ };
59
+ }
60
+
61
+ async #resolveEntrypoint(): Promise<string> {
62
+ const candidate = this.#entrypoint ?? (await this.#readPackageJsonMain());
63
+
64
+ if (!candidate) {
65
+ throw new Error(
66
+ "Entrypoint is required. Pass --entrypoint or define package.json main",
67
+ );
68
+ }
69
+
70
+ if (path.isAbsolute(candidate)) {
71
+ throw new Error("Entrypoint must be a relative path");
72
+ }
73
+
74
+ const normalized = path.normalize(candidate);
75
+ if (
76
+ normalized.startsWith("..") ||
77
+ path.isAbsolute(normalized) ||
78
+ normalized.includes(`${path.sep}..${path.sep}`)
79
+ ) {
80
+ throw new Error("Entrypoint must not escape the app directory");
81
+ }
82
+
83
+ const entrypointPath = path.join(this.#appPath, normalized);
84
+ try {
85
+ await access(entrypointPath);
86
+ } catch {
87
+ throw new Error(`Entrypoint file does not exist: ${entrypointPath}`);
88
+ }
89
+
90
+ return normalized.split(path.sep).join("/");
91
+ }
92
+
93
+ async #readPackageJsonMain(): Promise<string | undefined> {
94
+ const packageJsonPath = path.join(this.#appPath, "package.json");
95
+
96
+ let content: string;
97
+ try {
98
+ content = await readFile(packageJsonPath, "utf-8");
99
+ } catch (error) {
100
+ if (
101
+ error instanceof Error &&
102
+ "code" in error &&
103
+ error.code === "ENOENT"
104
+ ) {
105
+ return undefined;
106
+ }
107
+ throw new Error(
108
+ `Failed to read ${packageJsonPath}: ${error instanceof Error ? error.message : String(error)}`,
109
+ );
110
+ }
111
+
112
+ let parsed: { main?: unknown };
113
+ try {
114
+ parsed = JSON.parse(content) as { main?: unknown };
115
+ } catch (error) {
116
+ throw new Error(
117
+ `Failed to parse ${packageJsonPath}: ${error instanceof Error ? error.message : String(error)}`,
118
+ );
119
+ }
120
+
121
+ return typeof parsed.main === "string" ? parsed.main : undefined;
122
+ }
123
+
124
+ #runBuild(absoluteEntrypoint: string, bundleDir: string): Promise<void> {
125
+ return new Promise((resolve, reject) => {
126
+ execFile(
127
+ "bun",
128
+ [
129
+ "build",
130
+ absoluteEntrypoint,
131
+ "--outdir",
132
+ bundleDir,
133
+ "--target",
134
+ "bun",
135
+ "--sourcemap=external",
136
+ ],
137
+ (error, _stdout, stderr) => {
138
+ if (error) {
139
+ if ("code" in error && error.code === "ENOENT") {
140
+ reject(
141
+ new Error(
142
+ "Bun is required to build the application but was not found. Install it from https://bun.sh",
143
+ ),
144
+ );
145
+ return;
146
+ }
147
+ const message = stderr.trim() || error.message;
148
+ reject(new Error(`Bun build failed:\n${message}`));
149
+ return;
150
+ }
151
+ resolve();
152
+ },
153
+ );
154
+ });
155
+ }
156
+
157
+ #selectRuntimeEntrypoint(
158
+ outputPaths: string[],
159
+ bundleDir: string,
160
+ absoluteEntrypoint: string,
161
+ ): string {
162
+ const jsOutputs = outputPaths.filter((output) => output.endsWith(".js"));
163
+ if (jsOutputs.length === 0) {
164
+ throw new Error("Bun build produced no JavaScript output");
165
+ }
166
+
167
+ const expectedBasename = `${path.basename(absoluteEntrypoint, path.extname(absoluteEntrypoint))}.js`;
168
+ const matchingByName = jsOutputs.find(
169
+ (output) => path.basename(output) === expectedBasename,
170
+ );
171
+ const selected = matchingByName ?? jsOutputs[0];
172
+
173
+ if (!selected) {
174
+ throw new Error("Unable to determine built entrypoint");
175
+ }
176
+
177
+ const relative = path.relative(bundleDir, selected);
178
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
179
+ throw new Error("Built entrypoint is outside the bundle directory");
180
+ }
181
+
182
+ return relative.split(path.sep).join("/");
183
+ }
184
+ }