@prisma/compute-sdk 0.19.0 → 0.21.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 (64) hide show
  1. package/dist/api-client.d.ts.map +1 -1
  2. package/dist/api-client.js +24 -8
  3. package/dist/api-client.js.map +1 -1
  4. package/dist/archive.d.ts +1 -1
  5. package/dist/archive.d.ts.map +1 -1
  6. package/dist/archive.js +34 -15
  7. package/dist/archive.js.map +1 -1
  8. package/dist/artifact-postprocess.d.ts +7 -0
  9. package/dist/artifact-postprocess.d.ts.map +1 -0
  10. package/dist/artifact-postprocess.js +54 -0
  11. package/dist/artifact-postprocess.js.map +1 -0
  12. package/dist/astro-build.d.ts +2 -2
  13. package/dist/astro-build.d.ts.map +1 -1
  14. package/dist/astro-build.js +6 -4
  15. package/dist/astro-build.js.map +1 -1
  16. package/dist/auto-build.d.ts +2 -2
  17. package/dist/auto-build.d.ts.map +1 -1
  18. package/dist/auto-build.js +7 -5
  19. package/dist/auto-build.js.map +1 -1
  20. package/dist/build-strategy.d.ts +7 -6
  21. package/dist/build-strategy.d.ts.map +1 -1
  22. package/dist/build-strategy.js +11 -7
  23. package/dist/build-strategy.js.map +1 -1
  24. package/dist/bun-build.d.ts +2 -2
  25. package/dist/bun-build.d.ts.map +1 -1
  26. package/dist/bun-build.js +9 -7
  27. package/dist/bun-build.js.map +1 -1
  28. package/dist/compute-client.d.ts.map +1 -1
  29. package/dist/compute-client.js +17 -29
  30. package/dist/compute-client.js.map +1 -1
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/nextjs-build.d.ts +2 -2
  36. package/dist/nextjs-build.d.ts.map +1 -1
  37. package/dist/nextjs-build.js +8 -4
  38. package/dist/nextjs-build.js.map +1 -1
  39. package/dist/nuxt-build.d.ts +2 -2
  40. package/dist/nuxt-build.d.ts.map +1 -1
  41. package/dist/nuxt-build.js +6 -4
  42. package/dist/nuxt-build.js.map +1 -1
  43. package/dist/tanstack-start-build.d.ts +2 -2
  44. package/dist/tanstack-start-build.d.ts.map +1 -1
  45. package/dist/tanstack-start-build.js +5 -3
  46. package/dist/tanstack-start-build.js.map +1 -1
  47. package/dist/upload-artifact.d.ts +5 -0
  48. package/dist/upload-artifact.d.ts.map +1 -0
  49. package/dist/upload-artifact.js +27 -0
  50. package/dist/upload-artifact.js.map +1 -0
  51. package/package.json +1 -1
  52. package/src/api-client.ts +24 -8
  53. package/src/archive.ts +42 -10
  54. package/src/artifact-postprocess.ts +74 -0
  55. package/src/astro-build.ts +6 -4
  56. package/src/auto-build.ts +7 -5
  57. package/src/build-strategy.ts +14 -6
  58. package/src/bun-build.ts +13 -6
  59. package/src/compute-client.ts +21 -31
  60. package/src/index.ts +3 -0
  61. package/src/nextjs-build.ts +9 -4
  62. package/src/nuxt-build.ts +6 -4
  63. package/src/tanstack-start-build.ts +5 -3
  64. package/src/upload-artifact.ts +30 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-artifact.d.ts","sourceRoot":"","sources":["../src/upload-artifact.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,UAAU,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAsBf"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * PUT a gzipped compute artifact to a Foundry-minted presigned upload URL.
3
+ */
4
+ export async function uploadArtifact(uploadUrl, body, signal) {
5
+ signal?.throwIfAborted();
6
+ // Intentionally do not pass AbortSignal to this mutating upload request.
7
+ // Aborting transport can leave completion ambiguous server-side.
8
+ const response = await fetch(uploadUrl, {
9
+ method: "PUT",
10
+ headers: { "content-type": "application/gzip" },
11
+ body,
12
+ });
13
+ if (!response.ok) {
14
+ let message = `Artifact upload failed with HTTP ${response.status}`;
15
+ try {
16
+ const json = (await response.json());
17
+ if (typeof json.error === "string" && json.error.length > 0) {
18
+ message = `Artifact upload failed: ${json.error}`;
19
+ }
20
+ }
21
+ catch {
22
+ // ignore body parse failures
23
+ }
24
+ throw new Error(message);
25
+ }
26
+ }
27
+ //# sourceMappingURL=upload-artifact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-artifact.js","sourceRoot":"","sources":["../src/upload-artifact.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,SAAiB,EACjB,IAAgB,EAChB,MAAoB;IAEpB,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,yEAAyE;IACzE,iEAAiE;IACjE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;QACtC,MAAM,EAAE,KAAK;QACb,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI;KACL,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,IAAI,OAAO,GAAG,oCAAoC,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpE,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAwB,CAAC;YAC5D,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC5D,OAAO,GAAG,2BAA2B,IAAI,CAAC,KAAK,EAAE,CAAC;YACpD,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,6BAA6B;QAC/B,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/compute-sdk",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "TypeScript SDK for deploying and managing applications on Prisma Compute",
5
5
  "license": "Apache-2.0",
6
6
  "repository": "prisma/project-compute",
package/src/api-client.ts CHANGED
@@ -31,10 +31,12 @@ export class InternalApiClient {
31
31
  }
32
32
 
33
33
  async createProject(body: CreateProjectBody, signal?: AbortSignal) {
34
+ signal?.throwIfAborted();
34
35
  return this.#request(
35
36
  this.#client.POST("/v1/projects", {
36
37
  body: body as SdkCreateProjectBody,
37
- signal,
38
+ // Intentionally do not pass AbortSignal for mutating operations.
39
+ // Aborting transport can leave remote completion ambiguous.
38
40
  }),
39
41
  "/v1/projects",
40
42
  signal,
@@ -69,11 +71,13 @@ export class InternalApiClient {
69
71
  body: CreateProjectServiceBody,
70
72
  signal?: AbortSignal,
71
73
  ) {
74
+ signal?.throwIfAborted();
72
75
  return this.#request(
73
76
  this.#client.POST("/v1/projects/{projectId}/compute-services", {
74
77
  params: { path: { projectId } },
75
78
  body: body as SdkCreateProjectServiceBody,
76
- signal,
79
+ // Intentionally do not pass AbortSignal for mutating operations.
80
+ // Aborting transport can leave remote completion ambiguous.
77
81
  }),
78
82
  "/v1/projects/{projectId}/compute-services",
79
83
  signal,
@@ -95,10 +99,12 @@ export class InternalApiClient {
95
99
  computeServiceId: string,
96
100
  signal?: AbortSignal,
97
101
  ): Promise<Result<void, ApiClientError>> {
102
+ signal?.throwIfAborted();
98
103
  return this.#unwrapVoid(
99
104
  this.#client.DELETE("/v1/compute-services/{computeServiceId}", {
100
105
  params: { path: { computeServiceId } },
101
- signal,
106
+ // Intentionally do not pass AbortSignal for mutating operations.
107
+ // Aborting transport can leave remote completion ambiguous.
102
108
  }),
103
109
  signal,
104
110
  );
@@ -124,11 +130,13 @@ export class InternalApiClient {
124
130
  body?: CreateServiceVersionBody,
125
131
  signal?: AbortSignal,
126
132
  ) {
133
+ signal?.throwIfAborted();
127
134
  return this.#request(
128
135
  this.#client.POST("/v1/compute-services/{computeServiceId}/versions", {
129
136
  params: { path: { computeServiceId } },
130
137
  body,
131
- signal,
138
+ // Intentionally do not pass AbortSignal for mutating operations.
139
+ // Aborting transport can leave remote completion ambiguous.
132
140
  }),
133
141
  "/v1/compute-services/{computeServiceId}/versions",
134
142
  signal,
@@ -147,10 +155,12 @@ export class InternalApiClient {
147
155
  }
148
156
 
149
157
  async startVersion(versionId: string, signal?: AbortSignal) {
158
+ signal?.throwIfAborted();
150
159
  return this.#request(
151
160
  this.#client.POST("/v1/compute-services/versions/{versionId}/start", {
152
161
  params: { path: { versionId } },
153
- signal,
162
+ // Intentionally do not pass AbortSignal for mutating operations.
163
+ // Aborting transport can leave remote completion ambiguous.
154
164
  }),
155
165
  "/v1/compute-services/versions/{versionId}/start",
156
166
  signal,
@@ -161,10 +171,12 @@ export class InternalApiClient {
161
171
  versionId: string,
162
172
  signal?: AbortSignal,
163
173
  ): Promise<Result<void, ApiClientError>> {
174
+ signal?.throwIfAborted();
164
175
  return this.#unwrapVoid(
165
176
  this.#client.POST("/v1/compute-services/versions/{versionId}/stop", {
166
177
  params: { path: { versionId } },
167
- signal,
178
+ // Intentionally do not pass AbortSignal for mutating operations.
179
+ // Aborting transport can leave remote completion ambiguous.
168
180
  }),
169
181
  signal,
170
182
  );
@@ -174,10 +186,12 @@ export class InternalApiClient {
174
186
  versionId: string,
175
187
  signal?: AbortSignal,
176
188
  ): Promise<Result<void, ApiClientError>> {
189
+ signal?.throwIfAborted();
177
190
  return this.#unwrapVoid(
178
191
  this.#client.DELETE("/v1/compute-services/versions/{versionId}", {
179
192
  params: { path: { versionId } },
180
- signal,
193
+ // Intentionally do not pass AbortSignal for mutating operations.
194
+ // Aborting transport can leave remote completion ambiguous.
181
195
  }),
182
196
  signal,
183
197
  );
@@ -188,11 +202,13 @@ export class InternalApiClient {
188
202
  body: PromoteServiceBody,
189
203
  signal?: AbortSignal,
190
204
  ) {
205
+ signal?.throwIfAborted();
191
206
  return this.#request(
192
207
  this.#client.POST("/v1/compute-services/{computeServiceId}/promote", {
193
208
  params: { path: { computeServiceId } },
194
209
  body,
195
- signal,
210
+ // Intentionally do not pass AbortSignal for mutating operations.
211
+ // Aborting transport can leave remote completion ambiguous.
196
212
  }),
197
213
  "/v1/compute-services/{computeServiceId}/promote",
198
214
  signal,
package/src/archive.ts CHANGED
@@ -17,12 +17,14 @@ const COMPUTE_MANIFEST_VERSION = "1" as const;
17
17
  export async function createArchive(
18
18
  directory: string,
19
19
  entrypoint: string,
20
+ signal?: AbortSignal,
20
21
  ): Promise<Uint8Array> {
22
+ signal?.throwIfAborted();
21
23
  const packer = pack();
22
24
  const rootPath = path.resolve(directory);
23
25
 
24
26
  // Walk the directory and add all files
25
- await addDirectory(packer, rootPath, rootPath, "bundle");
27
+ await addDirectory(packer, rootPath, rootPath, "bundle", signal);
26
28
 
27
29
  // Inject the manifest as a synthetic entry
28
30
  const manifest = JSON.stringify(
@@ -39,7 +41,7 @@ export async function createArchive(
39
41
  packer.finalize();
40
42
 
41
43
  // Pipe through gzip and collect into buffer
42
- return await collectGzipped(packer);
44
+ return await collectGzipped(packer, signal);
43
45
  }
44
46
 
45
47
  async function addDirectory(
@@ -47,22 +49,25 @@ async function addDirectory(
47
49
  rootPath: string,
48
50
  fsPath: string,
49
51
  tarPrefix: string,
52
+ signal?: AbortSignal,
50
53
  ): Promise<void> {
54
+ signal?.throwIfAborted();
51
55
  const directoryPath = resolvePathWithinRoot(rootPath, fsPath);
52
56
  const entries = await readdir(directoryPath, { withFileTypes: true });
53
57
 
54
58
  for (const entry of entries) {
59
+ signal?.throwIfAborted();
55
60
  const fullPath = path.join(directoryPath, entry.name);
56
61
  const tarPath = `${tarPrefix}/${entry.name}`;
57
62
 
58
63
  if (entry.isSymbolicLink()) {
59
64
  const linkStat = await lstat(fullPath);
60
- await addSymlink(packer, rootPath, fullPath, tarPath, linkStat);
65
+ await addSymlink(packer, rootPath, fullPath, tarPath, linkStat, signal);
61
66
  } else if (entry.isDirectory()) {
62
- await addDirectory(packer, rootPath, fullPath, tarPath);
67
+ await addDirectory(packer, rootPath, fullPath, tarPath, signal);
63
68
  } else if (entry.isFile()) {
64
69
  const fileStat = await lstat(fullPath);
65
- await addFile(packer, rootPath, fullPath, tarPath, fileStat);
70
+ await addFile(packer, rootPath, fullPath, tarPath, fileStat, signal);
66
71
  }
67
72
  }
68
73
  }
@@ -73,7 +78,9 @@ async function addFile(
73
78
  fsPath: string,
74
79
  tarPath: string,
75
80
  fileStat: { size: number; mode: number; mtime: Date },
81
+ signal?: AbortSignal,
76
82
  ): Promise<void> {
83
+ signal?.throwIfAborted();
77
84
  const filePath = resolvePathWithinRoot(rootPath, fsPath);
78
85
  const content = await readFile(filePath);
79
86
  packer.entry(
@@ -93,13 +100,16 @@ async function addSymlink(
93
100
  fsPath: string,
94
101
  tarPath: string,
95
102
  linkStat: { mode: number; mtime: Date },
103
+ signal?: AbortSignal,
96
104
  ): Promise<void> {
105
+ signal?.throwIfAborted();
97
106
  const symlinkPath = resolvePathWithinRoot(rootPath, fsPath);
98
107
  const target = await readlink(symlinkPath);
99
108
  const linkname = await resolveArchiveSymlinkTarget(
100
109
  rootPath,
101
110
  symlinkPath,
102
111
  target,
112
+ signal,
103
113
  );
104
114
 
105
115
  packer.entry({
@@ -115,7 +125,9 @@ async function resolveArchiveSymlinkTarget(
115
125
  rootPath: string,
116
126
  symlinkPath: string,
117
127
  target: string,
128
+ signal?: AbortSignal,
118
129
  ): Promise<string> {
130
+ signal?.throwIfAborted();
119
131
  const symlinkDir = path.dirname(symlinkPath);
120
132
  const resolvedTarget = resolvePathWithinRoot(
121
133
  rootPath,
@@ -163,11 +175,27 @@ function resolvePathWithinRoot(rootPath: string, fsPath: string): string {
163
175
  return resolvedPath;
164
176
  }
165
177
 
166
- function collectGzipped(packer: Pack): Promise<Uint8Array> {
167
- return new Promise<Uint8Array>((resolve, reject) => {
168
- const chunks: Buffer[] = [];
169
- const gzip = createGzip();
170
-
178
+ function collectGzipped(
179
+ packer: Pack,
180
+ signal?: AbortSignal,
181
+ ): Promise<Uint8Array> {
182
+ signal?.throwIfAborted();
183
+
184
+ const chunks: Buffer[] = [];
185
+ const gzip = createGzip();
186
+ const onAbort = () => {
187
+ // AbortSignal.abort() sets signal.reason; fallback keeps a stream error path if a non-standard signal omits it.
188
+ const abortError =
189
+ signal?.reason instanceof Error
190
+ ? signal.reason
191
+ : new DOMException("The operation was aborted", "AbortError");
192
+ gzip.destroy(abortError);
193
+ packer.destroy(abortError);
194
+ };
195
+
196
+ signal?.addEventListener("abort", onAbort, { once: true });
197
+
198
+ const promise = new Promise<Uint8Array>((resolve, reject) => {
171
199
  packer.pipe(gzip);
172
200
 
173
201
  gzip.on("data", (chunk: Buffer) => {
@@ -181,4 +209,8 @@ function collectGzipped(packer: Pack): Promise<Uint8Array> {
181
209
  gzip.on("error", reject);
182
210
  packer.on("error", reject);
183
211
  });
212
+
213
+ return promise.finally(() => {
214
+ signal?.removeEventListener("abort", onAbort);
215
+ });
184
216
  }
@@ -0,0 +1,74 @@
1
+ import { cp, readdir, readlink, realpath, rm, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Materialize symlinks inside a build artifact that point outside the artifact
6
+ * directory but still live under the original app tree (typical for Next.js
7
+ * standalone output referencing monorepo node_modules).
8
+ */
9
+ export async function normalizeArtifactSymlinks(
10
+ artifactDir: string,
11
+ appPath: string,
12
+ signal?: AbortSignal,
13
+ ): Promise<void> {
14
+ signal?.throwIfAborted();
15
+ const normalizedArtifactDir = await realpath(path.resolve(artifactDir));
16
+ const normalizedAppPath = await realpath(path.resolve(appPath));
17
+
18
+ await walkDirectory(path.resolve(artifactDir));
19
+
20
+ async function walkDirectory(directory: string): Promise<void> {
21
+ signal?.throwIfAborted();
22
+ const entries = await readdir(directory, { withFileTypes: true });
23
+
24
+ for (const entry of entries) {
25
+ const fullPath = path.join(directory, entry.name);
26
+
27
+ if (entry.isDirectory()) {
28
+ await walkDirectory(fullPath);
29
+ continue;
30
+ }
31
+
32
+ if (!entry.isSymbolicLink()) {
33
+ continue;
34
+ }
35
+
36
+ const target = await readlink(fullPath);
37
+ const resolvedTarget = path.resolve(path.dirname(fullPath), target);
38
+ const realTarget = await realpath(resolvedTarget);
39
+
40
+ if (isPathWithin(normalizedArtifactDir, realTarget)) {
41
+ continue;
42
+ }
43
+
44
+ if (!isPathWithin(normalizedAppPath, realTarget)) {
45
+ throw new Error(
46
+ `Build artifact symlink escapes the app directory: ${realTarget}`,
47
+ );
48
+ }
49
+
50
+ const targetStat = await stat(realTarget);
51
+ await rm(fullPath, { force: true, recursive: true });
52
+ await cp(realTarget, fullPath, {
53
+ recursive: targetStat.isDirectory(),
54
+ // Keep nested symlinks intact so walkDirectory can validate each one.
55
+ dereference: false,
56
+ });
57
+
58
+ if (targetStat.isDirectory()) {
59
+ await walkDirectory(fullPath);
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ function isPathWithin(rootPath: string, candidatePath: string): boolean {
66
+ const relativePath = path.relative(rootPath, candidatePath);
67
+
68
+ return (
69
+ relativePath === "" ||
70
+ (!relativePath.startsWith(`..${path.sep}`) &&
71
+ relativePath !== ".." &&
72
+ !path.isAbsolute(relativePath))
73
+ );
74
+ }
@@ -27,14 +27,15 @@ export class AstroBuild implements BuildStrategy {
27
27
  this.#appPath = options.appPath;
28
28
  }
29
29
 
30
- async canBuild(): Promise<boolean> {
30
+ async canBuild(signal?: AbortSignal): Promise<boolean> {
31
31
  return (
32
- (await hasRootFile(this.#appPath, ASTRO_CONFIG_FILENAMES)) ||
33
- (await hasPackageDependency(this.#appPath, ["astro"]))
32
+ (await hasRootFile(this.#appPath, ASTRO_CONFIG_FILENAMES, signal)) ||
33
+ (await hasPackageDependency(this.#appPath, ["astro"], signal))
34
34
  );
35
35
  }
36
36
 
37
- async execute(): Promise<BuildArtifact> {
37
+ async execute(signal?: AbortSignal): Promise<BuildArtifact> {
38
+ signal?.throwIfAborted();
38
39
  await runPackageCli({
39
40
  appPath: this.#appPath,
40
41
  cliName: "astro",
@@ -42,6 +43,7 @@ export class AstroBuild implements BuildStrategy {
42
43
  failurePrefix: "Astro",
43
44
  missingMessage:
44
45
  "Could not find the Astro CLI. Install it with `npm install astro` or ensure npx/bunx is available.",
46
+ signal,
45
47
  });
46
48
 
47
49
  const distDir = path.join(this.#appPath, "dist");
package/src/auto-build.ts CHANGED
@@ -25,19 +25,21 @@ export class AutoBuild implements BuildStrategy {
25
25
  ];
26
26
  }
27
27
 
28
- async canBuild(): Promise<boolean> {
28
+ async canBuild(signal?: AbortSignal): Promise<boolean> {
29
29
  for (const strategy of this.#strategies) {
30
- if (await strategy.canBuild()) {
30
+ signal?.throwIfAborted();
31
+ if (await strategy.canBuild(signal)) {
31
32
  return true;
32
33
  }
33
34
  }
34
35
  return false;
35
36
  }
36
37
 
37
- async execute(): Promise<BuildArtifact> {
38
+ async execute(signal?: AbortSignal): Promise<BuildArtifact> {
38
39
  for (const strategy of this.#strategies) {
39
- if (await strategy.canBuild()) {
40
- return strategy.execute();
40
+ signal?.throwIfAborted();
41
+ if (await strategy.canBuild(signal)) {
42
+ return strategy.execute(signal);
41
43
  }
42
44
  }
43
45
  throw new Error("No suitable build strategy found for this application");
@@ -32,8 +32,8 @@ export interface BuildArtifact {
32
32
  * A build strategy produces a deployable artifact.
33
33
  */
34
34
  export interface BuildStrategy {
35
- canBuild(): Promise<boolean>;
36
- execute(): Promise<BuildArtifact>;
35
+ canBuild(signal?: AbortSignal): Promise<boolean>;
36
+ execute(signal?: AbortSignal): Promise<BuildArtifact>;
37
37
  }
38
38
 
39
39
  /**
@@ -49,11 +49,12 @@ export class PreBuilt implements BuildStrategy {
49
49
  this.#entrypoint = options.entrypoint;
50
50
  }
51
51
 
52
- async canBuild(): Promise<boolean> {
52
+ async canBuild(_signal?: AbortSignal): Promise<boolean> {
53
53
  return true;
54
54
  }
55
55
 
56
- async execute(): Promise<BuildArtifact> {
56
+ async execute(signal?: AbortSignal): Promise<BuildArtifact> {
57
+ signal?.throwIfAborted();
57
58
  const normalized = path.normalize(this.#entrypoint);
58
59
 
59
60
  if (path.isAbsolute(normalized)) {
@@ -92,7 +93,9 @@ export class PreBuilt implements BuildStrategy {
92
93
  export async function hasRootFile(
93
94
  appPath: string,
94
95
  filenames: readonly string[],
96
+ signal?: AbortSignal,
95
97
  ): Promise<boolean> {
98
+ signal?.throwIfAborted();
96
99
  let entries: string[];
97
100
  try {
98
101
  entries = await readdir(appPath);
@@ -110,7 +113,9 @@ export async function hasRootFile(
110
113
  export async function hasPackageDependency(
111
114
  appPath: string,
112
115
  packageNames: readonly string[],
116
+ signal?: AbortSignal,
113
117
  ): Promise<boolean> {
118
+ signal?.throwIfAborted();
114
119
  let content: string;
115
120
  try {
116
121
  content = await readFile(path.join(appPath, "package.json"), "utf-8");
@@ -154,6 +159,7 @@ export async function runPackageCli(opts: {
154
159
  failurePrefix: string;
155
160
  /** Thrown when no launcher is available. */
156
161
  missingMessage: string;
162
+ signal?: AbortSignal;
157
163
  }): Promise<void> {
158
164
  const localBin = path.join(
159
165
  opts.appPath,
@@ -168,8 +174,9 @@ export async function runPackageCli(opts: {
168
174
  ];
169
175
 
170
176
  for (const { command, args } of candidates) {
177
+ opts.signal?.throwIfAborted();
171
178
  try {
172
- await exec(command, args, opts.appPath, opts.failurePrefix);
179
+ await exec(command, args, opts.appPath, opts.failurePrefix, opts.signal);
173
180
  return;
174
181
  } catch (error) {
175
182
  if (isENOENT(error)) continue;
@@ -197,9 +204,10 @@ function exec(
197
204
  args: string[],
198
205
  cwd: string,
199
206
  failurePrefix: string,
207
+ signal?: AbortSignal,
200
208
  ): Promise<void> {
201
209
  return new Promise((resolve, reject) => {
202
- execFile(command, args, { cwd }, (error, _stdout, stderr) => {
210
+ execFile(command, args, { cwd, signal }, (error, _stdout, stderr) => {
203
211
  if (error) {
204
212
  if ("code" in error && error.code === "ENOENT") {
205
213
  reject(
package/src/bun-build.ts CHANGED
@@ -24,12 +24,13 @@ export class BunBuild implements BuildStrategy {
24
24
  this.#entrypoint = options.entrypoint;
25
25
  }
26
26
 
27
- async canBuild(): Promise<boolean> {
27
+ async canBuild(_signal?: AbortSignal): Promise<boolean> {
28
28
  return true;
29
29
  }
30
30
 
31
- async execute(): Promise<BuildArtifact> {
32
- const entrypoint = await this.#resolveEntrypoint();
31
+ async execute(signal?: AbortSignal): Promise<BuildArtifact> {
32
+ signal?.throwIfAborted();
33
+ const entrypoint = await this.#resolveEntrypoint(signal);
33
34
 
34
35
  const outDir = await mkdtemp(path.join(os.tmpdir(), "compute-build-"));
35
36
  const bundleDir = path.join(outDir, "bundle");
@@ -38,7 +39,7 @@ export class BunBuild implements BuildStrategy {
38
39
  const absoluteEntrypoint = path.join(this.#appPath, entrypoint);
39
40
 
40
41
  try {
41
- await this.#runBuild(absoluteEntrypoint, bundleDir);
42
+ await this.#runBuild(absoluteEntrypoint, bundleDir, signal);
42
43
  } catch (error) {
43
44
  await rm(outDir, { recursive: true, force: true });
44
45
  throw error;
@@ -62,7 +63,8 @@ export class BunBuild implements BuildStrategy {
62
63
  };
63
64
  }
64
65
 
65
- async #resolveEntrypoint(): Promise<string> {
66
+ async #resolveEntrypoint(signal?: AbortSignal): Promise<string> {
67
+ signal?.throwIfAborted();
66
68
  const candidate = this.#entrypoint ?? (await this.#readPackageJsonMain());
67
69
 
68
70
  if (!candidate) {
@@ -125,7 +127,11 @@ export class BunBuild implements BuildStrategy {
125
127
  return typeof parsed.main === "string" ? parsed.main : undefined;
126
128
  }
127
129
 
128
- #runBuild(absoluteEntrypoint: string, bundleDir: string): Promise<void> {
130
+ #runBuild(
131
+ absoluteEntrypoint: string,
132
+ bundleDir: string,
133
+ signal?: AbortSignal,
134
+ ): Promise<void> {
129
135
  return new Promise((resolve, reject) => {
130
136
  execFile(
131
137
  "bun",
@@ -138,6 +144,7 @@ export class BunBuild implements BuildStrategy {
138
144
  "bun",
139
145
  "--sourcemap=external",
140
146
  ],
147
+ { signal },
141
148
  (error, _stdout, stderr) => {
142
149
  if (error) {
143
150
  if ("code" in error && error.code === "ENOENT") {
@@ -48,6 +48,7 @@ import type {
48
48
  VersionInfo,
49
49
  } from "./types.ts";
50
50
  import { REGIONS } from "./types.ts";
51
+ import { uploadArtifact } from "./upload-artifact.ts";
51
52
 
52
53
  type VersionDetailData = InferOk<
53
54
  Awaited<ReturnType<InternalApiClient["getVersion"]>>
@@ -243,7 +244,7 @@ export class ComputeClient {
243
244
  options.progress?.onBuildStart?.();
244
245
  const artifact = yield* Result.await(
245
246
  Result.tryPromise({
246
- try: () => options.strategy.execute(),
247
+ try: () => options.strategy.execute(options.signal),
247
248
  catch: (e) =>
248
249
  new BuildError({
249
250
  message: e instanceof Error ? e.message : String(e),
@@ -258,7 +259,12 @@ export class ComputeClient {
258
259
  options.progress?.onArchiveCreating?.();
259
260
  const archiveBytes = yield* Result.await(
260
261
  Result.tryPromise({
261
- try: () => createArchive(artifact.directory, artifact.entrypoint),
262
+ try: () =>
263
+ createArchive(
264
+ artifact.directory,
265
+ artifact.entrypoint,
266
+ options.signal,
267
+ ),
262
268
  catch: (e) =>
263
269
  new ArtifactError({
264
270
  message:
@@ -304,7 +310,7 @@ export class ComputeClient {
304
310
 
305
311
  options.progress?.onUploadStart?.();
306
312
  yield* Result.await(
307
- self.#uploadArtifact(uploadUrl, archiveBytes, options.signal),
313
+ self.#uploadArtifactResult(uploadUrl, archiveBytes, options.signal),
308
314
  );
309
315
  options.progress?.onUploadComplete?.();
310
316
 
@@ -1152,10 +1158,12 @@ export class ComputeClient {
1152
1158
  }
1153
1159
 
1154
1160
  #checkAborted(signal?: AbortSignal): Result<void, CancelledError> {
1155
- if (signal?.aborted) {
1161
+ try {
1162
+ signal?.throwIfAborted();
1163
+ return Result.ok(undefined);
1164
+ } catch {
1156
1165
  return Result.err(new CancelledError());
1157
1166
  }
1158
- return Result.ok(undefined);
1159
1167
  }
1160
1168
 
1161
1169
  async #resolveDeployTarget(options: {
@@ -1304,40 +1312,22 @@ export class ComputeClient {
1304
1312
  });
1305
1313
  }
1306
1314
 
1307
- async #uploadArtifact(
1315
+ async #uploadArtifactResult(
1308
1316
  url: string,
1309
1317
  body: Uint8Array,
1310
1318
  signal?: AbortSignal,
1311
1319
  ): Promise<Result<void, ArtifactError | CancelledError>> {
1312
1320
  return Result.tryPromise({
1313
1321
  try: async () => {
1314
- const res = await fetch(url, {
1315
- method: "PUT",
1316
- headers: { "content-type": "application/gzip" },
1317
- body,
1318
- signal,
1322
+ await uploadArtifact(url, body, signal);
1323
+ },
1324
+ catch: (e) => {
1325
+ if (signal?.aborted) return new CancelledError();
1326
+ return new ArtifactError({
1327
+ message: e instanceof Error ? e.message : String(e),
1328
+ cause: e,
1319
1329
  });
1320
-
1321
- if (!res.ok) {
1322
- let message = `Artifact upload failed with HTTP ${res.status}`;
1323
- try {
1324
- const json = (await res.json()) as { error?: unknown };
1325
- if (typeof json.error === "string" && json.error.length > 0) {
1326
- message = `Artifact upload failed: ${json.error}`;
1327
- }
1328
- } catch {
1329
- // ignore body parse failures
1330
- }
1331
- throw new Error(message);
1332
- }
1333
1330
  },
1334
- catch: (e) =>
1335
- signal?.aborted
1336
- ? new CancelledError()
1337
- : new ArtifactError({
1338
- message: e instanceof Error ? e.message : String(e),
1339
- cause: e,
1340
- }),
1341
1331
  });
1342
1332
  }
1343
1333
  }
package/src/index.ts CHANGED
@@ -7,6 +7,8 @@ export {
7
7
  type Result,
8
8
  TaggedError,
9
9
  } from "better-result";
10
+ export { createArchive } from "./archive.ts";
11
+ export { normalizeArtifactSymlinks } from "./artifact-postprocess.ts";
10
12
  export { AstroBuild } from "./astro-build.ts";
11
13
  export { AutoBuild } from "./auto-build.ts";
12
14
  export type { BuildArtifact, BuildStrategy } from "./build-strategy.ts";
@@ -94,3 +96,4 @@ export type {
94
96
  VersionInfo,
95
97
  } from "./types.ts";
96
98
  export { KNOWN_REGION_IDS, REGIONS } from "./types.ts";
99
+ export { uploadArtifact } from "./upload-artifact.ts";