@prisma/compute-sdk 0.18.0 → 0.20.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 (49) 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/astro-build.d.ts +2 -2
  9. package/dist/astro-build.d.ts.map +1 -1
  10. package/dist/astro-build.js +6 -4
  11. package/dist/astro-build.js.map +1 -1
  12. package/dist/auto-build.d.ts +2 -2
  13. package/dist/auto-build.d.ts.map +1 -1
  14. package/dist/auto-build.js +7 -5
  15. package/dist/auto-build.js.map +1 -1
  16. package/dist/build-strategy.d.ts +7 -6
  17. package/dist/build-strategy.d.ts.map +1 -1
  18. package/dist/build-strategy.js +11 -7
  19. package/dist/build-strategy.js.map +1 -1
  20. package/dist/bun-build.d.ts +2 -2
  21. package/dist/bun-build.d.ts.map +1 -1
  22. package/dist/bun-build.js +9 -7
  23. package/dist/bun-build.js.map +1 -1
  24. package/dist/compute-client.d.ts.map +1 -1
  25. package/dist/compute-client.js +16 -9
  26. package/dist/compute-client.js.map +1 -1
  27. package/dist/nextjs-build.d.ts +2 -2
  28. package/dist/nextjs-build.d.ts.map +1 -1
  29. package/dist/nextjs-build.js +58 -8
  30. package/dist/nextjs-build.js.map +1 -1
  31. package/dist/nuxt-build.d.ts +2 -2
  32. package/dist/nuxt-build.d.ts.map +1 -1
  33. package/dist/nuxt-build.js +6 -4
  34. package/dist/nuxt-build.js.map +1 -1
  35. package/dist/tanstack-start-build.d.ts +2 -2
  36. package/dist/tanstack-start-build.d.ts.map +1 -1
  37. package/dist/tanstack-start-build.js +5 -3
  38. package/dist/tanstack-start-build.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/api-client.ts +24 -8
  41. package/src/archive.ts +42 -10
  42. package/src/astro-build.ts +6 -4
  43. package/src/auto-build.ts +7 -5
  44. package/src/build-strategy.ts +14 -6
  45. package/src/bun-build.ts +13 -6
  46. package/src/compute-client.ts +21 -12
  47. package/src/nextjs-build.ts +74 -8
  48. package/src/nuxt-build.ts +6 -4
  49. package/src/tanstack-start-build.ts +5 -3
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
  }
@@ -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") {
@@ -243,7 +243,7 @@ export class ComputeClient {
243
243
  options.progress?.onBuildStart?.();
244
244
  const artifact = yield* Result.await(
245
245
  Result.tryPromise({
246
- try: () => options.strategy.execute(),
246
+ try: () => options.strategy.execute(options.signal),
247
247
  catch: (e) =>
248
248
  new BuildError({
249
249
  message: e instanceof Error ? e.message : String(e),
@@ -258,7 +258,12 @@ export class ComputeClient {
258
258
  options.progress?.onArchiveCreating?.();
259
259
  const archiveBytes = yield* Result.await(
260
260
  Result.tryPromise({
261
- try: () => createArchive(artifact.directory, artifact.entrypoint),
261
+ try: () =>
262
+ createArchive(
263
+ artifact.directory,
264
+ artifact.entrypoint,
265
+ options.signal,
266
+ ),
262
267
  catch: (e) =>
263
268
  new ArtifactError({
264
269
  message:
@@ -1152,10 +1157,12 @@ export class ComputeClient {
1152
1157
  }
1153
1158
 
1154
1159
  #checkAborted(signal?: AbortSignal): Result<void, CancelledError> {
1155
- if (signal?.aborted) {
1160
+ try {
1161
+ signal?.throwIfAborted();
1162
+ return Result.ok(undefined);
1163
+ } catch {
1156
1164
  return Result.err(new CancelledError());
1157
1165
  }
1158
- return Result.ok(undefined);
1159
1166
  }
1160
1167
 
1161
1168
  async #resolveDeployTarget(options: {
@@ -1311,11 +1318,13 @@ export class ComputeClient {
1311
1318
  ): Promise<Result<void, ArtifactError | CancelledError>> {
1312
1319
  return Result.tryPromise({
1313
1320
  try: async () => {
1321
+ signal?.throwIfAborted();
1322
+ // Intentionally do not pass AbortSignal to this mutating upload request.
1323
+ // Aborting transport can leave completion ambiguous server-side.
1314
1324
  const res = await fetch(url, {
1315
1325
  method: "PUT",
1316
1326
  headers: { "content-type": "application/gzip" },
1317
1327
  body,
1318
- signal,
1319
1328
  });
1320
1329
 
1321
1330
  if (!res.ok) {
@@ -1331,13 +1340,13 @@ export class ComputeClient {
1331
1340
  throw new Error(message);
1332
1341
  }
1333
1342
  },
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
- }),
1343
+ catch: (e) => {
1344
+ if (signal?.aborted) return new CancelledError();
1345
+ return new ArtifactError({
1346
+ message: e instanceof Error ? e.message : String(e),
1347
+ cause: e,
1348
+ });
1349
+ },
1341
1350
  });
1342
1351
  }
1343
1352
  }
@@ -1,4 +1,4 @@
1
- import { cp, mkdtemp, rm, stat } from "node:fs/promises";
1
+ import { cp, mkdtemp, readdir, rm, stat } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import type { BuildArtifact, BuildStrategy } from "./build-strategy.ts";
@@ -26,14 +26,15 @@ export class NextjsBuild implements BuildStrategy {
26
26
  this.#appPath = options.appPath;
27
27
  }
28
28
 
29
- async canBuild(): Promise<boolean> {
29
+ async canBuild(signal?: AbortSignal): Promise<boolean> {
30
30
  return (
31
- (await hasRootFile(this.#appPath, NEXT_CONFIG_FILENAMES)) ||
32
- (await hasPackageDependency(this.#appPath, ["next"]))
31
+ (await hasRootFile(this.#appPath, NEXT_CONFIG_FILENAMES, signal)) ||
32
+ (await hasPackageDependency(this.#appPath, ["next"], signal))
33
33
  );
34
34
  }
35
35
 
36
- async execute(): Promise<BuildArtifact> {
36
+ async execute(signal?: AbortSignal): Promise<BuildArtifact> {
37
+ signal?.throwIfAborted();
37
38
  await runPackageCli({
38
39
  appPath: this.#appPath,
39
40
  cliName: "next",
@@ -41,6 +42,7 @@ export class NextjsBuild implements BuildStrategy {
41
42
  failurePrefix: "Next.js",
42
43
  missingMessage:
43
44
  "Could not find the Next.js CLI. Install it with `npm install next` or ensure npx/bunx is available.",
45
+ signal,
44
46
  });
45
47
 
46
48
  const standaloneDir = path.join(this.#appPath, ".next", "standalone");
@@ -61,9 +63,20 @@ export class NextjsBuild implements BuildStrategy {
61
63
  verbatimSymlinks: true,
62
64
  });
63
65
 
66
+ // In monorepos, Next preserves the workspace-relative path from the
67
+ // workspace root down to the app, so server.js lands at e.g.
68
+ // .next/standalone/apps/web/server.js, not at the standalone root.
69
+ // The runtime needs the correct entrypoint, and Next expects public/
70
+ // and .next/static/ to be siblings of server.js.
71
+ const serverSubpath = await findServerSubpath(artifactDir);
72
+ const serverDir =
73
+ serverSubpath === undefined
74
+ ? artifactDir
75
+ : path.join(artifactDir, serverSubpath);
76
+
64
77
  const publicDir = path.join(this.#appPath, "public");
65
78
  if (await directoryExists(publicDir)) {
66
- await cp(publicDir, path.join(artifactDir, "public"), {
79
+ await cp(publicDir, path.join(serverDir, "public"), {
67
80
  recursive: true,
68
81
  verbatimSymlinks: true,
69
82
  });
@@ -71,15 +84,20 @@ export class NextjsBuild implements BuildStrategy {
71
84
 
72
85
  const staticDir = path.join(this.#appPath, ".next", "static");
73
86
  if (await directoryExists(staticDir)) {
74
- await cp(staticDir, path.join(artifactDir, ".next", "static"), {
87
+ await cp(staticDir, path.join(serverDir, ".next", "static"), {
75
88
  recursive: true,
76
89
  verbatimSymlinks: true,
77
90
  });
78
91
  }
79
92
 
93
+ const entrypoint =
94
+ serverSubpath === undefined
95
+ ? "server.js"
96
+ : path.posix.join(serverSubpath, "server.js");
97
+
80
98
  return {
81
99
  directory: artifactDir,
82
- entrypoint: "server.js",
100
+ entrypoint,
83
101
  defaultPortMapping: { http: 3000 },
84
102
  cleanup: () => rm(outDir, { recursive: true, force: true }),
85
103
  };
@@ -94,3 +112,51 @@ async function directoryExists(dirPath: string): Promise<boolean> {
94
112
  const s = await stat(dirPath).catch(() => null);
95
113
  return s?.isDirectory() ?? false;
96
114
  }
115
+
116
+ /**
117
+ * Scan the Next.js standalone output to find the directory that contains
118
+ * server.js, relative to the standalone root. Returns undefined when
119
+ * server.js is at the root (single-app repos) and a posix-joined subpath
120
+ * like "apps/web" for monorepos. node_modules is skipped because
121
+ * third-party packages may ship a file named server.js.
122
+ */
123
+ async function findServerSubpath(
124
+ standaloneDir: string,
125
+ ): Promise<string | undefined> {
126
+ const matches: (string | undefined)[] = [];
127
+
128
+ async function walk(
129
+ currentDir: string,
130
+ relative: string | undefined,
131
+ ): Promise<void> {
132
+ const entries = await readdir(currentDir, { withFileTypes: true });
133
+ for (const entry of entries) {
134
+ if (entry.name === "node_modules") continue;
135
+ const fullPath = path.join(currentDir, entry.name);
136
+ if (entry.isDirectory()) {
137
+ const nextRelative =
138
+ relative === undefined
139
+ ? entry.name
140
+ : path.posix.join(relative, entry.name);
141
+ await walk(fullPath, nextRelative);
142
+ } else if (entry.isFile() && entry.name === "server.js") {
143
+ matches.push(relative);
144
+ }
145
+ }
146
+ }
147
+
148
+ await walk(standaloneDir, undefined);
149
+
150
+ if (matches.length > 1) {
151
+ const locations = matches.map((m) => m ?? "<root>").join(", ");
152
+ throw new Error(
153
+ `Next.js standalone output has multiple server.js files (${locations}). Cannot determine the application entrypoint.`,
154
+ );
155
+ }
156
+ if (matches.length === 0) {
157
+ throw new Error(
158
+ "Next.js standalone output is missing server.js. The build may not have completed successfully.",
159
+ );
160
+ }
161
+ return matches[0];
162
+ }
package/src/nuxt-build.ts CHANGED
@@ -27,14 +27,15 @@ export class NuxtBuild 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, NUXT_CONFIG_FILENAMES)) ||
33
- (await hasPackageDependency(this.#appPath, ["nuxt"]))
32
+ (await hasRootFile(this.#appPath, NUXT_CONFIG_FILENAMES, signal)) ||
33
+ (await hasPackageDependency(this.#appPath, ["nuxt"], 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: "nuxt",
@@ -42,6 +43,7 @@ export class NuxtBuild implements BuildStrategy {
42
43
  failurePrefix: "Nuxt",
43
44
  missingMessage:
44
45
  "Could not find the Nuxt CLI. Install it with `npm install nuxt` or ensure npx/bunx is available.",
46
+ signal,
45
47
  });
46
48
 
47
49
  const outputDir = path.join(this.#appPath, ".output");
@@ -20,11 +20,12 @@ export class TanstackStartBuild implements BuildStrategy {
20
20
  this.#appPath = options.appPath;
21
21
  }
22
22
 
23
- async canBuild(): Promise<boolean> {
24
- return hasPackageDependency(this.#appPath, TANSTACK_START_PACKAGES);
23
+ async canBuild(signal?: AbortSignal): Promise<boolean> {
24
+ return hasPackageDependency(this.#appPath, TANSTACK_START_PACKAGES, signal);
25
25
  }
26
26
 
27
- async execute(): Promise<BuildArtifact> {
27
+ async execute(signal?: AbortSignal): Promise<BuildArtifact> {
28
+ signal?.throwIfAborted();
28
29
  await runPackageCli({
29
30
  appPath: this.#appPath,
30
31
  cliName: "vite",
@@ -32,6 +33,7 @@ export class TanstackStartBuild implements BuildStrategy {
32
33
  failurePrefix: "TanStack Start",
33
34
  missingMessage:
34
35
  "Could not find the Vite CLI. Install it with `npm install vite` or ensure npx/bunx is available.",
36
+ signal,
35
37
  });
36
38
 
37
39
  const outputDir = path.join(this.#appPath, ".output");