@prisma/cli 3.0.0-beta.1 → 3.0.0-beta.11

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 (66) hide show
  1. package/README.md +5 -3
  2. package/dist/adapters/git.js +8 -3
  3. package/dist/adapters/local-state.js +12 -4
  4. package/dist/adapters/mock-api.js +81 -2
  5. package/dist/adapters/token-storage.js +64 -23
  6. package/dist/cli.js +18 -3
  7. package/dist/cli2.js +9 -5
  8. package/dist/commands/app/index.js +84 -59
  9. package/dist/commands/branch/index.js +2 -27
  10. package/dist/commands/database/index.js +159 -0
  11. package/dist/commands/env.js +8 -4
  12. package/dist/controllers/app-env-api.js +54 -0
  13. package/dist/controllers/app-env-file.js +181 -0
  14. package/dist/controllers/app-env.js +286 -131
  15. package/dist/controllers/app.js +699 -244
  16. package/dist/controllers/auth.js +8 -8
  17. package/dist/controllers/branch.js +78 -48
  18. package/dist/controllers/database.js +318 -0
  19. package/dist/controllers/project.js +156 -85
  20. package/dist/lib/app/branch-database-deploy.js +325 -0
  21. package/dist/lib/app/branch-database.js +215 -0
  22. package/dist/lib/app/bun-project.js +12 -5
  23. package/dist/lib/app/compute-config.js +147 -0
  24. package/dist/lib/app/deploy-output.js +10 -1
  25. package/dist/lib/app/deploy-plan.js +58 -0
  26. package/dist/lib/app/env-config.js +1 -1
  27. package/dist/lib/app/env-file.js +82 -0
  28. package/dist/lib/app/env-vars.js +28 -2
  29. package/dist/lib/app/local-dev.js +8 -49
  30. package/dist/lib/app/preview-branch-database.js +102 -0
  31. package/dist/lib/app/preview-build-settings.js +376 -0
  32. package/dist/lib/app/preview-build.js +314 -97
  33. package/dist/lib/app/preview-provider.js +178 -65
  34. package/dist/lib/app/production-deploy-gate.js +161 -0
  35. package/dist/lib/auth/auth-ops.js +30 -21
  36. package/dist/lib/auth/guard.js +3 -2
  37. package/dist/lib/auth/login.js +116 -14
  38. package/dist/lib/database/provider.js +167 -0
  39. package/dist/lib/diagnostics.js +15 -0
  40. package/dist/lib/fs/home-path.js +24 -0
  41. package/dist/lib/git/local-branch.js +53 -0
  42. package/dist/lib/git/local-status.js +57 -0
  43. package/dist/lib/project/interactive-setup.js +1 -1
  44. package/dist/lib/project/local-pin.js +182 -33
  45. package/dist/lib/project/resolution.js +204 -50
  46. package/dist/lib/project/setup.js +66 -19
  47. package/dist/output/patterns.js +1 -1
  48. package/dist/presenters/app-env.js +149 -14
  49. package/dist/presenters/app.js +178 -23
  50. package/dist/presenters/branch.js +37 -102
  51. package/dist/presenters/database.js +274 -0
  52. package/dist/presenters/project.js +57 -39
  53. package/dist/presenters/verbose-context.js +64 -0
  54. package/dist/shell/command-meta.js +103 -13
  55. package/dist/shell/command-runner.js +57 -19
  56. package/dist/shell/diagnostics-output.js +57 -0
  57. package/dist/shell/errors.js +12 -1
  58. package/dist/shell/help.js +30 -20
  59. package/dist/shell/output.js +10 -1
  60. package/dist/shell/runtime.js +11 -7
  61. package/dist/shell/ui.js +42 -3
  62. package/dist/shell/update-check.js +247 -0
  63. package/dist/use-cases/branch.js +20 -68
  64. package/dist/use-cases/create-cli-gateways.js +2 -17
  65. package/dist/use-cases/project.js +2 -1
  66. package/package.json +26 -10
@@ -1,7 +1,10 @@
1
- import { resolveBunEntrypoint } from "./bun-project.js";
2
- import { chmod, copyFile, cp, lstat, mkdir, readFile, readdir, readlink, rm, stat } from "node:fs/promises";
1
+ import { readBunPackageJson, resolveBunEntrypoint } from "./bun-project.js";
2
+ import { hasAnyPackageDependency, hasPackageDependency, hasRootFile, joinPosix, nextOutputRootFromStandaloneDirectory, resolvePreviewBuildSettings, runResolvedBuildCommand } from "./preview-build-settings.js";
3
+ import { resolveSourceRoot } from "@prisma/compute-sdk/config";
4
+ import { chmod, copyFile, cp, lstat, mkdir, mkdtemp, readdir, readlink, rm, stat, writeFile } from "node:fs/promises";
3
5
  import path from "node:path";
4
- import { AstroBuild, BunBuild, NextjsBuild, NuxtBuild, TanstackStartBuild } from "@prisma/compute-sdk";
6
+ import os from "node:os";
7
+ import { AstroBuild, BunBuild, NuxtBuild } from "@prisma/compute-sdk";
5
8
  //#region src/lib/app/preview-build.ts
6
9
  const PREVIEW_BUILD_TYPES = [
7
10
  "auto",
@@ -16,24 +19,32 @@ var PreviewBuildStrategy = class {
16
19
  #appPath;
17
20
  #entrypoint;
18
21
  #buildType;
22
+ #signal;
23
+ #buildSettings;
19
24
  constructor(options) {
20
25
  this.#appPath = options.appPath;
21
26
  this.#entrypoint = options.entrypoint;
22
27
  this.#buildType = options.buildType ?? "auto";
28
+ this.#signal = options.signal;
29
+ this.#buildSettings = options.buildSettings;
23
30
  }
24
- async canBuild() {
31
+ async canBuild(signal = this.#signal) {
25
32
  const { strategy } = await resolvePreviewBuildStrategy({
26
33
  appPath: this.#appPath,
27
34
  entrypoint: this.#entrypoint,
28
- buildType: this.#buildType
35
+ buildType: this.#buildType,
36
+ signal,
37
+ buildSettings: this.#buildSettings
29
38
  });
30
- return strategy.canBuild();
39
+ return strategy.canBuild(signal);
31
40
  }
32
- async execute() {
41
+ async execute(signal = this.#signal) {
33
42
  const { artifact } = await executePreviewBuild({
34
43
  appPath: this.#appPath,
35
44
  entrypoint: this.#entrypoint,
36
- buildType: this.#buildType
45
+ buildType: this.#buildType,
46
+ signal,
47
+ buildSettings: this.#buildSettings
37
48
  });
38
49
  return artifact;
39
50
  }
@@ -42,12 +53,13 @@ async function executePreviewBuild(options) {
42
53
  const { strategy, buildType } = await resolvePreviewBuildStrategy({
43
54
  appPath: options.appPath,
44
55
  entrypoint: options.entrypoint,
45
- buildType: options.buildType ?? "auto"
56
+ buildType: options.buildType ?? "auto",
57
+ signal: options.signal,
58
+ buildSettings: options.buildSettings
46
59
  });
47
- const artifact = await strategy.execute();
60
+ const artifact = await strategy.execute(options.signal);
48
61
  try {
49
- if (buildType === "nextjs") await restageNextjsArtifact(artifact, options.appPath);
50
- await normalizeArtifactSymlinks(artifact.directory, options.appPath);
62
+ await normalizeArtifactSymlinks(artifact.directory, options.appPath, options.signal);
51
63
  return {
52
64
  artifact,
53
65
  buildType
@@ -62,7 +74,9 @@ async function resolvePreviewBuildStrategy(options) {
62
74
  const strategy = await createPreviewBuildStrategy({
63
75
  appPath: options.appPath,
64
76
  entrypoint: options.entrypoint,
65
- buildType: options.buildType
77
+ buildType: options.buildType,
78
+ signal: options.signal,
79
+ buildSettings: options.buildSettings
66
80
  });
67
81
  return {
68
82
  buildType: options.buildType,
@@ -74,9 +88,11 @@ async function resolvePreviewBuildStrategy(options) {
74
88
  const strategy = await createPreviewBuildStrategy({
75
89
  appPath: options.appPath,
76
90
  entrypoint: options.entrypoint,
77
- buildType
91
+ buildType,
92
+ signal: options.signal,
93
+ buildSettings: options.buildSettings
78
94
  });
79
- if (await strategy.canBuild()) return {
95
+ if (await strategy.canBuild(options.signal)) return {
80
96
  buildType,
81
97
  strategy
82
98
  };
@@ -86,25 +102,210 @@ async function resolvePreviewBuildStrategy(options) {
86
102
  strategy: await createPreviewBuildStrategy({
87
103
  appPath: options.appPath,
88
104
  entrypoint: options.entrypoint,
89
- buildType: "bun"
105
+ buildType: "bun",
106
+ signal: options.signal,
107
+ buildSettings: options.buildSettings
90
108
  })
91
109
  };
92
110
  }
93
111
  async function createPreviewBuildStrategy(options) {
94
112
  switch (options.buildType) {
95
- case "nextjs": return new NextjsBuild({ appPath: options.appPath });
113
+ case "nextjs": return new PreviewNextjsBuild({
114
+ appPath: options.appPath,
115
+ buildSettings: options.buildSettings
116
+ });
96
117
  case "nuxt": return new NuxtBuild({ appPath: options.appPath });
97
118
  case "astro": return new AstroBuild({ appPath: options.appPath });
98
- case "tanstack-start": return new TanstackStartBuild({ appPath: options.appPath });
119
+ case "tanstack-start": return new PreviewTanstackStartBuild({
120
+ appPath: options.appPath,
121
+ buildSettings: options.buildSettings
122
+ });
99
123
  case "bun": {
100
- const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint);
101
- return new BunBuild({
124
+ const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint, options.signal);
125
+ return new PreviewBunBuild({
102
126
  appPath: options.appPath,
103
- entrypoint
127
+ strategy: new BunBuild({
128
+ appPath: options.appPath,
129
+ entrypoint
130
+ }),
131
+ buildSettings: options.buildSettings
132
+ });
133
+ }
134
+ }
135
+ }
136
+ var PreviewNextjsBuild = class {
137
+ #appPath;
138
+ #buildSettings;
139
+ constructor(options) {
140
+ this.#appPath = options.appPath;
141
+ this.#buildSettings = options.buildSettings;
142
+ }
143
+ async canBuild(signal) {
144
+ const packageJson = await readBunPackageJson(this.#appPath, signal);
145
+ return await hasRootFile(this.#appPath, NEXT_CONFIG_FILENAMES, signal) || hasPackageDependency(packageJson, "next");
146
+ }
147
+ async execute(signal) {
148
+ const settings = this.#buildSettings ?? await resolvePreviewBuildSettings({
149
+ appPath: this.#appPath,
150
+ buildType: "nextjs",
151
+ signal
152
+ });
153
+ await runResolvedBuildCommand(this.#appPath, settings, "Next.js", signal);
154
+ const standaloneDir = path.join(this.#appPath, settings.outputDirectory);
155
+ if (!await directoryExists(standaloneDir, signal)) return stageNextjsFullTreeFallbackArtifact(this.#appPath, signal);
156
+ const outDir = await unsupportedFilesystemBoundary(signal, () => mkdtemp(path.join(os.tmpdir(), "compute-build-")));
157
+ try {
158
+ const artifactDir = path.join(outDir, "app");
159
+ await stageNextjsStandaloneArtifact({
160
+ standaloneDir,
161
+ artifactDir,
162
+ appPath: this.#appPath,
163
+ signal
164
+ });
165
+ const entrypoint = await findNextStandaloneEntrypoint(artifactDir, signal);
166
+ await copyNextjsStaticAssets({
167
+ appPath: this.#appPath,
168
+ artifactDir,
169
+ outputRoot: nextOutputRootFromStandaloneDirectory(settings.outputDirectory),
170
+ entrypoint,
171
+ signal
104
172
  });
173
+ return {
174
+ directory: artifactDir,
175
+ entrypoint,
176
+ defaultPortMapping: { http: 3e3 },
177
+ cleanup: () => rm(outDir, {
178
+ recursive: true,
179
+ force: true
180
+ })
181
+ };
182
+ } catch (error) {
183
+ await rm(outDir, {
184
+ recursive: true,
185
+ force: true
186
+ });
187
+ throw error;
105
188
  }
106
189
  }
190
+ };
191
+ const FULL_TREE_NEXT_START_ENTRYPOINT = "prisma-next-start.cjs";
192
+ const FULL_TREE_NEXT_START_SOURCE = [
193
+ "process.chdir(__dirname);",
194
+ "process.env.NODE_ENV = \"production\";",
195
+ "process.argv.push(\"start\", \"-p\", process.env.PORT ?? \"3000\");",
196
+ "require(\"next/dist/bin/next\");",
197
+ ""
198
+ ].join("\n");
199
+ async function stageNextjsFullTreeFallbackArtifact(appPath, signal) {
200
+ const outDir = await unsupportedFilesystemBoundary(signal, () => mkdtemp(path.join(os.tmpdir(), "compute-build-")));
201
+ try {
202
+ const artifactDir = path.join(outDir, "app");
203
+ await unsupportedFilesystemBoundary(signal, () => cp(appPath, artifactDir, {
204
+ recursive: true,
205
+ verbatimSymlinks: true,
206
+ filter: (source) => !isExcludedFromFullTreeArtifact(path.basename(source))
207
+ }));
208
+ await unsupportedFilesystemBoundary(signal, () => writeFile(path.join(artifactDir, FULL_TREE_NEXT_START_ENTRYPOINT), FULL_TREE_NEXT_START_SOURCE));
209
+ return {
210
+ directory: artifactDir,
211
+ entrypoint: FULL_TREE_NEXT_START_ENTRYPOINT,
212
+ defaultPortMapping: { http: 3e3 },
213
+ cleanup: () => rm(outDir, {
214
+ recursive: true,
215
+ force: true
216
+ })
217
+ };
218
+ } catch (error) {
219
+ await rm(outDir, {
220
+ recursive: true,
221
+ force: true
222
+ });
223
+ throw error;
224
+ }
225
+ }
226
+ /** Excludes VCS internals and dotenv files (local secrets, superseded by the deploy env). */
227
+ function isExcludedFromFullTreeArtifact(basename) {
228
+ return basename === ".git" || basename === ".env" || basename.startsWith(".env.");
107
229
  }
230
+ var PreviewTanstackStartBuild = class {
231
+ #appPath;
232
+ #buildSettings;
233
+ constructor(options) {
234
+ this.#appPath = options.appPath;
235
+ this.#buildSettings = options.buildSettings;
236
+ }
237
+ async canBuild(signal) {
238
+ return hasAnyPackageDependency(await readBunPackageJson(this.#appPath, signal), TANSTACK_START_PACKAGES);
239
+ }
240
+ async execute(signal) {
241
+ const settings = this.#buildSettings ?? await resolvePreviewBuildSettings({
242
+ appPath: this.#appPath,
243
+ buildType: "tanstack-start",
244
+ signal
245
+ });
246
+ await runResolvedBuildCommand(this.#appPath, settings, "TanStack Start", signal);
247
+ const outputDir = path.join(this.#appPath, settings.outputDirectory);
248
+ const entrypoint = "server/index.mjs";
249
+ const entryPath = path.join(outputDir, entrypoint);
250
+ if (!(await unsupportedFilesystemBoundary(signal, () => stat(entryPath).catch(() => null)))?.isFile()) throw new Error(`TanStack Start build did not produce a Nitro node server entrypoint at ${joinPosix(settings.outputDirectory, entrypoint)}. Ensure your vite.config includes the tanstackStart() and nitro() plugins with the default node preset, or set build.outputDirectory in prisma.compute.ts.`);
251
+ const outDir = await unsupportedFilesystemBoundary(signal, () => mkdtemp(path.join(os.tmpdir(), "compute-build-")));
252
+ try {
253
+ const artifactDir = path.join(outDir, "app");
254
+ await unsupportedFilesystemBoundary(signal, () => cp(outputDir, artifactDir, {
255
+ recursive: true,
256
+ verbatimSymlinks: true
257
+ }));
258
+ return {
259
+ directory: artifactDir,
260
+ entrypoint,
261
+ defaultPortMapping: { http: 3e3 },
262
+ cleanup: () => rm(outDir, {
263
+ recursive: true,
264
+ force: true
265
+ })
266
+ };
267
+ } catch (error) {
268
+ await rm(outDir, {
269
+ recursive: true,
270
+ force: true
271
+ });
272
+ throw error;
273
+ }
274
+ }
275
+ };
276
+ var PreviewBunBuild = class {
277
+ #appPath;
278
+ #strategy;
279
+ #buildSettings;
280
+ constructor(options) {
281
+ this.#appPath = options.appPath;
282
+ this.#strategy = options.strategy;
283
+ this.#buildSettings = options.buildSettings;
284
+ }
285
+ async canBuild(signal) {
286
+ return this.#strategy.canBuild(signal);
287
+ }
288
+ async execute(signal) {
289
+ const settings = this.#buildSettings ?? await resolvePreviewBuildSettings({
290
+ appPath: this.#appPath,
291
+ buildType: "bun",
292
+ signal
293
+ });
294
+ await runResolvedBuildCommand(this.#appPath, settings, "Bun", signal);
295
+ return this.#strategy.execute(signal);
296
+ }
297
+ };
298
+ const NEXT_CONFIG_FILENAMES = [
299
+ "next.config.js",
300
+ "next.config.mjs",
301
+ "next.config.ts",
302
+ "next.config.mts"
303
+ ];
304
+ const TANSTACK_START_PACKAGES = [
305
+ "@tanstack/react-start",
306
+ "@tanstack/solid-start",
307
+ "@tanstack/start"
308
+ ];
108
309
  async function stageNextjsStandaloneArtifact(options) {
109
310
  const standaloneRoot = path.resolve(options.standaloneDir);
110
311
  const artifactRoot = path.resolve(options.artifactDir);
@@ -112,74 +313,97 @@ async function stageNextjsStandaloneArtifact(options) {
112
313
  await copyPathMaterializingSymlinks(standaloneRoot, artifactRoot, {
113
314
  standaloneRoot,
114
315
  appRoot,
115
- sourceRoot: await resolveSourceRoot(appRoot)
316
+ sourceRoot: await resolveSourceRoot(appRoot, options.signal),
317
+ signal: options.signal
116
318
  });
117
- await hoistPnpmDependencies(path.join(artifactRoot, "node_modules"));
319
+ await hoistIsolatedStoreDependencies(path.join(artifactRoot, "node_modules"), options.signal);
118
320
  }
119
- async function restageNextjsArtifact(artifact, appPath) {
120
- const artifactDir = artifact.directory;
121
- const standaloneDir = path.join(appPath, ".next", "standalone");
122
- await rm(artifactDir, {
123
- recursive: true,
124
- force: true
125
- });
126
- await stageNextjsStandaloneArtifact({
127
- standaloneDir,
128
- artifactDir,
129
- appPath
130
- });
131
- const serverSubpath = nextjsServerSubpath(artifact.entrypoint);
132
- const serverDir = serverSubpath ? path.join(artifactDir, serverSubpath) : artifactDir;
133
- const publicDir = path.join(appPath, "public");
134
- if (await directoryExists(publicDir)) await cp(publicDir, path.join(serverDir, "public"), {
321
+ async function copyNextjsStaticAssets(options) {
322
+ const serverSubpath = nextjsServerSubpath(options.entrypoint);
323
+ const serverDir = serverSubpath ? path.join(options.artifactDir, serverSubpath) : options.artifactDir;
324
+ const publicDir = path.join(options.appPath, "public");
325
+ if (await directoryExists(publicDir, options.signal)) await unsupportedFilesystemBoundary(options.signal, () => cp(publicDir, path.join(serverDir, "public"), {
135
326
  recursive: true,
136
327
  verbatimSymlinks: true
137
- });
138
- const staticDir = path.join(appPath, ".next", "static");
139
- if (await directoryExists(staticDir)) await cp(staticDir, path.join(serverDir, ".next", "static"), {
328
+ }));
329
+ const staticDir = path.join(options.appPath, options.outputRoot, "static");
330
+ if (await directoryExists(staticDir, options.signal)) await unsupportedFilesystemBoundary(options.signal, () => cp(staticDir, path.join(serverDir, options.outputRoot, "static"), {
140
331
  recursive: true,
141
332
  verbatimSymlinks: true
142
- });
333
+ }));
334
+ }
335
+ async function findNextStandaloneEntrypoint(artifactDir, signal) {
336
+ const rootEntrypoint = path.join(artifactDir, "server.js");
337
+ if ((await unsupportedFilesystemBoundary(signal, () => stat(rootEntrypoint).catch(() => null)))?.isFile()) return "server.js";
338
+ const candidates = [];
339
+ await walk(artifactDir);
340
+ candidates.sort((left, right) => left.split("/").length - right.split("/").length || left.localeCompare(right));
341
+ const selected = candidates[0];
342
+ if (!selected) throw new Error(`Next.js standalone output did not contain server.js in ${artifactDir}`);
343
+ return selected;
344
+ async function walk(directory) {
345
+ const entries = await unsupportedFilesystemBoundary(signal, () => readdir(directory, { withFileTypes: true }));
346
+ for (const entry of entries) {
347
+ if (entry.name === "node_modules") continue;
348
+ const fullPath = path.join(directory, entry.name);
349
+ if (entry.isDirectory()) {
350
+ await walk(fullPath);
351
+ continue;
352
+ }
353
+ if (entry.isFile() && entry.name === "server.js") candidates.push(path.relative(artifactDir, fullPath).split(path.sep).join("/"));
354
+ }
355
+ }
143
356
  }
144
357
  function nextjsServerSubpath(entrypoint) {
145
358
  const dir = path.posix.dirname(entrypoint);
146
359
  return dir === "." ? "" : dir;
147
360
  }
148
- async function hoistPnpmDependencies(nodeModulesDir) {
149
- const pnpmNodeModulesDir = path.join(nodeModulesDir, ".pnpm", "node_modules");
150
- if (!await directoryExists(pnpmNodeModulesDir)) return;
151
- const entries = await readdir(pnpmNodeModulesDir, { withFileTypes: true });
361
+ /**
362
+ * pnpm and bun (isolated linker) both keep packages in a virtual store with a
363
+ * shared symlink farm (`.pnpm/node_modules`, `.bun/node_modules`). Hoist the
364
+ * farm entries to the artifact's node_modules root so Node-style resolution
365
+ * works after symlinks are materialized.
366
+ */
367
+ async function hoistIsolatedStoreDependencies(nodeModulesDir, signal) {
368
+ await hoistStoreDependencies(nodeModulesDir, path.join(nodeModulesDir, ".pnpm", "node_modules"), signal);
369
+ await hoistStoreDependencies(nodeModulesDir, path.join(nodeModulesDir, ".bun", "node_modules"), signal);
370
+ }
371
+ async function hoistStoreDependencies(nodeModulesDir, storeNodeModulesDir, signal) {
372
+ if (!await directoryExists(storeNodeModulesDir, signal)) return;
373
+ const entries = await unsupportedFilesystemBoundary(signal, () => readdir(storeNodeModulesDir, { withFileTypes: true }));
152
374
  for (const entry of entries) {
153
- const sourcePath = path.join(pnpmNodeModulesDir, entry.name);
375
+ const sourcePath = path.join(storeNodeModulesDir, entry.name);
154
376
  if (entry.name.startsWith("@") && entry.isDirectory()) {
155
- const scopedEntries = await readdir(sourcePath, { withFileTypes: true });
377
+ const scopedEntries = await unsupportedFilesystemBoundary(signal, () => readdir(sourcePath, { withFileTypes: true }));
156
378
  for (const scopedEntry of scopedEntries) {
157
379
  const scopedDestination = path.join(nodeModulesDir, entry.name, scopedEntry.name);
158
- if (await pathExists(scopedDestination)) continue;
159
- await mkdir(path.dirname(scopedDestination), { recursive: true });
380
+ if (await pathExists(scopedDestination, signal)) continue;
381
+ await unsupportedFilesystemBoundary(signal, () => mkdir(path.dirname(scopedDestination), { recursive: true }));
160
382
  await copyPathMaterializingSymlinks(path.join(sourcePath, scopedEntry.name), scopedDestination, {
161
- standaloneRoot: pnpmNodeModulesDir,
383
+ standaloneRoot: storeNodeModulesDir,
162
384
  appRoot: nodeModulesDir,
163
- sourceRoot: nodeModulesDir
385
+ sourceRoot: nodeModulesDir,
386
+ signal
164
387
  });
165
388
  }
166
389
  continue;
167
390
  }
168
391
  const destinationPath = path.join(nodeModulesDir, entry.name);
169
- if (await pathExists(destinationPath)) continue;
392
+ if (await pathExists(destinationPath, signal)) continue;
170
393
  await copyPathMaterializingSymlinks(sourcePath, destinationPath, {
171
- standaloneRoot: pnpmNodeModulesDir,
394
+ standaloneRoot: storeNodeModulesDir,
172
395
  appRoot: nodeModulesDir,
173
- sourceRoot: nodeModulesDir
396
+ sourceRoot: nodeModulesDir,
397
+ signal
174
398
  });
175
399
  }
176
400
  }
177
- async function normalizeArtifactSymlinks(artifactDir, appPath) {
401
+ async function normalizeArtifactSymlinks(artifactDir, appPath, signal) {
178
402
  const normalizedArtifactDir = path.resolve(artifactDir);
179
403
  const normalizedAppPath = path.resolve(appPath);
180
404
  await walkDirectory(normalizedArtifactDir);
181
405
  async function walkDirectory(directory) {
182
- const entries = await readdir(directory, { withFileTypes: true });
406
+ const entries = await unsupportedFilesystemBoundary(signal, () => readdir(directory, { withFileTypes: true }));
183
407
  for (const entry of entries) {
184
408
  const fullPath = path.join(directory, entry.name);
185
409
  if (entry.isDirectory()) {
@@ -187,19 +411,19 @@ async function normalizeArtifactSymlinks(artifactDir, appPath) {
187
411
  continue;
188
412
  }
189
413
  if (!entry.isSymbolicLink()) continue;
190
- const target = await readlink(fullPath);
414
+ const target = await unsupportedFilesystemBoundary(signal, () => readlink(fullPath));
191
415
  const resolvedTarget = path.resolve(path.dirname(fullPath), target);
192
416
  if (isPathWithin(normalizedArtifactDir, resolvedTarget)) continue;
193
417
  if (!isPathWithin(normalizedAppPath, resolvedTarget)) throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`);
194
- const targetStat = await stat(resolvedTarget);
195
- await rm(fullPath, {
418
+ const targetStat = await unsupportedFilesystemBoundary(signal, () => stat(resolvedTarget));
419
+ await unsupportedFilesystemBoundary(signal, () => rm(fullPath, {
196
420
  force: true,
197
421
  recursive: true
198
- });
199
- await cp(resolvedTarget, fullPath, {
422
+ }));
423
+ await unsupportedFilesystemBoundary(signal, () => cp(resolvedTarget, fullPath, {
200
424
  recursive: targetStat.isDirectory(),
201
425
  dereference: true
202
- });
426
+ }));
203
427
  if (targetStat.isDirectory()) await walkDirectory(fullPath);
204
428
  }
205
429
  }
@@ -208,8 +432,11 @@ function isPathWithin(rootPath, candidatePath) {
208
432
  const relativePath = path.relative(rootPath, candidatePath);
209
433
  return relativePath === "" || !relativePath.startsWith(`..${path.sep}`) && relativePath !== ".." && !path.isAbsolute(relativePath);
210
434
  }
435
+ function isPathWithinWorkspaceDependency(sourceRoot, candidatePath) {
436
+ return isPathWithin(path.join(sourceRoot, "node_modules"), candidatePath);
437
+ }
211
438
  async function copyPathMaterializingSymlinks(sourcePath, destinationPath, options) {
212
- const sourceStat = await lstat(sourcePath);
439
+ const sourceStat = await unsupportedFilesystemBoundary(options.signal, () => lstat(sourcePath));
213
440
  if (sourceStat.isSymbolicLink()) {
214
441
  const resolvedTarget = await resolveSymlinkTarget(sourcePath, options);
215
442
  if (resolvedTarget === null) return;
@@ -217,27 +444,27 @@ async function copyPathMaterializingSymlinks(sourcePath, destinationPath, option
217
444
  return;
218
445
  }
219
446
  if (sourceStat.isDirectory()) {
220
- await mkdir(destinationPath, { recursive: true });
221
- const entries = await readdir(sourcePath, { withFileTypes: true });
447
+ await unsupportedFilesystemBoundary(options.signal, () => mkdir(destinationPath, { recursive: true }));
448
+ const entries = await unsupportedFilesystemBoundary(options.signal, () => readdir(sourcePath, { withFileTypes: true }));
222
449
  for (const entry of entries) await copyPathMaterializingSymlinks(path.join(sourcePath, entry.name), path.join(destinationPath, entry.name), options);
223
450
  return;
224
451
  }
225
452
  if (sourceStat.isFile()) {
226
- await mkdir(path.dirname(destinationPath), { recursive: true });
227
- await copyFile(sourcePath, destinationPath);
228
- await chmod(destinationPath, sourceStat.mode);
453
+ await unsupportedFilesystemBoundary(options.signal, () => mkdir(path.dirname(destinationPath), { recursive: true }));
454
+ await unsupportedFilesystemBoundary(options.signal, () => copyFile(sourcePath, destinationPath));
455
+ await unsupportedFilesystemBoundary(options.signal, () => chmod(destinationPath, sourceStat.mode));
229
456
  }
230
457
  }
231
458
  async function resolveSymlinkTarget(symlinkPath, options) {
232
- const linkTarget = await readlink(symlinkPath);
459
+ const linkTarget = await unsupportedFilesystemBoundary(options.signal, () => readlink(symlinkPath));
233
460
  const resolvedTarget = path.resolve(path.dirname(symlinkPath), linkTarget);
234
- if (await pathExists(resolvedTarget)) {
235
- if (!isPathWithin(options.appRoot, resolvedTarget) && !isPathWithin(options.sourceRoot, resolvedTarget)) throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`);
461
+ if (await pathExists(resolvedTarget, options.signal)) {
462
+ if (!isPathWithin(options.appRoot, resolvedTarget) && !isPathWithinWorkspaceDependency(options.sourceRoot, resolvedTarget)) throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`);
236
463
  return resolvedTarget;
237
464
  }
238
465
  if (isPathWithin(options.standaloneRoot, resolvedTarget)) {
239
466
  const fallbackTarget = path.join(options.appRoot, path.relative(options.standaloneRoot, resolvedTarget));
240
- if (await pathExists(fallbackTarget)) return fallbackTarget;
467
+ if (await pathExists(fallbackTarget, options.signal)) return fallbackTarget;
241
468
  }
242
469
  if (isPnpmHoistLink(symlinkPath)) return null;
243
470
  throw new Error(`Next.js standalone symlink target is missing: ${symlinkPath} -> ${linkTarget} (resolved to ${resolvedTarget})`);
@@ -247,38 +474,28 @@ function isPnpmHoistLink(symlinkPath) {
247
474
  for (let i = 0; i < parts.length - 1; i++) if (parts[i] === ".pnpm" && parts[i + 1] === "node_modules") return true;
248
475
  return false;
249
476
  }
250
- async function pathExists(targetPath) {
477
+ async function pathExists(targetPath, signal) {
251
478
  try {
252
- await stat(targetPath);
479
+ await unsupportedFilesystemBoundary(signal, () => stat(targetPath));
253
480
  return true;
254
- } catch {
481
+ } catch (error) {
482
+ if (signal?.aborted) throw error;
255
483
  return false;
256
484
  }
257
485
  }
258
- async function directoryExists(targetPath) {
486
+ async function directoryExists(targetPath, signal) {
259
487
  try {
260
- return (await stat(targetPath)).isDirectory();
261
- } catch {
488
+ return (await unsupportedFilesystemBoundary(signal, () => stat(targetPath))).isDirectory();
489
+ } catch (error) {
490
+ if (signal?.aborted) throw error;
262
491
  return false;
263
492
  }
264
493
  }
265
- async function resolveSourceRoot(appRoot) {
266
- let current = path.resolve(appRoot);
267
- while (true) {
268
- if (await pathExists(path.join(current, ".git")) || await pathExists(path.join(current, "pnpm-workspace.yaml")) || await pathExists(path.join(current, "bun.lock")) || await pathExists(path.join(current, "bun.lockb")) || await packageJsonDeclaresWorkspaces(current)) return current;
269
- const parent = path.dirname(current);
270
- if (parent === current) return path.resolve(appRoot);
271
- current = parent;
272
- }
273
- }
274
- async function packageJsonDeclaresWorkspaces(directory) {
275
- try {
276
- const content = await readFile(path.join(directory, "package.json"), "utf8");
277
- const parsed = JSON.parse(content);
278
- return Boolean(parsed.workspaces);
279
- } catch {
280
- return false;
281
- }
494
+ async function unsupportedFilesystemBoundary(signal, operation) {
495
+ signal?.throwIfAborted();
496
+ const result = await operation();
497
+ signal?.throwIfAborted();
498
+ return result;
282
499
  }
283
500
  //#endregion
284
501
  export { PREVIEW_BUILD_TYPES, PreviewBuildStrategy, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild };