@prisma/cli 3.0.0-dev.69.1 → 3.0.0-dev.71.1

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.
@@ -65,7 +65,7 @@ function createDeployCommand(runtime) {
65
65
  "hono",
66
66
  "tanstack-start",
67
67
  "bun"
68
- ])).addOption(new Option("--entry <path>", "Entrypoint path for Bun deploys")).addOption(new Option("--http-port <port>", "HTTP port override for the deployed app")).addOption(new Option("--env <name=value|file>", "Environment variable assignment or dotenv file").argParser(collectRepeatableValues)).addOption(new Option("--db", "Create and wire an isolated database for the preview Branch")).addOption(new Option("--no-db", "Skip branch database setup")).addOption(new Option("--prod", "Confirm intent to deploy to production"));
68
+ ])).addOption(new Option("--entry <path>", "Entrypoint path for Bun deploys")).addOption(new Option("--http-port <port>", "HTTP port override for the deployed app")).addOption(new Option("--env <name=value|file>", "Environment variable assignment or dotenv file").argParser(collectRepeatableValues)).addOption(new Option("--db", "Create and wire a Prisma Postgres database for this deploy target")).addOption(new Option("--no-db", "Skip database setup")).addOption(new Option("--prod", "Confirm intent to deploy to production"));
69
69
  addGlobalFlags(command);
70
70
  command.action(async (options) => {
71
71
  const appName = options.app;
@@ -80,7 +80,7 @@ function createDeployCommand(runtime) {
80
80
  const db = options.db;
81
81
  const hasDbConflict = hasFlag(runtime.argv, "--db") && hasFlag(runtime.argv, "--no-db");
82
82
  await runCommand(runtime, "app.deploy", options, (context) => {
83
- if (hasDbConflict) throw usageError("app deploy accepts either --db or --no-db", "--db requests branch database setup, while --no-db disables it.", "Pass exactly one database setup flag.", ["prisma-cli app deploy --db", "prisma-cli app deploy --no-db"], "app");
83
+ if (hasDbConflict) throw usageError("app deploy accepts either --db or --no-db", "--db requests database setup, while --no-db disables it.", "Pass exactly one database setup flag.", ["prisma-cli app deploy --db", "prisma-cli app deploy --no-db"], "app");
84
84
  return runAppDeploy(context, appName, {
85
85
  projectRef,
86
86
  createProjectName,
@@ -17,6 +17,7 @@ import { buildProjectSetupNextActions, inferTargetName, projectNotFoundError, re
17
17
  import { bindProjectToDirectory, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary } from "../lib/project/setup.js";
18
18
  import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js";
19
19
  import { readLocalGitBranch } from "../lib/git/local-branch.js";
20
+ import { resolveOrCreatePreviewBuildSettings } from "../lib/app/preview-build-settings.js";
20
21
  import { PREVIEW_BUILD_TYPES, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild } from "../lib/app/preview-build.js";
21
22
  import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction.js";
22
23
  import { maybeSetupBranchDatabase } from "../lib/app/branch-database-deploy.js";
@@ -161,7 +162,7 @@ async function runAppDeploy(context, appName, options) {
161
162
  });
162
163
  framework = customized.framework;
163
164
  runtime = customized.runtime;
164
- await enforceProductionDeployGate(context, provider, {
165
+ const productionDeployGate = await enforceProductionDeployGate(context, provider, {
165
166
  appId: selectedApp.appId,
166
167
  appName: selectedApp.displayName,
167
168
  branchKind: target.branch.kind,
@@ -170,10 +171,17 @@ async function runAppDeploy(context, appName, options) {
170
171
  const buildType = framework.buildType;
171
172
  assertSupportedEntrypoint(buildType, options?.entrypoint, "deploy");
172
173
  const entrypoint = await resolveDeployEntrypoint(context.runtime.cwd, framework, options?.entrypoint, context.runtime.signal);
174
+ const buildSettingsResolution = await resolveOrCreatePreviewBuildSettings({
175
+ appPath: context.runtime.cwd,
176
+ buildType,
177
+ signal: context.runtime.signal
178
+ });
179
+ maybeRenderDeployBuildSettings(context, buildSettingsResolution);
173
180
  const portMapping = parseDeployPortMapping(String(runtime.port));
174
181
  const branchDatabaseSetup = await maybeSetupBranchDatabase(context, provider, projectId, toBranchDatabaseDeployBranch(target.branch), {
175
182
  db: options?.db,
176
- providedEnvVars: envVars
183
+ providedEnvVars: envVars,
184
+ firstProductionDeploy: productionDeployGate.firstProductionDeploy
177
185
  });
178
186
  const progressState = createPreviewDeployProgressState();
179
187
  const deployStartedAt = Date.now();
@@ -186,6 +194,7 @@ async function runAppDeploy(context, appName, options) {
186
194
  region: selectedApp.region,
187
195
  entrypoint,
188
196
  buildType,
197
+ buildSettings: buildSettingsResolution.settings,
189
198
  portMapping,
190
199
  envVars,
191
200
  interaction: void 0,
@@ -214,6 +223,18 @@ async function runAppDeploy(context, appName, options) {
214
223
  },
215
224
  deployment: deployResult.deployment,
216
225
  deploySettings: {
226
+ config: {
227
+ path: buildSettingsResolution.relativeConfigPath,
228
+ status: buildSettingsResolution.status
229
+ },
230
+ buildCommand: {
231
+ value: buildSettingsResolution.settings.buildCommand,
232
+ source: buildSettingsResolution.settings.buildCommandSource
233
+ },
234
+ outputDirectory: {
235
+ value: buildSettingsResolution.settings.outputDirectory,
236
+ source: buildSettingsResolution.settings.outputDirectorySource
237
+ },
217
238
  framework: {
218
239
  key: framework.key,
219
240
  buildType,
@@ -1603,7 +1624,6 @@ async function detectNextConfig(cwd, signal) {
1603
1624
  for (const candidate of [
1604
1625
  "next.config.js",
1605
1626
  "next.config.mjs",
1606
- "next.config.cjs",
1607
1627
  "next.config.ts",
1608
1628
  "next.config.mts"
1609
1629
  ]) {
@@ -1696,6 +1716,20 @@ async function maybeRenderDeploySetupBlock(context, details) {
1696
1716
  const prefix = details.includeDirectory ? `Deploying ${directory} to` : "Deploying to";
1697
1717
  context.output.stderr.write(`${prefix} ${details.projectName} / ${details.branchName} / ${details.appName}\n\n`);
1698
1718
  }
1719
+ function maybeRenderDeployBuildSettings(context, resolution) {
1720
+ if (context.flags.json || context.flags.quiet) return;
1721
+ const settings = resolution.settings;
1722
+ const title = resolution.status === "created" ? `Created ${resolution.relativeConfigPath}` : `Using ${resolution.relativeConfigPath}`;
1723
+ context.output.stderr.write(`${title}\n${renderDeployOutputRows(context.ui, [{
1724
+ label: "Build Command",
1725
+ value: settings.buildCommand ?? "none",
1726
+ origin: settings.buildCommandSource ?? void 0
1727
+ }, {
1728
+ label: "Output Directory",
1729
+ value: settings.outputDirectory,
1730
+ origin: settings.outputDirectorySource ?? void 0
1731
+ }]).join("\n")}\n\n`);
1732
+ }
1699
1733
  function maybeRenderProjectLinked(context, directory, projectName, localPinPath) {
1700
1734
  if (context.flags.json || context.flags.quiet) return;
1701
1735
  context.output.stderr.write(`${context.ui.success("✔")} Linked "${directory}" to Project "${projectName}"\nSaved ${localPinPath}\n\n`);
@@ -9,65 +9,65 @@ import path from "node:path";
9
9
  async function maybeSetupBranchDatabase(context, provider, projectId, branch, options) {
10
10
  if (options.db === false) return emptyBranchDatabaseSetupOutcome();
11
11
  if (hasProvidedDatabaseEnvVars(options.providedEnvVars)) {
12
- if (options.db === true) throw usageError("Branch database setup cannot be combined with provided database env vars", "The deploy command received --db and a DATABASE_URL or DIRECT_URL value from --env.", "Remove the --env database value to let --db create a branch override, or remove --db to deploy with the provided value.", ["prisma-cli app deploy --db", "prisma-cli app deploy --env DATABASE_URL=postgresql://example"], "app");
12
+ if (options.db === true) throw usageError("Database setup cannot be combined with provided database env vars", "The deploy command received --db and a DATABASE_URL or DIRECT_URL value from --env.", "Remove the --env database value to let --db create and wire a database, or remove --db to deploy with the provided value.", ["prisma-cli app deploy --db", "prisma-cli app deploy --env DATABASE_URL=postgresql://example"], "app");
13
13
  return emptyBranchDatabaseSetupOutcome();
14
14
  }
15
- if (branch.kind === "production") {
16
- if (options.db === true) throw usageError("Branch database setup is only available for preview branches", "Production database wiring is a durable environment decision and is not created implicitly by app deploy.", "Use project env commands to manage production DATABASE_URL, or deploy a preview branch with --db.", ["prisma-cli project env add DATABASE_URL=<value> --role production", "prisma-cli app deploy --branch feature/db --db"], "app");
15
+ if (branch.kind === "production" && !options.firstProductionDeploy) {
16
+ if (options.db === true) throw productionDatabaseSetupAfterFirstDeployError();
17
17
  return emptyBranchDatabaseSetupOutcome();
18
18
  }
19
- const localSignal = await inspectBranchDatabaseSignal(context.runtime.cwd, context.runtime.signal);
20
- const envState = await inspectBranchDatabaseEnv(provider, projectId, branch.id, context.runtime.signal);
21
- const branchEnvVars = [envState.branchDatabaseUrl, envState.branchDirectUrl].filter((variable) => Boolean(variable)).map((variable) => variable.key).sort();
22
- if (envState.branchDatabaseUrl) {
23
- const warning = options.db === true ? `Branch "${branch.name}" already has DATABASE_URL. Leaving branch database env vars unchanged.` : null;
19
+ const envState = await inspectBranchDatabaseEnv(provider, projectId, branch, context.runtime.signal);
20
+ const targetEnvVars = getTargetDatabaseEnvVarKeys(envState);
21
+ if (hasExistingDatabaseEnvForTarget(branch, envState)) {
22
+ const warning = options.db === true ? existingDatabaseEnvWarning(branch, targetEnvVars) : null;
24
23
  if (warning) emitBranchDatabaseWarning(context, warning);
25
24
  return {
26
25
  result: options.db === true ? {
27
26
  status: "skipped",
28
- reason: "branch-env-exists",
29
- envVars: branchEnvVars,
27
+ reason: existingDatabaseEnvReason(branch),
28
+ envVars: targetEnvVars,
30
29
  schema: null
31
30
  } : void 0,
32
31
  warnings: warning ? [warning] : []
33
32
  };
34
33
  }
34
+ const localSignal = await inspectBranchDatabaseSignal(context.runtime.cwd, context.runtime.signal);
35
35
  if (localSignal.unsupportedSchema) {
36
- if (options.db === true) throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch.name, context);
36
+ if (options.db === true) throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch, context);
37
37
  return emptyBranchDatabaseSetupOutcome();
38
38
  }
39
- const hasSignal = hasBranchDatabaseSignal(localSignal) || Boolean(envState.previewDatabaseUrl);
39
+ const hasSignal = hasBranchDatabaseSignal(localSignal) || Boolean(envState.inheritedPreviewDatabaseUrl);
40
40
  if (options.db !== true) {
41
41
  if (!hasSignal) return emptyBranchDatabaseSetupOutcome();
42
42
  if (!canPrompt(context) || context.flags.yes) {
43
- const warning = "This app appears to use DATABASE_URL. Run prisma-cli app deploy --db to create an isolated database for this preview branch.";
43
+ const warning = databasePromptSuppressedWarning(branch);
44
44
  emitBranchDatabaseWarning(context, warning);
45
45
  return {
46
46
  result: void 0,
47
47
  warnings: [warning]
48
48
  };
49
49
  }
50
- maybeRenderBranchDatabaseSignal(context, branch.name, localSignal, envState);
50
+ maybeRenderBranchDatabaseSignal(context, branch, localSignal, envState);
51
51
  if (!await confirmPrompt({
52
52
  input: context.runtime.stdin,
53
53
  output: context.output.stderr,
54
- message: `Create an isolated database for branch "${branch.name}"?`,
54
+ message: databasePromptMessage(branch),
55
55
  initialValue: false
56
56
  })) return emptyBranchDatabaseSetupOutcome();
57
- }
57
+ } else if (!canPrompt(context) && !context.flags.yes) throw nonInteractiveDatabaseSetupRequiresYesError(branch);
58
58
  return setupBranchDatabase(context, provider, projectId, branch, localSignal, envState);
59
59
  }
60
60
  async function setupBranchDatabase(context, provider, projectId, branch, signal, envState) {
61
- emitBranchDatabaseProgress(context, "pending", "Creating branch database");
61
+ emitBranchDatabaseProgress(context, "pending", "Creating database");
62
62
  const database = await provider.createBranchDatabase({
63
63
  projectId,
64
64
  branchId: branch.id,
65
65
  branchName: branch.name,
66
66
  signal: context.runtime.signal
67
67
  }).catch((error) => {
68
- throw branchDatabaseSetupFailedError("Failed to create branch database", error, branch.name);
68
+ throw branchDatabaseSetupFailedError("Failed to create database", error, branch);
69
69
  });
70
- emitBranchDatabaseProgress(context, "success", "Created branch database");
70
+ emitBranchDatabaseProgress(context, "success", "Created database");
71
71
  try {
72
72
  let schemaSetup = null;
73
73
  const warnings = [];
@@ -80,12 +80,12 @@ async function setupBranchDatabase(context, provider, projectId, branch, signal,
80
80
  databaseUrl: database.databaseUrl,
81
81
  directUrl: database.directUrl
82
82
  }).catch((error) => {
83
- throw schemaSetupFailedError(error, signal.schema, branch.name, context.runtime.cwd);
83
+ throw schemaSetupFailedError(error, signal.schema, branch, context.runtime.cwd);
84
84
  });
85
85
  emitBranchDatabaseProgress(context, "success", "Applied database schema");
86
- } else skippedSchemaWarning = "No supported Prisma schema source was found. Branch database env vars were created, but schema setup was skipped.";
86
+ } else skippedSchemaWarning = "No supported Prisma schema source was found. Database env vars were created, but schema setup was skipped.";
87
87
  const envVars = await upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState);
88
- emitBranchDatabaseProgress(context, "success", `Added branch env override${envVars.length === 1 ? "" : "s"} ${envVars.join(", ")}`);
88
+ emitBranchDatabaseProgress(context, "success", `Added ${envScopeLabel(branch)} env var${envVars.length === 1 ? "" : "s"} ${envVars.join(", ")}`);
89
89
  if (skippedSchemaWarning) {
90
90
  emitBranchDatabaseWarning(context, skippedSchemaWarning);
91
91
  warnings.push(skippedSchemaWarning);
@@ -107,37 +107,36 @@ async function setupBranchDatabase(context, provider, projectId, branch, signal,
107
107
  warnings
108
108
  };
109
109
  } catch (error) {
110
- throw await cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch.name, error);
110
+ throw await cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch, error);
111
111
  }
112
112
  }
113
113
  async function upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState) {
114
+ const scope = envScopeForBranch(branch);
114
115
  const written = [];
115
116
  await upsertBranchDatabaseEnvVar(context, provider, {
116
117
  projectId,
117
- branchId: branch.id,
118
- className: "preview",
118
+ ...scope,
119
119
  key: "DATABASE_URL",
120
120
  value: database.databaseUrl,
121
- existing: envState.branchDatabaseUrl,
122
- branchName: branch.name
121
+ existing: envState.targetDatabaseUrl,
122
+ branch
123
123
  });
124
124
  written.push("DATABASE_URL");
125
125
  if (database.directUrl) {
126
126
  await upsertBranchDatabaseEnvVar(context, provider, {
127
127
  projectId,
128
- branchId: branch.id,
129
- className: "preview",
128
+ ...scope,
130
129
  key: "DIRECT_URL",
131
130
  value: database.directUrl,
132
- existing: envState.branchDirectUrl,
133
- branchName: branch.name
131
+ existing: envState.targetDirectUrl,
132
+ branch
134
133
  });
135
134
  written.push("DIRECT_URL");
136
- } else if (envState.branchDirectUrl) await provider.deleteEnvironmentVariable({
137
- envVarId: envState.branchDirectUrl.id,
135
+ } else if (branch.kind === "preview" && envState.targetDirectUrl) await provider.deleteEnvironmentVariable({
136
+ envVarId: envState.targetDirectUrl.id,
138
137
  signal: context.runtime.signal
139
138
  }).catch((error) => {
140
- throw branchDatabaseSetupFailedError("Failed to remove stale DIRECT_URL", error, branch.name);
139
+ throw branchDatabaseSetupFailedError("Failed to remove stale DIRECT_URL", error, branch);
141
140
  });
142
141
  return written;
143
142
  }
@@ -148,37 +147,39 @@ async function upsertBranchDatabaseEnvVar(context, provider, options) {
148
147
  value: options.value,
149
148
  signal: context.runtime.signal
150
149
  }).catch((error) => {
151
- throw branchDatabaseSetupFailedError(`Failed to update ${options.key}`, error, options.branchName);
150
+ throw branchDatabaseSetupFailedError(`Failed to update ${options.key}`, error, options.branch);
152
151
  });
153
152
  return;
154
153
  }
155
154
  await provider.createEnvironmentVariable({
156
155
  projectId: options.projectId,
157
- branchId: options.branchId,
158
156
  className: options.className,
159
157
  key: options.key,
160
158
  value: options.value,
159
+ ...options.branchId ? { branchId: options.branchId } : {},
161
160
  signal: context.runtime.signal
162
161
  }).catch((error) => {
163
- throw branchDatabaseSetupFailedError(`Failed to write ${options.key}`, error, options.branchName);
162
+ throw branchDatabaseSetupFailedError(`Failed to write ${options.key}`, error, options.branch);
164
163
  });
165
164
  }
166
- async function inspectBranchDatabaseEnv(provider, projectId, branchId, signal) {
165
+ async function inspectBranchDatabaseEnv(provider, projectId, branch, signal) {
166
+ const scope = envScopeForBranch(branch);
167
167
  const [databaseUrlRows, directUrlRows] = await Promise.all([provider.listEnvironmentVariables({
168
168
  projectId,
169
- className: "preview",
169
+ className: scope.className,
170
170
  key: "DATABASE_URL",
171
171
  signal
172
172
  }), provider.listEnvironmentVariables({
173
173
  projectId,
174
- className: "preview",
174
+ className: scope.className,
175
175
  key: "DIRECT_URL",
176
176
  signal
177
177
  })]);
178
+ const targetBranchId = branch.kind === "preview" ? branch.id : null;
178
179
  return {
179
- branchDatabaseUrl: findEnvVar(databaseUrlRows, { branchId }),
180
- branchDirectUrl: findEnvVar(directUrlRows, { branchId }),
181
- previewDatabaseUrl: findEnvVar(databaseUrlRows, { branchId: null })
180
+ targetDatabaseUrl: findEnvVar(databaseUrlRows, { branchId: targetBranchId }),
181
+ targetDirectUrl: findEnvVar(directUrlRows, { branchId: targetBranchId }),
182
+ inheritedPreviewDatabaseUrl: branch.kind === "preview" ? findEnvVar(databaseUrlRows, { branchId: null }) : null
182
183
  };
183
184
  }
184
185
  function findEnvVar(rows, options) {
@@ -187,14 +188,47 @@ function findEnvVar(rows, options) {
187
188
  function hasProvidedDatabaseEnvVars(envVars) {
188
189
  return Boolean(envVars && ("DATABASE_URL" in envVars || "DIRECT_URL" in envVars));
189
190
  }
190
- function maybeRenderBranchDatabaseSignal(context, branchName, signal, envState) {
191
+ function envScopeForBranch(branch) {
192
+ return branch.kind === "production" ? { className: "production" } : {
193
+ className: "preview",
194
+ branchId: branch.id
195
+ };
196
+ }
197
+ function envScopeLabel(branch) {
198
+ return branch.kind === "production" ? "production" : "branch";
199
+ }
200
+ function getTargetDatabaseEnvVarKeys(envState) {
201
+ return [envState.targetDatabaseUrl, envState.targetDirectUrl].filter((variable) => Boolean(variable)).map((variable) => variable.key).sort();
202
+ }
203
+ function hasExistingDatabaseEnvForTarget(branch, envState) {
204
+ if (branch.kind === "production") return Boolean(envState.targetDatabaseUrl || envState.targetDirectUrl);
205
+ return Boolean(envState.targetDatabaseUrl);
206
+ }
207
+ function existingDatabaseEnvReason(branch) {
208
+ return branch.kind === "production" ? "production-env-exists" : "branch-env-exists";
209
+ }
210
+ function existingDatabaseEnvWarning(branch, envVars) {
211
+ if (branch.kind === "production") return `Production already has ${envVars.join(" and ")}. Treating it as BYO database configuration and leaving env vars unchanged.`;
212
+ return `Branch "${branch.name}" already has DATABASE_URL. Leaving branch database env vars unchanged.`;
213
+ }
214
+ function databasePromptSuppressedWarning(branch) {
215
+ if (branch.kind === "production") return "This app appears to use DATABASE_URL. Run prisma-cli app deploy --db --yes to create and wire a Prisma Postgres database for this first production deploy.";
216
+ return "This app appears to use DATABASE_URL. Run prisma-cli app deploy --db to create an isolated database for this preview branch.";
217
+ }
218
+ function databasePromptMessage(branch) {
219
+ return branch.kind === "production" ? "Create a Prisma Postgres database for production?" : `Create an isolated database for branch "${branch.name}"?`;
220
+ }
221
+ function maybeRenderBranchDatabaseSignal(context, branch, signal, envState) {
191
222
  if (context.flags.json || context.flags.quiet) return;
192
223
  const rows = [
193
224
  signal.schema ? ` Schema ${path.relative(context.runtime.cwd, signal.schema.path) || defaultSchemaSourcePath(signal.schema)}` : null,
194
225
  signal.databaseUrlReferences.length > 0 ? ` Code ${signal.databaseUrlReferences.slice(0, 3).join(", ")}` : null,
195
- envState.previewDatabaseUrl ? " Env preview DATABASE_URL is inherited by this branch" : null
226
+ envState.inheritedPreviewDatabaseUrl ? " Env preview DATABASE_URL is inherited by this branch" : null
196
227
  ].filter((row) => Boolean(row));
197
- context.output.stderr.write(`Database signal found for branch "${branchName}"\n${rows.join("\n")}\n\n`);
228
+ context.output.stderr.write(`Database signal found for ${databaseTargetLabel(branch)}\n${rows.join("\n")}\n\n`);
229
+ }
230
+ function databaseTargetLabel(branch) {
231
+ return branch.kind === "production" ? `production branch "${branch.name}"` : `branch "${branch.name}"`;
198
232
  }
199
233
  function emitBranchDatabaseProgress(context, status, message) {
200
234
  if (context.flags.json || context.flags.quiet) return;
@@ -211,6 +245,12 @@ function emptyBranchDatabaseSetupOutcome() {
211
245
  warnings: []
212
246
  };
213
247
  }
248
+ function productionDatabaseSetupAfterFirstDeployError() {
249
+ return usageError("Database setup is only available during the first production deploy", "The selected production app already has a live deployment.", "Use project env commands to manage production DATABASE_URL, or deploy a preview branch with --db.", ["prisma-cli project env add DATABASE_URL=<value> --role production", "prisma-cli app deploy --branch feature/db --db"], "app");
250
+ }
251
+ function nonInteractiveDatabaseSetupRequiresYesError(branch) {
252
+ return usageError("Database setup requires --yes in non-interactive mode", "The deploy command received --db, but prompts are not available and --yes was not passed.", "Pass --yes together with --db to confirm non-interactive database creation.", [branch.kind === "production" ? "prisma-cli app deploy --prod --db --yes" : `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)} --db --yes`], "app");
253
+ }
214
254
  function formatSchemaSetupCommand(command) {
215
255
  switch (command) {
216
256
  case "migrate-deploy": return "prisma migrate deploy";
@@ -218,46 +258,46 @@ function formatSchemaSetupCommand(command) {
218
258
  case "prisma-next-db-init": return "prisma-next db init";
219
259
  }
220
260
  }
221
- function branchDatabaseSetupFailedError(summary, error, branchName) {
261
+ function branchDatabaseSetupFailedError(summary, error, branch) {
222
262
  return new CliError({
223
263
  code: "BRANCH_DATABASE_SETUP_FAILED",
224
264
  domain: "app",
225
265
  summary,
226
266
  why: error instanceof Error ? error.message : String(error),
227
- fix: "Retry the command, or create the branch database and env vars manually with project env commands.",
267
+ fix: "Retry the command, or create the database and env vars manually with project env commands.",
228
268
  debug: formatDebugDetails(error),
229
- meta: { branch: branchName },
269
+ meta: { branch: branch.name },
230
270
  exitCode: 1,
231
- nextSteps: [`prisma-cli app deploy --branch ${formatCommandArgument(branchName)} --db`, `prisma-cli project env list --branch ${formatCommandArgument(branchName)}`]
271
+ nextSteps: [formatAppDeployWithDbNextStep(branch), formatProjectEnvListNextStep(branch)]
232
272
  });
233
273
  }
234
- async function cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branchName, error) {
235
- const setupError = error instanceof CliError ? error : branchDatabaseSetupFailedError("Branch database setup failed", error, branchName);
236
- emitBranchDatabaseProgress(context, "pending", "Removing branch database after setup failed");
274
+ async function cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch, error) {
275
+ const setupError = error instanceof CliError ? error : branchDatabaseSetupFailedError("Database setup failed", error, branch);
276
+ emitBranchDatabaseProgress(context, "pending", "Removing database after setup failed");
237
277
  try {
238
278
  await provider.deleteBranchDatabase({
239
279
  databaseId: database.id,
240
280
  signal: context.runtime.signal
241
281
  });
242
- emitBranchDatabaseProgress(context, "success", "Removed branch database after setup failed");
282
+ emitBranchDatabaseProgress(context, "success", "Removed database after setup failed");
243
283
  } catch (cleanupError) {
244
- return branchDatabaseCleanupFailedError(setupError, cleanupError, database, branchName);
284
+ return branchDatabaseCleanupFailedError(setupError, cleanupError, database, branch);
245
285
  }
246
286
  return setupError;
247
287
  }
248
- function branchDatabaseCleanupFailedError(setupError, cleanupError, database, branchName) {
288
+ function branchDatabaseCleanupFailedError(setupError, cleanupError, database, branch) {
249
289
  const cleanupWhy = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
250
- const setupWhy = setupError.why ?? "Branch database setup failed.";
290
+ const setupWhy = setupError.why ?? "Database setup failed.";
251
291
  return new CliError({
252
292
  code: setupError.code,
253
293
  domain: setupError.domain,
254
294
  summary: setupError.summary,
255
295
  why: `${setupWhy} Prisma could not delete the created database "${database.name}" (${database.id}): ${cleanupWhy}`,
256
- fix: "Delete the created branch database from Console or contact Prisma support, then rerun deploy with --db.",
296
+ fix: "Delete the created database from Console or contact Prisma support, then rerun deploy with --db.",
257
297
  debug: formatCombinedDebugDetails(setupError, cleanupError),
258
298
  meta: {
259
299
  ...setupError.meta,
260
- branch: branchName,
300
+ branch: branch.name,
261
301
  databaseId: database.id,
262
302
  databaseName: database.name,
263
303
  cleanupFailed: true
@@ -266,7 +306,7 @@ function branchDatabaseCleanupFailedError(setupError, cleanupError, database, br
266
306
  nextSteps: []
267
307
  });
268
308
  }
269
- function schemaSetupFailedError(error, schema, branchName, cwd) {
309
+ function schemaSetupFailedError(error, schema, branch, cwd) {
270
310
  return new CliError({
271
311
  code: "SCHEMA_SETUP_FAILED",
272
312
  domain: "app",
@@ -275,17 +315,26 @@ function schemaSetupFailedError(error, schema, branchName, cwd) {
275
315
  fix: "Fix the Prisma schema or migrations, then rerun deploy with --db.",
276
316
  debug: formatDebugDetails(error),
277
317
  meta: {
278
- branch: branchName,
318
+ branch: branch.name,
279
319
  schemaPath: schema.path,
280
320
  source: schema.kind,
281
321
  command: schema.command
282
322
  },
283
323
  exitCode: 1,
284
- nextSteps: [...formatSchemaSetupNextSteps(schema, cwd), `prisma-cli app deploy --branch ${formatCommandArgument(branchName)} --db`]
324
+ nextSteps: [...formatSchemaSetupNextSteps(schema, cwd), formatAppDeployWithDbNextStep(branch)]
285
325
  });
286
326
  }
287
- function unsupportedBranchDatabaseSchemaError(schema, branchName, context) {
288
- return usageError("Branch database setup is not available for this Prisma schema", `${path.relative(context.runtime.cwd, schema.path) || defaultUnsupportedSchemaSourcePath(schema)} targets ${formatUnsupportedSchemaTarget(schema.target)}, but --db creates Prisma Postgres databases.`, "Use project env commands to provide a database URL for this branch, or switch the Prisma schema source to PostgreSQL before using --db.", [`prisma-cli project env add DATABASE_URL=<value> --branch ${formatCommandArgument(branchName)}`, `prisma-cli app deploy --branch ${formatCommandArgument(branchName)}`], "app");
327
+ function unsupportedBranchDatabaseSchemaError(schema, branch, context) {
328
+ return usageError("Database setup is not available for this Prisma schema", `${path.relative(context.runtime.cwd, schema.path) || defaultUnsupportedSchemaSourcePath(schema)} targets ${formatUnsupportedSchemaTarget(schema.target)}, but --db creates Prisma Postgres databases.`, "Use project env commands to provide a database URL, or switch the Prisma schema source to PostgreSQL before using --db.", [formatProjectEnvAddNextStep(branch), `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)}`], "app");
329
+ }
330
+ function formatAppDeployWithDbNextStep(branch) {
331
+ return `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)} --db`;
332
+ }
333
+ function formatProjectEnvListNextStep(branch) {
334
+ return branch.kind === "production" ? "prisma-cli project env list --role production" : `prisma-cli project env list --branch ${formatCommandArgument(branch.name)}`;
335
+ }
336
+ function formatProjectEnvAddNextStep(branch) {
337
+ return branch.kind === "production" ? "prisma-cli project env add DATABASE_URL=<value> --role production" : `prisma-cli project env add DATABASE_URL=<value> --branch ${formatCommandArgument(branch.name)}`;
289
338
  }
290
339
  function formatSchemaSetupNextSteps(schema, cwd) {
291
340
  const sourcePath = path.relative(cwd, schema.path) || defaultSchemaSourcePath(schema);
@@ -0,0 +1,385 @@
1
+ import { CliError } from "../../shell/errors.js";
2
+ import { readBunPackageJson } from "./bun-project.js";
3
+ import { readFile, readdir, stat, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { exec } from "node:child_process";
6
+ import { parseModule } from "magicast";
7
+ //#region src/lib/app/preview-build-settings.ts
8
+ const PRISMA_APP_CONFIG_FILENAME = "prisma.app.json";
9
+ const PRISMA_APP_CONFIG_SCHEMA_URL = "https://pris.ly/schemas/prisma-app-config.v1.json";
10
+ async function resolveOrCreatePreviewBuildSettings(options) {
11
+ const configPath = path.join(options.appPath, PRISMA_APP_CONFIG_FILENAME);
12
+ const existing = await readPreviewBuildSettingsConfig(configPath, options.signal);
13
+ if (existing) return {
14
+ status: "used",
15
+ configPath,
16
+ relativeConfigPath: PRISMA_APP_CONFIG_FILENAME,
17
+ settings: {
18
+ buildCommand: existing.buildCommand,
19
+ buildCommandSource: null,
20
+ outputDirectory: existing.outputDirectory,
21
+ outputDirectorySource: null
22
+ }
23
+ };
24
+ const settings = await resolvePreviewBuildSettings(options);
25
+ const config = {
26
+ $schema: PRISMA_APP_CONFIG_SCHEMA_URL,
27
+ buildCommand: settings.buildCommand,
28
+ outputDirectory: settings.outputDirectory
29
+ };
30
+ try {
31
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, {
32
+ encoding: "utf8",
33
+ flag: "wx",
34
+ signal: options.signal
35
+ });
36
+ } catch (error) {
37
+ if (error.code === "EEXIST") {
38
+ const raced = await readPreviewBuildSettingsConfig(configPath, options.signal);
39
+ if (raced) return {
40
+ status: "used",
41
+ configPath,
42
+ relativeConfigPath: PRISMA_APP_CONFIG_FILENAME,
43
+ settings: {
44
+ buildCommand: raced.buildCommand,
45
+ buildCommandSource: null,
46
+ outputDirectory: raced.outputDirectory,
47
+ outputDirectorySource: null
48
+ }
49
+ };
50
+ }
51
+ throw error;
52
+ }
53
+ return {
54
+ status: "created",
55
+ configPath,
56
+ relativeConfigPath: PRISMA_APP_CONFIG_FILENAME,
57
+ settings
58
+ };
59
+ }
60
+ async function resolvePreviewBuildSettings(options) {
61
+ switch (options.buildType) {
62
+ case "nextjs": {
63
+ const packageJson = await readBunPackageJson(options.appPath, options.signal);
64
+ const buildCommand = await resolveFrameworkBuildCommand(options.appPath, packageJson, {
65
+ command: "next build",
66
+ source: "Next.js default",
67
+ signal: options.signal
68
+ });
69
+ const outputRoot = await resolveNextOutputRoot(options.appPath, options.signal);
70
+ return {
71
+ buildCommand: buildCommand.command,
72
+ buildCommandSource: buildCommand.source,
73
+ outputDirectory: joinPosix(outputRoot, "standalone"),
74
+ outputDirectorySource: outputRoot === ".next" ? "Next.js output" : "next.config distDir"
75
+ };
76
+ }
77
+ case "tanstack-start": {
78
+ const packageJson = await readBunPackageJson(options.appPath, options.signal);
79
+ const buildCommand = await resolveFrameworkBuildCommand(options.appPath, packageJson, {
80
+ command: "vite build",
81
+ source: "TanStack Start default",
82
+ signal: options.signal
83
+ });
84
+ return {
85
+ buildCommand: buildCommand.command,
86
+ buildCommandSource: buildCommand.source,
87
+ outputDirectory: ".output",
88
+ outputDirectorySource: "TanStack Start output"
89
+ };
90
+ }
91
+ case "bun": {
92
+ const packageJson = await readBunPackageJson(options.appPath, options.signal);
93
+ const buildCommand = await resolveFrameworkBuildCommand(options.appPath, packageJson, {
94
+ command: null,
95
+ source: null,
96
+ signal: options.signal
97
+ });
98
+ return {
99
+ buildCommand: buildCommand.command,
100
+ buildCommandSource: buildCommand.source,
101
+ outputDirectory: ".",
102
+ outputDirectorySource: "app root"
103
+ };
104
+ }
105
+ }
106
+ }
107
+ async function readPreviewBuildSettingsConfig(configPath, signal) {
108
+ let content;
109
+ try {
110
+ content = await readFile(configPath, {
111
+ encoding: "utf8",
112
+ signal
113
+ });
114
+ } catch (error) {
115
+ if (error.code === "ENOENT") return null;
116
+ throw error;
117
+ }
118
+ let parsed;
119
+ try {
120
+ parsed = JSON.parse(content);
121
+ } catch (error) {
122
+ throw invalidPrismaAppConfigError(configPath, `The file is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
123
+ }
124
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw invalidPrismaAppConfigError(configPath, "The file must contain a JSON object.");
125
+ const raw = parsed;
126
+ if ("$schema" in raw && raw.$schema !== void 0 && typeof raw.$schema !== "string") throw invalidPrismaAppConfigError(configPath, "The $schema field must be a string when present.");
127
+ if (raw.buildCommand !== null && typeof raw.buildCommand !== "string") throw invalidPrismaAppConfigError(configPath, "The buildCommand field must be a string or null.");
128
+ let buildCommand = null;
129
+ if (typeof raw.buildCommand === "string") {
130
+ buildCommand = raw.buildCommand.trim();
131
+ if (buildCommand.length === 0) throw invalidPrismaAppConfigError(configPath, "The buildCommand field must not be an empty string. Use null to skip the build step.");
132
+ }
133
+ const outputDirectory = normalizeConfigOutputDirectory(configPath, raw.outputDirectory);
134
+ return {
135
+ buildCommand,
136
+ outputDirectory
137
+ };
138
+ }
139
+ function normalizeConfigOutputDirectory(configPath, value) {
140
+ if (typeof value !== "string" || value.trim().length === 0) throw invalidPrismaAppConfigError(configPath, "The outputDirectory field must be a non-empty string.");
141
+ const normalized = normalizeRelativePath(value);
142
+ if (!normalized) throw invalidPrismaAppConfigError(configPath, "The outputDirectory field must be a relative path inside the app directory.");
143
+ return normalized;
144
+ }
145
+ function invalidPrismaAppConfigError(configPath, why) {
146
+ return new CliError({
147
+ code: "APP_CONFIG_INVALID",
148
+ domain: "app",
149
+ summary: `Invalid ${PRISMA_APP_CONFIG_FILENAME}`,
150
+ why,
151
+ fix: `Edit ${PRISMA_APP_CONFIG_FILENAME} so buildCommand is a string or null and outputDirectory is a relative path inside the app root. Delete the file and rerun prisma-cli app deploy to regenerate defaults.`,
152
+ where: configPath,
153
+ meta: { configPath },
154
+ exitCode: 2,
155
+ nextSteps: ["prisma-cli app deploy"]
156
+ });
157
+ }
158
+ async function hasRootFile(appPath, filenames, signal) {
159
+ let entries;
160
+ try {
161
+ signal?.throwIfAborted();
162
+ entries = await readdir(appPath);
163
+ signal?.throwIfAborted();
164
+ } catch (error) {
165
+ if (signal?.aborted) throw error;
166
+ return false;
167
+ }
168
+ return entries.some((entry) => filenames.includes(entry));
169
+ }
170
+ function hasPackageDependency(packageJson, dependencyName) {
171
+ return hasAnyPackageDependency(packageJson, [dependencyName]);
172
+ }
173
+ function hasAnyPackageDependency(packageJson, dependencyNames) {
174
+ if (!packageJson) return false;
175
+ return [packageJson.dependencies, packageJson.devDependencies].some((group) => {
176
+ if (!group || typeof group !== "object") return false;
177
+ return dependencyNames.some((dependencyName) => dependencyName in group);
178
+ });
179
+ }
180
+ async function resolveFrameworkBuildCommand(appPath, packageJson, fallback) {
181
+ const buildScript = readBuildScript(packageJson);
182
+ if (buildScript) {
183
+ const packageManager = await resolvePackageManager(appPath, packageJson, fallback.signal);
184
+ if (!packageManager) return {
185
+ command: buildScript,
186
+ source: "package.json scripts.build"
187
+ };
188
+ return {
189
+ command: `${packageManager} run build`,
190
+ source: "package.json scripts.build"
191
+ };
192
+ }
193
+ return {
194
+ command: fallback.command,
195
+ source: fallback.source
196
+ };
197
+ }
198
+ function readBuildScript(packageJson) {
199
+ if (!packageJson?.scripts || typeof packageJson.scripts !== "object") return null;
200
+ const scripts = packageJson.scripts;
201
+ if (typeof scripts.build !== "string") return null;
202
+ const buildScript = scripts.build.trim();
203
+ return buildScript.length > 0 ? buildScript : null;
204
+ }
205
+ async function resolvePackageManager(appPath, packageJson, signal) {
206
+ const fromPackageManager = packageManagerFromPackageJson(packageJson?.packageManager);
207
+ if (fromPackageManager) return fromPackageManager;
208
+ if (await pathExists(path.join(appPath, "bun.lock"), signal) || await pathExists(path.join(appPath, "bun.lockb"), signal)) return "bun";
209
+ if (await pathExists(path.join(appPath, "pnpm-lock.yaml"), signal)) return "pnpm";
210
+ if (await pathExists(path.join(appPath, "yarn.lock"), signal)) return "yarn";
211
+ if (await pathExists(path.join(appPath, "package-lock.json"), signal)) return "npm";
212
+ }
213
+ function packageManagerFromPackageJson(value) {
214
+ if (typeof value !== "string") return null;
215
+ const name = value.split("@")[0];
216
+ return name === "bun" || name === "pnpm" || name === "yarn" || name === "npm" ? name : null;
217
+ }
218
+ async function runResolvedBuildCommand(appPath, settings, failurePrefix, signal) {
219
+ if (!settings.buildCommand) return;
220
+ await execBuildCommand(settings.buildCommand, appPath, failurePrefix, signal);
221
+ }
222
+ function execBuildCommand(command, cwd, failurePrefix, signal) {
223
+ return new Promise((resolve, reject) => {
224
+ const child = exec(command, {
225
+ cwd,
226
+ env: {
227
+ ...process.env,
228
+ PATH: [path.join(cwd, "node_modules", ".bin"), process.env.PATH].filter(Boolean).join(path.delimiter)
229
+ },
230
+ maxBuffer: 10 * 1024 * 1024,
231
+ signal
232
+ }, (error, stdout, stderr) => {
233
+ if (error) {
234
+ const output = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
235
+ reject(/* @__PURE__ */ new Error(`${failurePrefix} failed:\n${output || error.message}`));
236
+ return;
237
+ }
238
+ resolve();
239
+ });
240
+ if (signal?.aborted) child.kill();
241
+ });
242
+ }
243
+ async function resolveNextOutputRoot(appPath, signal) {
244
+ return (await readNextConfig(appPath, signal)).distDir ?? ".next";
245
+ }
246
+ async function readNextConfig(appPath, signal) {
247
+ for (const fileName of NEXT_CONFIG_FILENAMES) {
248
+ const filePath = path.join(appPath, fileName);
249
+ let content;
250
+ try {
251
+ content = await readFile(filePath, {
252
+ encoding: "utf8",
253
+ signal
254
+ });
255
+ } catch (error) {
256
+ if (error.code === "ENOENT") continue;
257
+ throw error;
258
+ }
259
+ return readStaticNextConfig(content);
260
+ }
261
+ return {};
262
+ }
263
+ const NEXT_CONFIG_FILENAMES = [
264
+ "next.config.js",
265
+ "next.config.mjs",
266
+ "next.config.ts",
267
+ "next.config.mts"
268
+ ];
269
+ function readStaticNextConfig(content) {
270
+ try {
271
+ const program = asAstNode(parseModule(content).$ast);
272
+ const bindings = program ? collectStaticBindings(program) : /* @__PURE__ */ new Map();
273
+ const configObject = program ? findExportedConfigObject(program, bindings) : null;
274
+ if (!configObject) return {};
275
+ const rawDistDir = readStaticStringProperty(configObject, "distDir");
276
+ const output = readStaticStringProperty(configObject, "output");
277
+ return {
278
+ distDir: rawDistDir ? normalizeRelativePath(rawDistDir) : void 0,
279
+ output: output === "standalone" || output === "export" ? output : void 0
280
+ };
281
+ } catch {
282
+ return {};
283
+ }
284
+ }
285
+ function joinPosix(...parts) {
286
+ return parts.join("/").replace(/\/+/g, "/");
287
+ }
288
+ function nextOutputRootFromStandaloneDirectory(outputDirectory) {
289
+ const normalized = outputDirectory.replace(/\/+$/g, "");
290
+ if (normalized === "standalone") return ".";
291
+ if (normalized.endsWith("/standalone")) {
292
+ const outputRoot = normalized.slice(0, -11);
293
+ return outputRoot.length > 0 ? outputRoot : ".";
294
+ }
295
+ const dirname = path.posix.dirname(normalized);
296
+ return dirname === "." ? "." : dirname;
297
+ }
298
+ function asAstNode(value) {
299
+ if (!value || typeof value !== "object") return null;
300
+ return typeof value.type === "string" ? value : null;
301
+ }
302
+ function astNodes(value) {
303
+ if (!Array.isArray(value)) return [];
304
+ return value.map(asAstNode).filter((node) => Boolean(node));
305
+ }
306
+ function collectStaticBindings(program) {
307
+ const bindings = /* @__PURE__ */ new Map();
308
+ for (const statement of astNodes(program.body)) {
309
+ if (statement.type !== "VariableDeclaration") continue;
310
+ for (const declaration of astNodes(statement.declarations)) {
311
+ const id = asAstNode(declaration.id);
312
+ const init = asAstNode(declaration.init);
313
+ if (id?.type === "Identifier" && typeof id.name === "string" && init) bindings.set(id.name, init);
314
+ }
315
+ }
316
+ return bindings;
317
+ }
318
+ function findExportedConfigObject(program, bindings) {
319
+ for (const statement of astNodes(program.body)) {
320
+ if (statement.type === "ExportDefaultDeclaration") return resolveConfigObject(statement.declaration, bindings);
321
+ if (statement.type !== "ExpressionStatement") continue;
322
+ const expression = asAstNode(statement.expression);
323
+ if (expression?.type !== "AssignmentExpression" || expression.operator !== "=") continue;
324
+ if (isModuleExports(expression.left)) return resolveConfigObject(expression.right, bindings);
325
+ }
326
+ return null;
327
+ }
328
+ function resolveConfigObject(value, bindings, depth = 0) {
329
+ if (depth > 4) return null;
330
+ const node = unwrapStaticExpression(asAstNode(value));
331
+ if (!node) return null;
332
+ if (node.type === "ObjectExpression") return node;
333
+ if (node.type === "Identifier" && typeof node.name === "string") return resolveConfigObject(bindings.get(node.name), bindings, depth + 1);
334
+ if (node.type === "CallExpression") return resolveConfigObject(astNodes(node.arguments)[0], bindings, depth + 1);
335
+ return null;
336
+ }
337
+ function unwrapStaticExpression(node) {
338
+ let current = node;
339
+ while (current?.type === "TSAsExpression" || current?.type === "TSSatisfiesExpression" || current?.type === "TSNonNullExpression") current = asAstNode(current.expression);
340
+ return current;
341
+ }
342
+ function isModuleExports(value) {
343
+ const node = asAstNode(value);
344
+ if (node?.type !== "MemberExpression" || node.computed === true) return false;
345
+ const object = asAstNode(node.object);
346
+ const property = asAstNode(node.property);
347
+ return object?.type === "Identifier" && object.name === "module" && property?.type === "Identifier" && property.name === "exports";
348
+ }
349
+ function readStaticStringProperty(objectExpression, propertyName) {
350
+ for (const property of astNodes(objectExpression.properties)) {
351
+ if (property.type !== "ObjectProperty" || property.computed === true) continue;
352
+ if (propertyKeyName(property.key) !== propertyName) continue;
353
+ const value = unwrapStaticExpression(asAstNode(property.value));
354
+ if (value?.type === "StringLiteral" && typeof value.value === "string") {
355
+ const trimmed = value.value.trim();
356
+ return trimmed.length > 0 ? trimmed : void 0;
357
+ }
358
+ }
359
+ }
360
+ function propertyKeyName(value) {
361
+ const key = asAstNode(value);
362
+ if (key?.type === "Identifier" && typeof key.name === "string") return key.name;
363
+ if (key?.type === "StringLiteral" && typeof key.value === "string") return key.value;
364
+ }
365
+ function normalizeRelativePath(value) {
366
+ const raw = value.trim().replace(/\\/g, "/");
367
+ if (raw.length === 0 || raw.split("/").includes("..")) return;
368
+ const normalized = path.posix.normalize(raw);
369
+ const segments = normalized.split("/");
370
+ if (path.win32.isAbsolute(value) || path.posix.isAbsolute(normalized) || segments.includes("..")) return;
371
+ return normalized === "." ? "." : normalized;
372
+ }
373
+ async function pathExists(targetPath, signal) {
374
+ try {
375
+ signal?.throwIfAborted();
376
+ await stat(targetPath);
377
+ signal?.throwIfAborted();
378
+ return true;
379
+ } catch (error) {
380
+ if (signal?.aborted) throw error;
381
+ return false;
382
+ }
383
+ }
384
+ //#endregion
385
+ export { PRISMA_APP_CONFIG_FILENAME, PRISMA_APP_CONFIG_SCHEMA_URL, hasAnyPackageDependency, hasPackageDependency, hasRootFile, joinPosix, nextOutputRootFromStandaloneDirectory, resolveOrCreatePreviewBuildSettings, resolvePreviewBuildSettings, runResolvedBuildCommand };
@@ -1,7 +1,9 @@
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 { PRISMA_APP_CONFIG_FILENAME, hasAnyPackageDependency, hasPackageDependency, hasRootFile, joinPosix, nextOutputRootFromStandaloneDirectory, resolvePreviewBuildSettings, runResolvedBuildCommand } from "./preview-build-settings.js";
3
+ import { chmod, copyFile, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm, stat } from "node:fs/promises";
3
4
  import path from "node:path";
4
- import { AstroBuild, BunBuild, NextjsBuild, NuxtBuild, TanstackStartBuild } from "@prisma/compute-sdk";
5
+ import os from "node:os";
6
+ import { AstroBuild, BunBuild, NuxtBuild } from "@prisma/compute-sdk";
5
7
  //#region src/lib/app/preview-build.ts
6
8
  const PREVIEW_BUILD_TYPES = [
7
9
  "auto",
@@ -17,18 +19,21 @@ var PreviewBuildStrategy = class {
17
19
  #entrypoint;
18
20
  #buildType;
19
21
  #signal;
22
+ #buildSettings;
20
23
  constructor(options) {
21
24
  this.#appPath = options.appPath;
22
25
  this.#entrypoint = options.entrypoint;
23
26
  this.#buildType = options.buildType ?? "auto";
24
27
  this.#signal = options.signal;
28
+ this.#buildSettings = options.buildSettings;
25
29
  }
26
30
  async canBuild(signal = this.#signal) {
27
31
  const { strategy } = await resolvePreviewBuildStrategy({
28
32
  appPath: this.#appPath,
29
33
  entrypoint: this.#entrypoint,
30
34
  buildType: this.#buildType,
31
- signal
35
+ signal,
36
+ buildSettings: this.#buildSettings
32
37
  });
33
38
  return strategy.canBuild(signal);
34
39
  }
@@ -37,7 +42,8 @@ var PreviewBuildStrategy = class {
37
42
  appPath: this.#appPath,
38
43
  entrypoint: this.#entrypoint,
39
44
  buildType: this.#buildType,
40
- signal
45
+ signal,
46
+ buildSettings: this.#buildSettings
41
47
  });
42
48
  return artifact;
43
49
  }
@@ -47,11 +53,11 @@ async function executePreviewBuild(options) {
47
53
  appPath: options.appPath,
48
54
  entrypoint: options.entrypoint,
49
55
  buildType: options.buildType ?? "auto",
50
- signal: options.signal
56
+ signal: options.signal,
57
+ buildSettings: options.buildSettings
51
58
  });
52
59
  const artifact = await strategy.execute(options.signal);
53
60
  try {
54
- if (buildType === "nextjs") await restageNextjsArtifact(artifact, options.appPath, options.signal);
55
61
  await normalizeArtifactSymlinks(artifact.directory, options.appPath, options.signal);
56
62
  return {
57
63
  artifact,
@@ -68,7 +74,8 @@ async function resolvePreviewBuildStrategy(options) {
68
74
  appPath: options.appPath,
69
75
  entrypoint: options.entrypoint,
70
76
  buildType: options.buildType,
71
- signal: options.signal
77
+ signal: options.signal,
78
+ buildSettings: options.buildSettings
72
79
  });
73
80
  return {
74
81
  buildType: options.buildType,
@@ -81,7 +88,8 @@ async function resolvePreviewBuildStrategy(options) {
81
88
  appPath: options.appPath,
82
89
  entrypoint: options.entrypoint,
83
90
  buildType,
84
- signal: options.signal
91
+ signal: options.signal,
92
+ buildSettings: options.buildSettings
85
93
  });
86
94
  if (await strategy.canBuild(options.signal)) return {
87
95
  buildType,
@@ -94,25 +102,170 @@ async function resolvePreviewBuildStrategy(options) {
94
102
  appPath: options.appPath,
95
103
  entrypoint: options.entrypoint,
96
104
  buildType: "bun",
97
- signal: options.signal
105
+ signal: options.signal,
106
+ buildSettings: options.buildSettings
98
107
  })
99
108
  };
100
109
  }
101
110
  async function createPreviewBuildStrategy(options) {
102
111
  switch (options.buildType) {
103
- case "nextjs": return new NextjsBuild({ appPath: options.appPath });
112
+ case "nextjs": return new PreviewNextjsBuild({
113
+ appPath: options.appPath,
114
+ buildSettings: options.buildSettings
115
+ });
104
116
  case "nuxt": return new NuxtBuild({ appPath: options.appPath });
105
117
  case "astro": return new AstroBuild({ appPath: options.appPath });
106
- case "tanstack-start": return new TanstackStartBuild({ appPath: options.appPath });
118
+ case "tanstack-start": return new PreviewTanstackStartBuild({
119
+ appPath: options.appPath,
120
+ buildSettings: options.buildSettings
121
+ });
107
122
  case "bun": {
108
123
  const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint, options.signal);
109
- return new BunBuild({
124
+ return new PreviewBunBuild({
110
125
  appPath: options.appPath,
111
- entrypoint
126
+ strategy: new BunBuild({
127
+ appPath: options.appPath,
128
+ entrypoint
129
+ }),
130
+ buildSettings: options.buildSettings
112
131
  });
113
132
  }
114
133
  }
115
134
  }
135
+ var PreviewNextjsBuild = class {
136
+ #appPath;
137
+ #buildSettings;
138
+ constructor(options) {
139
+ this.#appPath = options.appPath;
140
+ this.#buildSettings = options.buildSettings;
141
+ }
142
+ async canBuild(signal) {
143
+ const packageJson = await readBunPackageJson(this.#appPath, signal);
144
+ return await hasRootFile(this.#appPath, NEXT_CONFIG_FILENAMES, signal) || hasPackageDependency(packageJson, "next");
145
+ }
146
+ async execute(signal) {
147
+ const settings = this.#buildSettings ?? await resolvePreviewBuildSettings({
148
+ appPath: this.#appPath,
149
+ buildType: "nextjs",
150
+ signal
151
+ });
152
+ await runResolvedBuildCommand(this.#appPath, settings, "Next.js", signal);
153
+ const standaloneDir = path.join(this.#appPath, settings.outputDirectory);
154
+ if (!await directoryExists(standaloneDir, signal)) throw new Error(`Next.js build did not produce standalone output at ${settings.outputDirectory}. Add output: "standalone" to your next.config file, or update ${PRISMA_APP_CONFIG_FILENAME}.`);
155
+ const outDir = await unsupportedFilesystemBoundary(signal, () => mkdtemp(path.join(os.tmpdir(), "compute-build-")));
156
+ try {
157
+ const artifactDir = path.join(outDir, "app");
158
+ await stageNextjsStandaloneArtifact({
159
+ standaloneDir,
160
+ artifactDir,
161
+ appPath: this.#appPath,
162
+ signal
163
+ });
164
+ const entrypoint = await findNextStandaloneEntrypoint(artifactDir, signal);
165
+ await copyNextjsStaticAssets({
166
+ appPath: this.#appPath,
167
+ artifactDir,
168
+ outputRoot: nextOutputRootFromStandaloneDirectory(settings.outputDirectory),
169
+ entrypoint,
170
+ signal
171
+ });
172
+ return {
173
+ directory: artifactDir,
174
+ entrypoint,
175
+ defaultPortMapping: { http: 3e3 },
176
+ cleanup: () => rm(outDir, {
177
+ recursive: true,
178
+ force: true
179
+ })
180
+ };
181
+ } catch (error) {
182
+ await rm(outDir, {
183
+ recursive: true,
184
+ force: true
185
+ });
186
+ throw error;
187
+ }
188
+ }
189
+ };
190
+ var PreviewTanstackStartBuild = class {
191
+ #appPath;
192
+ #buildSettings;
193
+ constructor(options) {
194
+ this.#appPath = options.appPath;
195
+ this.#buildSettings = options.buildSettings;
196
+ }
197
+ async canBuild(signal) {
198
+ return hasAnyPackageDependency(await readBunPackageJson(this.#appPath, signal), TANSTACK_START_PACKAGES);
199
+ }
200
+ async execute(signal) {
201
+ const settings = this.#buildSettings ?? await resolvePreviewBuildSettings({
202
+ appPath: this.#appPath,
203
+ buildType: "tanstack-start",
204
+ signal
205
+ });
206
+ await runResolvedBuildCommand(this.#appPath, settings, "TanStack Start", signal);
207
+ const outputDir = path.join(this.#appPath, settings.outputDirectory);
208
+ const entrypoint = "server/index.mjs";
209
+ const entryPath = path.join(outputDir, entrypoint);
210
+ 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 update ${PRISMA_APP_CONFIG_FILENAME}.`);
211
+ const outDir = await unsupportedFilesystemBoundary(signal, () => mkdtemp(path.join(os.tmpdir(), "compute-build-")));
212
+ try {
213
+ const artifactDir = path.join(outDir, "app");
214
+ await unsupportedFilesystemBoundary(signal, () => cp(outputDir, artifactDir, {
215
+ recursive: true,
216
+ verbatimSymlinks: true
217
+ }));
218
+ return {
219
+ directory: artifactDir,
220
+ entrypoint,
221
+ defaultPortMapping: { http: 3e3 },
222
+ cleanup: () => rm(outDir, {
223
+ recursive: true,
224
+ force: true
225
+ })
226
+ };
227
+ } catch (error) {
228
+ await rm(outDir, {
229
+ recursive: true,
230
+ force: true
231
+ });
232
+ throw error;
233
+ }
234
+ }
235
+ };
236
+ var PreviewBunBuild = class {
237
+ #appPath;
238
+ #strategy;
239
+ #buildSettings;
240
+ constructor(options) {
241
+ this.#appPath = options.appPath;
242
+ this.#strategy = options.strategy;
243
+ this.#buildSettings = options.buildSettings;
244
+ }
245
+ async canBuild(signal) {
246
+ return this.#strategy.canBuild(signal);
247
+ }
248
+ async execute(signal) {
249
+ const settings = this.#buildSettings ?? await resolvePreviewBuildSettings({
250
+ appPath: this.#appPath,
251
+ buildType: "bun",
252
+ signal
253
+ });
254
+ await runResolvedBuildCommand(this.#appPath, settings, "Bun", signal);
255
+ return this.#strategy.execute(signal);
256
+ }
257
+ };
258
+ const NEXT_CONFIG_FILENAMES = [
259
+ "next.config.js",
260
+ "next.config.mjs",
261
+ "next.config.ts",
262
+ "next.config.mts"
263
+ ];
264
+ const TANSTACK_START_PACKAGES = [
265
+ "@tanstack/react-start",
266
+ "@tanstack/solid-start",
267
+ "@tanstack/start"
268
+ ];
116
269
  async function stageNextjsStandaloneArtifact(options) {
117
270
  const standaloneRoot = path.resolve(options.standaloneDir);
118
271
  const artifactRoot = path.resolve(options.artifactDir);
@@ -125,32 +278,42 @@ async function stageNextjsStandaloneArtifact(options) {
125
278
  });
126
279
  await hoistPnpmDependencies(path.join(artifactRoot, "node_modules"), options.signal);
127
280
  }
128
- async function restageNextjsArtifact(artifact, appPath, signal) {
129
- const artifactDir = artifact.directory;
130
- const standaloneDir = path.join(appPath, ".next", "standalone");
131
- await unsupportedFilesystemBoundary(signal, () => rm(artifactDir, {
132
- recursive: true,
133
- force: true
134
- }));
135
- await stageNextjsStandaloneArtifact({
136
- standaloneDir,
137
- artifactDir,
138
- appPath,
139
- signal
140
- });
141
- const serverSubpath = nextjsServerSubpath(artifact.entrypoint);
142
- const serverDir = serverSubpath ? path.join(artifactDir, serverSubpath) : artifactDir;
143
- const publicDir = path.join(appPath, "public");
144
- if (await directoryExists(publicDir, signal)) await unsupportedFilesystemBoundary(signal, () => cp(publicDir, path.join(serverDir, "public"), {
281
+ async function copyNextjsStaticAssets(options) {
282
+ const serverSubpath = nextjsServerSubpath(options.entrypoint);
283
+ const serverDir = serverSubpath ? path.join(options.artifactDir, serverSubpath) : options.artifactDir;
284
+ const publicDir = path.join(options.appPath, "public");
285
+ if (await directoryExists(publicDir, options.signal)) await unsupportedFilesystemBoundary(options.signal, () => cp(publicDir, path.join(serverDir, "public"), {
145
286
  recursive: true,
146
287
  verbatimSymlinks: true
147
288
  }));
148
- const staticDir = path.join(appPath, ".next", "static");
149
- if (await directoryExists(staticDir, signal)) await unsupportedFilesystemBoundary(signal, () => cp(staticDir, path.join(serverDir, ".next", "static"), {
289
+ const staticDir = path.join(options.appPath, options.outputRoot, "static");
290
+ if (await directoryExists(staticDir, options.signal)) await unsupportedFilesystemBoundary(options.signal, () => cp(staticDir, path.join(serverDir, options.outputRoot, "static"), {
150
291
  recursive: true,
151
292
  verbatimSymlinks: true
152
293
  }));
153
294
  }
295
+ async function findNextStandaloneEntrypoint(artifactDir, signal) {
296
+ const rootEntrypoint = path.join(artifactDir, "server.js");
297
+ if ((await unsupportedFilesystemBoundary(signal, () => stat(rootEntrypoint).catch(() => null)))?.isFile()) return "server.js";
298
+ const candidates = [];
299
+ await walk(artifactDir);
300
+ candidates.sort((left, right) => left.split("/").length - right.split("/").length || left.localeCompare(right));
301
+ const selected = candidates[0];
302
+ if (!selected) throw new Error(`Next.js standalone output did not contain server.js in ${artifactDir}`);
303
+ return selected;
304
+ async function walk(directory) {
305
+ const entries = await unsupportedFilesystemBoundary(signal, () => readdir(directory, { withFileTypes: true }));
306
+ for (const entry of entries) {
307
+ if (entry.name === "node_modules") continue;
308
+ const fullPath = path.join(directory, entry.name);
309
+ if (entry.isDirectory()) {
310
+ await walk(fullPath);
311
+ continue;
312
+ }
313
+ if (entry.isFile() && entry.name === "server.js") candidates.push(path.relative(artifactDir, fullPath).split(path.sep).join("/"));
314
+ }
315
+ }
316
+ }
154
317
  function nextjsServerSubpath(entrypoint) {
155
318
  const dir = path.posix.dirname(entrypoint);
156
319
  return dir === "." ? "" : dir;
@@ -165,7 +165,8 @@ function createPreviewAppProvider(client, options) {
165
165
  appPath: path.resolve(options.cwd),
166
166
  entrypoint: options.entrypoint,
167
167
  buildType: options.buildType,
168
- signal: options.signal
168
+ signal: options.signal,
169
+ buildSettings: options.buildSettings
169
170
  }),
170
171
  projectId: options.projectId,
171
172
  serviceId: resolvedApp.appId,
@@ -3,22 +3,22 @@ import { canPrompt } from "../../shell/runtime.js";
3
3
  import { confirmPrompt } from "../../shell/prompt.js";
4
4
  //#region src/lib/app/production-deploy-gate.ts
5
5
  async function enforceProductionDeployGate(context, provider, options) {
6
- if (options.branchKind !== "production") return;
6
+ if (options.branchKind !== "production") return { firstProductionDeploy: false };
7
7
  if (!options.appId) {
8
8
  renderFirstProductionDeployLine(context, options.appName);
9
- return;
9
+ return { firstProductionDeploy: true };
10
10
  }
11
11
  const currentLiveDeployment = resolveCurrentProductionDeployment(await provider.listDeployments(options.appId).catch((error) => {
12
12
  throw productionDeployInspectionFailedError(error);
13
13
  }));
14
14
  if (!currentLiveDeployment) {
15
15
  renderFirstProductionDeployLine(context, options.appName);
16
- return;
16
+ return { firstProductionDeploy: true };
17
17
  }
18
18
  if (!options.prod) throw productionDeployRequiresFlagError();
19
19
  if (context.flags.yes) {
20
20
  renderProductionDeployYesLine(context);
21
- return;
21
+ return { firstProductionDeploy: false };
22
22
  }
23
23
  if (!canPrompt(context)) throw productionDeployConfirmationRequiredError(options.appName);
24
24
  renderProductionDeployConfirmation(context, currentLiveDeployment);
@@ -28,6 +28,7 @@ async function enforceProductionDeployGate(context, provider, options) {
28
28
  message: "Deploy to production?",
29
29
  initialValue: false
30
30
  })) throw productionDeployCancelledError();
31
+ return { firstProductionDeploy: false };
31
32
  }
32
33
  function resolveCurrentProductionDeployment(result) {
33
34
  if (result.deployments.length === 0) return null;
@@ -43,11 +43,16 @@ function renderAppDeploy(context, descriptor, result) {
43
43
  ];
44
44
  }
45
45
  function serializeAppDeploy(result) {
46
- const { deploySettings: _deploySettings, localPin: _localPin, ...serialized } = result;
46
+ const { deploySettings, localPin: _localPin, ...serialized } = result;
47
47
  const { id: _branchId, ...branch } = serialized.branch;
48
48
  return {
49
49
  ...serialized,
50
- branch
50
+ branch,
51
+ deploySettings: {
52
+ config: deploySettings.config,
53
+ buildCommand: deploySettings.buildCommand,
54
+ outputDirectory: deploySettings.outputDirectory
55
+ }
51
56
  };
52
57
  }
53
58
  function renderBranchDatabaseDeploySummary(context, result) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/cli",
3
- "version": "3.0.0-dev.69.1",
3
+ "version": "3.0.0-dev.71.1",
4
4
  "description": "Command-line interface for the Prisma Developer Platform.",
5
5
  "type": "module",
6
6
  "bin": {