@prisma/cli 3.0.0-dev.86.1 → 3.0.0-dev.88.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.
- package/dist/commands/app/index.js +64 -51
- package/dist/controllers/app.js +561 -276
- package/dist/lib/app/branch-database-deploy.js +45 -104
- package/dist/lib/app/branch-database.js +22 -184
- package/dist/lib/app/compute-config.js +147 -0
- package/dist/lib/app/deploy-plan.js +58 -0
- package/dist/lib/app/local-dev.js +3 -60
- package/dist/lib/app/preview-build-settings.js +95 -104
- package/dist/lib/app/preview-build.js +35 -56
- package/dist/lib/app/production-deploy-gate.js +2 -2
- package/dist/lib/diagnostics.js +1 -1
- package/dist/lib/fs/home-path.js +24 -0
- package/dist/lib/git/local-branch.js +15 -3
- package/dist/lib/project/resolution.js +1 -1
- package/dist/lib/project/setup.js +10 -8
- package/dist/presenters/app.js +45 -40
- package/dist/presenters/project.js +2 -8
- package/dist/shell/diagnostics-output.js +2 -8
- package/dist/shell/runtime.js +7 -3
- package/package.json +5 -3
|
@@ -4,27 +4,11 @@ import { renderSummaryLine } from "../../shell/ui.js";
|
|
|
4
4
|
import { canPrompt } from "../../shell/runtime.js";
|
|
5
5
|
import { formatCommandArgument } from "../../shell/command-arguments.js";
|
|
6
6
|
import "../project/setup.js";
|
|
7
|
-
import { hasBranchDatabaseSignal, inspectBranchDatabaseSignal
|
|
7
|
+
import { hasBranchDatabaseSignal, inspectBranchDatabaseSignal } from "./branch-database.js";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
//#region src/lib/app/branch-database-deploy.ts
|
|
10
10
|
async function maybeSetupBranchDatabase(context, provider, projectId, branch, options) {
|
|
11
11
|
if (options.db === false) return emptyBranchDatabaseSetupOutcome();
|
|
12
|
-
const preflight = branchDatabasePreflight(branch, options);
|
|
13
|
-
if (preflight) return preflight;
|
|
14
|
-
const envState = await inspectBranchDatabaseEnv(provider, projectId, branch, context.runtime.signal);
|
|
15
|
-
const existingEnvOutcome = existingBranchDatabaseEnvOutcome(context, branch, getTargetDatabaseEnvVarKeys(envState), envState, options.db);
|
|
16
|
-
if (existingEnvOutcome) return existingEnvOutcome;
|
|
17
|
-
const localSignal = await inspectBranchDatabaseSignal(context.runtime.cwd, context.runtime.signal);
|
|
18
|
-
if (localSignal.unsupportedSchema) {
|
|
19
|
-
if (options.db === true) throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch, context);
|
|
20
|
-
return emptyBranchDatabaseSetupOutcome();
|
|
21
|
-
}
|
|
22
|
-
const promptOutcome = await branchDatabasePromptOutcome(context, branch, localSignal, envState, options.db);
|
|
23
|
-
if (promptOutcome) return promptOutcome;
|
|
24
|
-
if (options.db === true && !canPrompt(context) && !context.flags.yes) throw nonInteractiveDatabaseSetupRequiresYesError(branch);
|
|
25
|
-
return setupBranchDatabase(context, provider, projectId, branch, localSignal, envState);
|
|
26
|
-
}
|
|
27
|
-
function branchDatabasePreflight(branch, options) {
|
|
28
12
|
if (hasProvidedDatabaseEnvVars(options.providedEnvVars)) {
|
|
29
13
|
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");
|
|
30
14
|
return emptyBranchDatabaseSetupOutcome();
|
|
@@ -33,42 +17,47 @@ function branchDatabasePreflight(branch, options) {
|
|
|
33
17
|
if (options.db === true) throw productionDatabaseSetupAfterFirstDeployError();
|
|
34
18
|
return emptyBranchDatabaseSetupOutcome();
|
|
35
19
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (warning) emitBranchDatabaseWarning(context, warning);
|
|
42
|
-
return {
|
|
43
|
-
result: requested === true ? {
|
|
44
|
-
status: "skipped",
|
|
45
|
-
reason: existingDatabaseEnvReason(branch),
|
|
46
|
-
envVars: targetEnvVars,
|
|
47
|
-
schema: null
|
|
48
|
-
} : void 0,
|
|
49
|
-
warnings: warning ? [warning] : []
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
async function branchDatabasePromptOutcome(context, branch, localSignal, envState, requested) {
|
|
53
|
-
if (requested === true) return null;
|
|
54
|
-
if (!(hasBranchDatabaseSignal(localSignal) || Boolean(envState.inheritedPreviewDatabaseUrl))) return emptyBranchDatabaseSetupOutcome();
|
|
55
|
-
if (!canPrompt(context) || context.flags.yes) {
|
|
56
|
-
const warning = databasePromptSuppressedWarning(branch);
|
|
57
|
-
emitBranchDatabaseWarning(context, warning);
|
|
20
|
+
const envState = await inspectBranchDatabaseEnv(provider, projectId, branch, context.runtime.signal);
|
|
21
|
+
const targetEnvVars = getTargetDatabaseEnvVarKeys(envState);
|
|
22
|
+
if (hasExistingDatabaseEnvForTarget(branch, envState)) {
|
|
23
|
+
const warning = options.db === true ? existingDatabaseEnvWarning(branch, targetEnvVars) : null;
|
|
24
|
+
if (warning) emitBranchDatabaseWarning(context, warning);
|
|
58
25
|
return {
|
|
59
|
-
result:
|
|
60
|
-
|
|
26
|
+
result: options.db === true ? {
|
|
27
|
+
status: "skipped",
|
|
28
|
+
reason: existingDatabaseEnvReason(branch),
|
|
29
|
+
envVars: targetEnvVars
|
|
30
|
+
} : void 0,
|
|
31
|
+
warnings: warning ? [warning] : []
|
|
61
32
|
};
|
|
62
33
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
34
|
+
const localSignal = await inspectBranchDatabaseSignal(options.projectDir, context.runtime.signal);
|
|
35
|
+
if (localSignal.unsupportedSchema) {
|
|
36
|
+
if (options.db === true) throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch, context);
|
|
37
|
+
return emptyBranchDatabaseSetupOutcome();
|
|
38
|
+
}
|
|
39
|
+
const hasSignal = hasBranchDatabaseSignal(localSignal) || Boolean(envState.inheritedPreviewDatabaseUrl);
|
|
40
|
+
if (options.db !== true) {
|
|
41
|
+
if (!hasSignal) return emptyBranchDatabaseSetupOutcome();
|
|
42
|
+
if (!canPrompt(context) || context.flags.yes) {
|
|
43
|
+
const warning = databasePromptSuppressedWarning(branch);
|
|
44
|
+
emitBranchDatabaseWarning(context, warning);
|
|
45
|
+
return {
|
|
46
|
+
result: void 0,
|
|
47
|
+
warnings: [warning]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
maybeRenderBranchDatabaseSignal(context, branch, localSignal, envState);
|
|
51
|
+
if (!await confirmPrompt({
|
|
52
|
+
input: context.runtime.stdin,
|
|
53
|
+
output: context.output.stderr,
|
|
54
|
+
message: databasePromptMessage(branch),
|
|
55
|
+
initialValue: false
|
|
56
|
+
})) return emptyBranchDatabaseSetupOutcome();
|
|
57
|
+
} else if (!canPrompt(context) && !context.flags.yes) throw nonInteractiveDatabaseSetupRequiresYesError(branch);
|
|
58
|
+
return setupBranchDatabase(context, provider, projectId, branch, localSignal, envState, options.projectDir);
|
|
59
|
+
}
|
|
60
|
+
async function setupBranchDatabase(context, provider, projectId, branch, signal, envState, projectDir) {
|
|
72
61
|
emitBranchDatabaseProgress(context, "pending", "Creating database");
|
|
73
62
|
const database = await provider.createBranchDatabase({
|
|
74
63
|
projectId,
|
|
@@ -80,28 +69,11 @@ async function setupBranchDatabase(context, provider, projectId, branch, signal,
|
|
|
80
69
|
});
|
|
81
70
|
emitBranchDatabaseProgress(context, "success", "Created database");
|
|
82
71
|
try {
|
|
83
|
-
let schemaSetup = null;
|
|
84
|
-
const warnings = [];
|
|
85
|
-
let skippedSchemaWarning = null;
|
|
86
|
-
const schema = signal.schema;
|
|
87
|
-
if (schema) {
|
|
88
|
-
emitBranchDatabaseProgress(context, "pending", `Applying database schema with ${formatSchemaSetupCommand(schema.command)}`);
|
|
89
|
-
schemaSetup = await runBranchDatabaseSchemaSetup({
|
|
90
|
-
context,
|
|
91
|
-
schema,
|
|
92
|
-
databaseUrl: database.databaseUrl,
|
|
93
|
-
directUrl: database.directUrl
|
|
94
|
-
}).catch((error) => {
|
|
95
|
-
throw schemaSetupFailedError(error, schema, branch, context.runtime.cwd);
|
|
96
|
-
});
|
|
97
|
-
emitBranchDatabaseProgress(context, "success", "Applied database schema");
|
|
98
|
-
} else skippedSchemaWarning = "No supported Prisma schema source was found. Database env vars were created, but schema setup was skipped.";
|
|
99
72
|
const envVars = await upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState);
|
|
100
|
-
emitBranchDatabaseProgress(context, "success", `Added ${envScopeLabel(branch)} env var${envVars.length === 1 ? "" : "s"} ${envVars.join(", ")}`);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
73
|
+
emitBranchDatabaseProgress(context, "success", `Added ${envScopeLabel(branch)} env var${envVars.length === 1 ? "" : "s"} ${envVars.join(", ")} — shared by every app on ${envScopeLabel(branch) === "production" ? "production" : `branch "${branch.name}"`}`);
|
|
74
|
+
const schemaCommand = signal.schema ? `DATABASE_URL=<url> npx ${formatSchemaSetupCommand(signal.schema.command)} (detected ${path.relative(projectDir, signal.schema.path) || signal.schema.path})` : "your own migration tooling";
|
|
75
|
+
const schemaSuggestion = `The new database is empty. Get a connection URL with \`prisma-cli database connection create ${database.id}\`, then apply your schema with ${schemaCommand}.`;
|
|
76
|
+
emitBranchDatabaseWarning(context, schemaSuggestion);
|
|
105
77
|
return {
|
|
106
78
|
result: {
|
|
107
79
|
status: "created",
|
|
@@ -109,14 +81,9 @@ async function setupBranchDatabase(context, provider, projectId, branch, signal,
|
|
|
109
81
|
id: database.id,
|
|
110
82
|
name: database.name
|
|
111
83
|
},
|
|
112
|
-
envVars
|
|
113
|
-
schema: schemaSetup ? {
|
|
114
|
-
command: schemaSetup.command,
|
|
115
|
-
source: schemaSetup.source,
|
|
116
|
-
path: schemaSetup.schemaPath
|
|
117
|
-
} : null
|
|
84
|
+
envVars
|
|
118
85
|
},
|
|
119
|
-
warnings
|
|
86
|
+
warnings: [schemaSuggestion]
|
|
120
87
|
};
|
|
121
88
|
} catch (error) {
|
|
122
89
|
throw await cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch, error);
|
|
@@ -318,24 +285,6 @@ function branchDatabaseCleanupFailedError(setupError, cleanupError, database, br
|
|
|
318
285
|
nextSteps: []
|
|
319
286
|
});
|
|
320
287
|
}
|
|
321
|
-
function schemaSetupFailedError(error, schema, branch, cwd) {
|
|
322
|
-
return new CliError({
|
|
323
|
-
code: "SCHEMA_SETUP_FAILED",
|
|
324
|
-
domain: "app",
|
|
325
|
-
summary: "Database schema setup failed",
|
|
326
|
-
why: error instanceof Error ? error.message : String(error),
|
|
327
|
-
fix: "Fix the Prisma schema or migrations, then rerun deploy with --db.",
|
|
328
|
-
debug: formatDebugDetails(error),
|
|
329
|
-
meta: {
|
|
330
|
-
branch: branch.name,
|
|
331
|
-
schemaPath: schema.path,
|
|
332
|
-
source: schema.kind,
|
|
333
|
-
command: schema.command
|
|
334
|
-
},
|
|
335
|
-
exitCode: 1,
|
|
336
|
-
nextSteps: [...formatSchemaSetupNextSteps(schema, cwd), formatAppDeployWithDbNextStep(branch)]
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
288
|
function unsupportedBranchDatabaseSchemaError(schema, branch, context) {
|
|
340
289
|
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");
|
|
341
290
|
}
|
|
@@ -348,14 +297,6 @@ function formatProjectEnvListNextStep(branch) {
|
|
|
348
297
|
function formatProjectEnvAddNextStep(branch) {
|
|
349
298
|
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)}`;
|
|
350
299
|
}
|
|
351
|
-
function formatSchemaSetupNextSteps(schema, cwd) {
|
|
352
|
-
const sourcePath = path.relative(cwd, schema.path) || defaultSchemaSourcePath(schema);
|
|
353
|
-
switch (schema.command) {
|
|
354
|
-
case "migrate-deploy": return [`npx --no-install prisma migrate deploy --schema ${formatCommandArgument(sourcePath)}`];
|
|
355
|
-
case "db-push": return [`npx --no-install prisma db push --schema ${formatCommandArgument(sourcePath)}`];
|
|
356
|
-
case "prisma-next-db-init": return [`npx --no-install prisma-next contract emit --config ${formatCommandArgument(sourcePath)}`, `npx --no-install prisma-next db init --config ${formatCommandArgument(sourcePath)} --db <DATABASE_URL>`];
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
300
|
function defaultSchemaSourcePath(schema) {
|
|
360
301
|
return schema.kind === "prisma-next" ? "prisma-next.config.ts" : "schema.prisma";
|
|
361
302
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { access, readFile, readdir, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
3
|
//#region src/lib/app/branch-database.ts
|
|
5
4
|
const SKIPPED_DIRECTORIES = new Set([
|
|
6
5
|
".git",
|
|
@@ -67,61 +66,31 @@ function hasBranchDatabaseSignal(signal) {
|
|
|
67
66
|
if (signal.unsupportedSchema) return false;
|
|
68
67
|
return Boolean(signal.schema || signal.databaseUrlReferences.length > 0);
|
|
69
68
|
}
|
|
70
|
-
async function runBranchDatabaseSchemaSetup(options) {
|
|
71
|
-
const schemaPath = path.relative(options.context.runtime.cwd, options.schema.path) || defaultSchemaSourcePath(options.schema);
|
|
72
|
-
const prisma = await resolvePrismaInvocation(options.context.runtime.cwd);
|
|
73
|
-
const commands = buildSchemaSetupCommands(options.schema, schemaPath, options.databaseUrl, prisma);
|
|
74
|
-
for (const command of commands) await runPrismaCommand({
|
|
75
|
-
context: options.context,
|
|
76
|
-
...command,
|
|
77
|
-
env: {
|
|
78
|
-
DATABASE_URL: options.databaseUrl,
|
|
79
|
-
...options.directUrl ? { DIRECT_URL: options.directUrl } : {}
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
return {
|
|
83
|
-
command: options.schema.command,
|
|
84
|
-
source: options.schema.kind,
|
|
85
|
-
schemaPath
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
69
|
async function scanDirectory(cwd, directory, depth, state, signal) {
|
|
89
70
|
signal.throwIfAborted();
|
|
90
71
|
if (depth > MAX_SCAN_DEPTH || state.filesVisited >= MAX_SCAN_FILES) return;
|
|
91
|
-
|
|
92
|
-
if (!entries) return;
|
|
93
|
-
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
94
|
-
for (const entry of entries) {
|
|
95
|
-
signal.throwIfAborted();
|
|
96
|
-
if (state.filesVisited >= MAX_SCAN_FILES) return;
|
|
97
|
-
await scanDirectoryEntry(cwd, directory, entry, depth, state, signal);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
async function readDirectoryEntries(directory) {
|
|
72
|
+
let entries;
|
|
101
73
|
try {
|
|
102
|
-
|
|
74
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
103
75
|
} catch (error) {
|
|
104
|
-
if (error.code === "ENOENT") return
|
|
76
|
+
if (error.code === "ENOENT") return;
|
|
105
77
|
throw error;
|
|
106
78
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
79
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
signal.throwIfAborted();
|
|
82
|
+
if (state.filesVisited >= MAX_SCAN_FILES) return;
|
|
83
|
+
const entryPath = path.join(directory, entry.name);
|
|
84
|
+
if (entry.isDirectory()) {
|
|
85
|
+
if (!SKIPPED_DIRECTORIES.has(entry.name)) await scanDirectory(cwd, entryPath, depth + 1, state, signal);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!entry.isFile()) continue;
|
|
89
|
+
state.filesVisited += 1;
|
|
90
|
+
if (entry.name === "schema.prisma") state.schemaCandidates.push(entryPath);
|
|
91
|
+
if (isPrismaNextConfigFile(entry.name)) state.prismaNextConfigCandidates.push(entryPath);
|
|
92
|
+
if (state.databaseUrlReferences.length < MAX_DATABASE_URL_REFERENCE_FILES && shouldScanForDatabaseUrl(entry.name) && await fileContainsDatabaseUrl(entryPath, signal)) state.databaseUrlReferences.push(path.relative(cwd, entryPath) || entry.name);
|
|
113
93
|
}
|
|
114
|
-
if (!entry.isFile()) return;
|
|
115
|
-
state.filesVisited += 1;
|
|
116
|
-
collectBranchDatabaseCandidate(entryPath, entry.name, state);
|
|
117
|
-
if (await shouldRecordDatabaseUrlReference(entryPath, entry.name, state, signal)) state.databaseUrlReferences.push(path.relative(cwd, entryPath) || entry.name);
|
|
118
|
-
}
|
|
119
|
-
function collectBranchDatabaseCandidate(entryPath, entryName, state) {
|
|
120
|
-
if (entryName === "schema.prisma") state.schemaCandidates.push(entryPath);
|
|
121
|
-
if (isPrismaNextConfigFile(entryName)) state.prismaNextConfigCandidates.push(entryPath);
|
|
122
|
-
}
|
|
123
|
-
async function shouldRecordDatabaseUrlReference(entryPath, entryName, state, signal) {
|
|
124
|
-
return state.databaseUrlReferences.length < MAX_DATABASE_URL_REFERENCE_FILES && shouldScanForDatabaseUrl(entryName) && await fileContainsDatabaseUrl(entryPath, signal);
|
|
125
94
|
}
|
|
126
95
|
async function selectPrismaOrmSchema(cwd, candidates, signal) {
|
|
127
96
|
const sorted = sortByPreferredRelativePath(cwd, candidates, "schema.prisma");
|
|
@@ -155,15 +124,12 @@ async function selectPrismaOrmSchema(cwd, candidates, signal) {
|
|
|
155
124
|
};
|
|
156
125
|
}
|
|
157
126
|
function selectPrismaNextConfig(cwd, candidates, mode) {
|
|
158
|
-
const matches = candidates.filter(
|
|
127
|
+
const matches = candidates.filter((candidate) => {
|
|
128
|
+
const isSupported = candidate.target === "postgresql" || candidate.target === "unknown";
|
|
129
|
+
return mode === "supported" ? isSupported : !isSupported;
|
|
130
|
+
});
|
|
159
131
|
return sortByPreferredRelativePath(cwd, matches.map((candidate) => candidate.path), "prisma-next.config.ts").map((candidatePath) => matches.find((candidate) => candidate.path === candidatePath)).find((candidate) => Boolean(candidate)) ?? null;
|
|
160
132
|
}
|
|
161
|
-
function isSupportedPrismaNextConfig(candidate) {
|
|
162
|
-
return candidate.target === "postgresql" || candidate.target === "unknown";
|
|
163
|
-
}
|
|
164
|
-
function isUnsupportedPrismaNextConfig(candidate) {
|
|
165
|
-
return !isSupportedPrismaNextConfig(candidate);
|
|
166
|
-
}
|
|
167
133
|
function sortByPreferredRelativePath(cwd, candidates, preferredRootFile) {
|
|
168
134
|
return candidates.map((candidate) => ({
|
|
169
135
|
absolute: candidate,
|
|
@@ -245,133 +211,5 @@ async function readTextFileIfSmall(filePath, signal) {
|
|
|
245
211
|
signal
|
|
246
212
|
});
|
|
247
213
|
}
|
|
248
|
-
const FALLBACK_PRISMA_CLI_VERSION = "6.19.3";
|
|
249
|
-
/**
|
|
250
|
-
* Picks how `prisma` CLI commands are invoked for schema setup. Projects
|
|
251
|
-
* with the CLI installed run their own binary (version-exact). Projects
|
|
252
|
-
* without it fall back to a versioned `npx prisma@<x>` pinned to the
|
|
253
|
-
* installed `@prisma/client` — never bare `npx prisma`, which resolves to
|
|
254
|
-
* latest and can be a major version ahead of the project's schema.
|
|
255
|
-
*/
|
|
256
|
-
async function resolvePrismaInvocation(cwd) {
|
|
257
|
-
if (await localPrismaBinExists(cwd)) return {
|
|
258
|
-
argsPrefix: ["--no-install", "prisma"],
|
|
259
|
-
displayPrefix: "npx --no-install prisma"
|
|
260
|
-
};
|
|
261
|
-
const pinned = await readInstalledPrismaClientVersion(cwd) ?? FALLBACK_PRISMA_CLI_VERSION;
|
|
262
|
-
return {
|
|
263
|
-
argsPrefix: ["--yes", `prisma@${pinned}`],
|
|
264
|
-
displayPrefix: `npx prisma@${pinned}`
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
/** npm/pnpm name the local CLI shim `prisma` on POSIX and `prisma.cmd`/`prisma.ps1` on Windows. */
|
|
268
|
-
async function localPrismaBinExists(cwd) {
|
|
269
|
-
const binDir = path.join(cwd, "node_modules", ".bin");
|
|
270
|
-
return (await Promise.all([
|
|
271
|
-
"prisma",
|
|
272
|
-
"prisma.cmd",
|
|
273
|
-
"prisma.ps1"
|
|
274
|
-
].map((name) => fileExists(path.join(binDir, name))))).some(Boolean);
|
|
275
|
-
}
|
|
276
|
-
async function fileExists(filePath) {
|
|
277
|
-
try {
|
|
278
|
-
await access(filePath);
|
|
279
|
-
return true;
|
|
280
|
-
} catch {
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
async function readInstalledPrismaClientVersion(cwd) {
|
|
285
|
-
try {
|
|
286
|
-
const raw = await readFile(path.join(cwd, "node_modules", "@prisma", "client", "package.json"), { encoding: "utf8" });
|
|
287
|
-
const parsed = JSON.parse(raw);
|
|
288
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
289
|
-
const version = parsed.version;
|
|
290
|
-
return typeof version === "string" && version.length > 0 ? version : null;
|
|
291
|
-
} catch {
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
function buildSchemaSetupCommands(schema, schemaPath, databaseUrl, prisma) {
|
|
296
|
-
if (schema.command === "migrate-deploy") return [{
|
|
297
|
-
args: [
|
|
298
|
-
...prisma.argsPrefix,
|
|
299
|
-
"migrate",
|
|
300
|
-
"deploy",
|
|
301
|
-
"--schema",
|
|
302
|
-
schemaPath
|
|
303
|
-
],
|
|
304
|
-
displayCommand: `${prisma.displayPrefix} migrate deploy`
|
|
305
|
-
}];
|
|
306
|
-
if (schema.command === "db-push") return [{
|
|
307
|
-
args: [
|
|
308
|
-
...prisma.argsPrefix,
|
|
309
|
-
"db",
|
|
310
|
-
"push",
|
|
311
|
-
"--schema",
|
|
312
|
-
schemaPath
|
|
313
|
-
],
|
|
314
|
-
displayCommand: `${prisma.displayPrefix} db push`
|
|
315
|
-
}];
|
|
316
|
-
return [{
|
|
317
|
-
args: [
|
|
318
|
-
"--no-install",
|
|
319
|
-
"prisma-next",
|
|
320
|
-
"contract",
|
|
321
|
-
"emit",
|
|
322
|
-
"--config",
|
|
323
|
-
schemaPath
|
|
324
|
-
],
|
|
325
|
-
displayCommand: "npx --no-install prisma-next contract emit"
|
|
326
|
-
}, {
|
|
327
|
-
args: [
|
|
328
|
-
"--no-install",
|
|
329
|
-
"prisma-next",
|
|
330
|
-
"db",
|
|
331
|
-
"init",
|
|
332
|
-
"--config",
|
|
333
|
-
schemaPath,
|
|
334
|
-
"--db",
|
|
335
|
-
databaseUrl
|
|
336
|
-
],
|
|
337
|
-
displayCommand: "npx --no-install prisma-next db init"
|
|
338
|
-
}];
|
|
339
|
-
}
|
|
340
|
-
function defaultSchemaSourcePath(schema) {
|
|
341
|
-
return schema.kind === "prisma-next" ? "prisma-next.config.ts" : "schema.prisma";
|
|
342
|
-
}
|
|
343
|
-
async function runPrismaCommand(options) {
|
|
344
|
-
const shouldPipeOutput = !options.context.flags.json && !options.context.flags.quiet;
|
|
345
|
-
const child = spawn("npx", options.args, {
|
|
346
|
-
cwd: options.context.runtime.cwd,
|
|
347
|
-
env: {
|
|
348
|
-
...options.context.runtime.env,
|
|
349
|
-
...options.env
|
|
350
|
-
},
|
|
351
|
-
signal: options.context.runtime.signal,
|
|
352
|
-
stdio: shouldPipeOutput ? [
|
|
353
|
-
"ignore",
|
|
354
|
-
"pipe",
|
|
355
|
-
"pipe"
|
|
356
|
-
] : [
|
|
357
|
-
"ignore",
|
|
358
|
-
"ignore",
|
|
359
|
-
"ignore"
|
|
360
|
-
]
|
|
361
|
-
});
|
|
362
|
-
if (shouldPipeOutput) {
|
|
363
|
-
child.stdout?.pipe(options.context.output.stderr, { end: false });
|
|
364
|
-
child.stderr?.pipe(options.context.output.stderr, { end: false });
|
|
365
|
-
}
|
|
366
|
-
const exit = await new Promise((resolve, reject) => {
|
|
367
|
-
child.once("error", reject);
|
|
368
|
-
child.once("close", (code, signal) => resolve({
|
|
369
|
-
code,
|
|
370
|
-
signal
|
|
371
|
-
}));
|
|
372
|
-
});
|
|
373
|
-
if (exit.signal) throw new Error(`${options.displayCommand} was terminated by ${exit.signal}.`);
|
|
374
|
-
if (exit.code !== 0) throw new Error(`${options.displayCommand} exited with code ${exit.code ?? 1}.`);
|
|
375
|
-
}
|
|
376
214
|
//#endregion
|
|
377
|
-
export { hasBranchDatabaseSignal, inspectBranchDatabaseSignal
|
|
215
|
+
export { hasBranchDatabaseSignal, inspectBranchDatabaseSignal };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { CliError } from "../../shell/errors.js";
|
|
2
|
+
import { COMPUTE_CONFIG_FILENAME, COMPUTE_CONFIG_FILENAME as COMPUTE_CONFIG_FILENAME$1, ComputeConfigTargetRequiredError, computeTargetAppDir, frameworkByKey, inferComputeTargetFromCwd, loadComputeConfig, selectComputeDeployTarget } from "@prisma/compute-sdk/config";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { matchError } from "better-result";
|
|
5
|
+
//#region src/lib/app/compute-config.ts
|
|
6
|
+
/**
|
|
7
|
+
* Loads the nearest compute config, searching from `cwd` up to the source
|
|
8
|
+
* root (repository or workspace boundary). Thin adapter over the SDK loader
|
|
9
|
+
* keeping the CLI's positional-signal call shape.
|
|
10
|
+
*/
|
|
11
|
+
async function loadComputeConfig$1(cwd, signal) {
|
|
12
|
+
return loadComputeConfig(cwd, { signal });
|
|
13
|
+
}
|
|
14
|
+
/** Local build/run strategy implied by a configured framework. */
|
|
15
|
+
function computeFrameworkToBuildType(framework) {
|
|
16
|
+
return frameworkByKey(framework).buildType;
|
|
17
|
+
}
|
|
18
|
+
function mergeComputeDeployInputs(options) {
|
|
19
|
+
const { cli, target, configFilename } = options;
|
|
20
|
+
const configAnnotation = `set by ${configFilename}`;
|
|
21
|
+
const framework = cli.framework ? {
|
|
22
|
+
value: cli.framework,
|
|
23
|
+
annotation: "set by --framework"
|
|
24
|
+
} : target?.framework ? {
|
|
25
|
+
value: target.framework,
|
|
26
|
+
annotation: configAnnotation
|
|
27
|
+
} : void 0;
|
|
28
|
+
const entrypoint = cli.entrypoint ? {
|
|
29
|
+
value: cli.entrypoint,
|
|
30
|
+
annotation: "set by --entry"
|
|
31
|
+
} : target?.entry ? {
|
|
32
|
+
value: target.entry,
|
|
33
|
+
annotation: configAnnotation
|
|
34
|
+
} : void 0;
|
|
35
|
+
const httpPort = cli.httpPort ? {
|
|
36
|
+
value: cli.httpPort,
|
|
37
|
+
annotation: "set by --http-port"
|
|
38
|
+
} : target?.httpPort ? {
|
|
39
|
+
value: String(target.httpPort),
|
|
40
|
+
annotation: configAnnotation
|
|
41
|
+
} : void 0;
|
|
42
|
+
const cliEnvInputs = cli.envInputs && cli.envInputs.length > 0 ? cli.envInputs : void 0;
|
|
43
|
+
const configEnvInputs = target && target.envInputs.length > 0 ? target.envInputs : void 0;
|
|
44
|
+
const envInputs = cliEnvInputs ?? configEnvInputs;
|
|
45
|
+
const configAppName = target?.name ? {
|
|
46
|
+
value: target.name,
|
|
47
|
+
annotation: configAnnotation
|
|
48
|
+
} : target?.key ? {
|
|
49
|
+
value: target.key,
|
|
50
|
+
annotation: configAnnotation
|
|
51
|
+
} : void 0;
|
|
52
|
+
return {
|
|
53
|
+
framework,
|
|
54
|
+
entrypoint,
|
|
55
|
+
httpPort,
|
|
56
|
+
envInputs,
|
|
57
|
+
envInputsFromConfig: !cliEnvInputs && configEnvInputs !== void 0,
|
|
58
|
+
configAppName,
|
|
59
|
+
appRoot: target?.root ?? void 0
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Merges CLI inputs for the local `app build` and `app run` commands with a
|
|
64
|
+
* selected config target. Explicit flags win; `--build-type auto` is the
|
|
65
|
+
* flag default and defers to the configured framework.
|
|
66
|
+
*/
|
|
67
|
+
function mergeComputeLocalInputs(options) {
|
|
68
|
+
const { cli, target } = options;
|
|
69
|
+
const cliBuildType = cli.buildType && cli.buildType !== "auto" ? cli.buildType : void 0;
|
|
70
|
+
const configBuildType = target?.framework ? computeFrameworkToBuildType(target.framework) : void 0;
|
|
71
|
+
return {
|
|
72
|
+
entrypoint: cli.entrypoint ?? target?.entry ?? void 0,
|
|
73
|
+
buildType: cliBuildType ?? configBuildType,
|
|
74
|
+
buildTypeFromConfig: !cliBuildType && configBuildType !== void 0,
|
|
75
|
+
port: cli.port ?? (target?.httpPort ? String(target.httpPort) : void 0),
|
|
76
|
+
appRoot: target?.root ?? void 0
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function computeConfigErrorToCliError(error, commandName = "deploy") {
|
|
80
|
+
const command = `prisma-cli app ${commandName}`;
|
|
81
|
+
return matchError(error, {
|
|
82
|
+
ComputeConfigAmbiguousError: (ambiguous) => new CliError({
|
|
83
|
+
code: "COMPUTE_CONFIG_INVALID",
|
|
84
|
+
domain: "app",
|
|
85
|
+
summary: "Multiple compute config files found",
|
|
86
|
+
why: ambiguous.message,
|
|
87
|
+
fix: `Keep exactly one compute config file, preferably ${COMPUTE_CONFIG_FILENAME}.`,
|
|
88
|
+
meta: { configPaths: ambiguous.configPaths },
|
|
89
|
+
exitCode: 2,
|
|
90
|
+
nextSteps: [command]
|
|
91
|
+
}),
|
|
92
|
+
ComputeConfigLoadError: (load) => new CliError({
|
|
93
|
+
code: "COMPUTE_CONFIG_INVALID",
|
|
94
|
+
domain: "app",
|
|
95
|
+
summary: `Could not load ${path.basename(load.configPath)}`,
|
|
96
|
+
why: load.message,
|
|
97
|
+
fix: `Fix the error in ${path.basename(load.configPath)} and rerun the command.`,
|
|
98
|
+
where: load.configPath,
|
|
99
|
+
meta: { configPath: load.configPath },
|
|
100
|
+
exitCode: 2,
|
|
101
|
+
nextSteps: [command]
|
|
102
|
+
}),
|
|
103
|
+
ComputeConfigInvalidError: (invalid) => new CliError({
|
|
104
|
+
code: "COMPUTE_CONFIG_INVALID",
|
|
105
|
+
domain: "app",
|
|
106
|
+
summary: `Invalid ${path.basename(invalid.configPath)}`,
|
|
107
|
+
why: invalid.issues.join(" "),
|
|
108
|
+
fix: `Edit ${path.basename(invalid.configPath)} so it default-exports defineComputeConfig({ app }) or defineComputeConfig({ apps }).`,
|
|
109
|
+
where: invalid.configPath,
|
|
110
|
+
meta: {
|
|
111
|
+
configPath: invalid.configPath,
|
|
112
|
+
issues: invalid.issues
|
|
113
|
+
},
|
|
114
|
+
exitCode: 2,
|
|
115
|
+
nextSteps: [command]
|
|
116
|
+
}),
|
|
117
|
+
ComputeConfigTargetRequiredError: (required) => new CliError({
|
|
118
|
+
code: "COMPUTE_CONFIG_TARGET_REQUIRED",
|
|
119
|
+
domain: "app",
|
|
120
|
+
summary: "App target required",
|
|
121
|
+
why: required.message,
|
|
122
|
+
fix: `Pass the app target, for example ${command} <target>.`,
|
|
123
|
+
meta: {
|
|
124
|
+
configPath: required.configPath,
|
|
125
|
+
availableTargets: required.availableTargets
|
|
126
|
+
},
|
|
127
|
+
exitCode: 2,
|
|
128
|
+
nextSteps: required.availableTargets.map((target) => `${command} ${target}`)
|
|
129
|
+
}),
|
|
130
|
+
ComputeConfigTargetUnknownError: (unknown) => new CliError({
|
|
131
|
+
code: "COMPUTE_CONFIG_TARGET_UNKNOWN",
|
|
132
|
+
domain: "app",
|
|
133
|
+
summary: `Unknown app target "${unknown.requestedTarget}"`,
|
|
134
|
+
why: unknown.message,
|
|
135
|
+
fix: unknown.availableTargets.length > 0 ? `Pass one of the configured targets: ${unknown.availableTargets.join(", ")}.` : "Remove the target argument; this config defines a single app.",
|
|
136
|
+
meta: {
|
|
137
|
+
configPath: unknown.configPath,
|
|
138
|
+
requestedTarget: unknown.requestedTarget,
|
|
139
|
+
availableTargets: unknown.availableTargets
|
|
140
|
+
},
|
|
141
|
+
exitCode: 2,
|
|
142
|
+
nextSteps: unknown.availableTargets.length > 0 ? unknown.availableTargets.map((target) => `${command} ${target}`) : [command]
|
|
143
|
+
})
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
export { COMPUTE_CONFIG_FILENAME$1 as COMPUTE_CONFIG_FILENAME, ComputeConfigTargetRequiredError, computeConfigErrorToCliError, computeFrameworkToBuildType, computeTargetAppDir, inferComputeTargetFromCwd, loadComputeConfig$1 as loadComputeConfig, mergeComputeDeployInputs, mergeComputeLocalInputs, selectComputeDeployTarget };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
//#region src/lib/app/deploy-plan.ts
|
|
2
|
+
/**
|
|
3
|
+
* Decides whether a deploy covers one app or the whole system. A multi-app
|
|
4
|
+
* config with no target named or inferred deploys every target in declaration
|
|
5
|
+
* order; everything else (single-app config, an explicit/inferred target, or
|
|
6
|
+
* no config at all) is a single deploy.
|
|
7
|
+
*/
|
|
8
|
+
function planAppDeploy(options) {
|
|
9
|
+
const { config, requestedTarget, hasCreateProject } = options;
|
|
10
|
+
if (!(config !== null && config.kind === "multi" && !requestedTarget && config.targets.length > 1)) return { mode: "single" };
|
|
11
|
+
const total = config.targets.length;
|
|
12
|
+
return {
|
|
13
|
+
mode: "all",
|
|
14
|
+
targets: config.targets.map((target, index) => ({
|
|
15
|
+
targetKey: target.key,
|
|
16
|
+
index,
|
|
17
|
+
total,
|
|
18
|
+
bindsCreateProject: index === 0 && hasCreateProject
|
|
19
|
+
}))
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Names the per-app inputs present in a deploy-all invocation, in the order
|
|
24
|
+
* they should be reported. An empty list means the run is unambiguous; the
|
|
25
|
+
* caller renders the usage error when it is not.
|
|
26
|
+
*/
|
|
27
|
+
function perAppInputsForDeployAll(inputs) {
|
|
28
|
+
return [
|
|
29
|
+
["--app", inputs.appName],
|
|
30
|
+
["--framework", inputs.framework],
|
|
31
|
+
["--entry", inputs.entrypoint],
|
|
32
|
+
["--http-port", inputs.httpPort],
|
|
33
|
+
["--env", inputs.envAssignments?.length ? inputs.envAssignments : void 0],
|
|
34
|
+
[inputs.appIdEnvVar.name, inputs.appIdEnvVar.value]
|
|
35
|
+
].filter(([, value]) => value !== void 0).map(([flag]) => flag);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Describes a deploy-all run that stopped at the first failing target: which
|
|
39
|
+
* targets already went live and which were never attempted, so the caller can
|
|
40
|
+
* surface a converge path. Pure; the caller folds this into the CliError.
|
|
41
|
+
*/
|
|
42
|
+
function describeDeployAllFailure(options) {
|
|
43
|
+
const { targetKeys, failedIndex, completed } = options;
|
|
44
|
+
const failedTarget = targetKeys[failedIndex];
|
|
45
|
+
const notAttempted = targetKeys.slice(failedIndex + 1);
|
|
46
|
+
return {
|
|
47
|
+
failedTarget,
|
|
48
|
+
completed,
|
|
49
|
+
notAttempted,
|
|
50
|
+
contextLines: [
|
|
51
|
+
`Deploying all apps stopped at "${failedTarget}" (${failedIndex + 1}/${targetKeys.length}).`,
|
|
52
|
+
...completed.length > 0 ? [`Already live: ${completed.map((entry) => entry.target).join(", ")}.`] : [],
|
|
53
|
+
...notAttempted.length > 0 ? [`Not attempted: ${notAttempted.join(", ")}.`] : []
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { describeDeployAllFailure, perAppInputsForDeployAll, planAppDeploy };
|