@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
|
@@ -1,25 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { resolveBunEntrypoint } from "./bun-project.js";
|
|
2
|
+
import "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
//#region src/lib/app/local-dev.ts
|
|
6
|
-
const NEXT_CONFIG_FILENAMES = [
|
|
7
|
-
"next.config.js",
|
|
8
|
-
"next.config.mjs",
|
|
9
|
-
"next.config.ts",
|
|
10
|
-
"next.config.mts"
|
|
11
|
-
];
|
|
12
6
|
const DEFAULT_LOCAL_DEV_PORT = 3e3;
|
|
13
|
-
async function resolveLocalBuildType(appPath, buildType, signal) {
|
|
14
|
-
if (buildType === "bun" || buildType === "nextjs") return buildType;
|
|
15
|
-
if (buildType !== "auto") return null;
|
|
16
|
-
return detectLocalBuildType(appPath, signal);
|
|
17
|
-
}
|
|
18
|
-
async function detectLocalBuildType(appPath, signal) {
|
|
19
|
-
if (await isNextProject(appPath, signal)) return "nextjs";
|
|
20
|
-
if (await isBunProject(appPath, signal)) return "bun";
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
7
|
async function runLocalApp(options) {
|
|
24
8
|
const spawnImpl = options.spawnImpl ?? spawn;
|
|
25
9
|
if (options.buildType === "nextjs") {
|
|
@@ -92,47 +76,6 @@ async function runLocalApp(options) {
|
|
|
92
76
|
signal: command.signal
|
|
93
77
|
};
|
|
94
78
|
}
|
|
95
|
-
async function isNextProject(appPath, signal) {
|
|
96
|
-
for (const fileName of NEXT_CONFIG_FILENAMES) {
|
|
97
|
-
signal?.throwIfAborted();
|
|
98
|
-
try {
|
|
99
|
-
await access(path.join(appPath, fileName));
|
|
100
|
-
signal?.throwIfAborted();
|
|
101
|
-
return true;
|
|
102
|
-
} catch (error) {
|
|
103
|
-
if (signal?.aborted) throw error;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return hasDependency(await readBunPackageJson(appPath, signal), "next");
|
|
107
|
-
}
|
|
108
|
-
async function isBunProject(appPath, signal) {
|
|
109
|
-
signal?.throwIfAborted();
|
|
110
|
-
try {
|
|
111
|
-
await access(path.join(appPath, "bun.lock"));
|
|
112
|
-
signal?.throwIfAborted();
|
|
113
|
-
return true;
|
|
114
|
-
} catch (error) {
|
|
115
|
-
if (signal?.aborted) throw error;
|
|
116
|
-
}
|
|
117
|
-
signal?.throwIfAborted();
|
|
118
|
-
try {
|
|
119
|
-
await access(path.join(appPath, "bun.lockb"));
|
|
120
|
-
signal?.throwIfAborted();
|
|
121
|
-
return true;
|
|
122
|
-
} catch (error) {
|
|
123
|
-
if (signal?.aborted) throw error;
|
|
124
|
-
}
|
|
125
|
-
const packageJson = await readBunPackageJson(appPath, signal);
|
|
126
|
-
if (!packageJson) return false;
|
|
127
|
-
const hasEntrypoint = typeof readBunPackageEntrypoint(packageJson) === "string";
|
|
128
|
-
const hasBunDependency = hasDependency(packageJson, "@types/bun") || hasDependency(packageJson, "bun");
|
|
129
|
-
const usesBunScripts = (typeof packageJson.scripts === "object" && packageJson.scripts !== null ? Object.values(packageJson.scripts) : []).some((value) => typeof value === "string" && /\bbun\b/.test(value));
|
|
130
|
-
return hasEntrypoint && (hasBunDependency || usesBunScripts);
|
|
131
|
-
}
|
|
132
|
-
function hasDependency(packageJson, dependencyName) {
|
|
133
|
-
if (!packageJson) return false;
|
|
134
|
-
return [packageJson.dependencies, packageJson.devDependencies].some((group) => typeof group === "object" && group !== null && dependencyName in group);
|
|
135
|
-
}
|
|
136
79
|
async function runWithFallback(candidates, options, spawnImpl, missingCommandMessage) {
|
|
137
80
|
for (const candidate of candidates) try {
|
|
138
81
|
const result = await spawnCommand(candidate, options, spawnImpl);
|
|
@@ -163,4 +106,4 @@ function spawnCommand(candidate, options, spawnImpl) {
|
|
|
163
106
|
});
|
|
164
107
|
}
|
|
165
108
|
//#endregion
|
|
166
|
-
export { DEFAULT_LOCAL_DEV_PORT,
|
|
109
|
+
export { DEFAULT_LOCAL_DEV_PORT, runLocalApp };
|
|
@@ -1,64 +1,102 @@
|
|
|
1
|
-
import { CliError } from "../../shell/errors.js";
|
|
2
1
|
import { readBunPackageJson } from "./bun-project.js";
|
|
3
|
-
import {
|
|
2
|
+
import { sourceRootLineage } from "@prisma/compute-sdk/config";
|
|
3
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { exec } from "node:child_process";
|
|
6
6
|
import { parseModule } from "magicast";
|
|
7
7
|
//#region src/lib/app/preview-build-settings.ts
|
|
8
|
+
/** Legacy build-settings file: no longer read or written, only detected for migration. */
|
|
8
9
|
const PRISMA_APP_CONFIG_FILENAME = "prisma.app.json";
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Detects a leftover `prisma.app.json`. The file is no longer used: one that
|
|
12
|
+
* matches the effective settings is reported for deletion, one with custom
|
|
13
|
+
* values must be migrated to the compute config so builds never silently
|
|
14
|
+
* change.
|
|
15
|
+
*/
|
|
16
|
+
async function detectLegacyBuildSettings(options) {
|
|
11
17
|
const configPath = path.join(options.appPath, PRISMA_APP_CONFIG_FILENAME);
|
|
12
|
-
|
|
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
|
-
};
|
|
18
|
+
let content;
|
|
30
19
|
try {
|
|
31
|
-
|
|
20
|
+
options.signal?.throwIfAborted();
|
|
21
|
+
content = await readFile(configPath, {
|
|
32
22
|
encoding: "utf8",
|
|
33
|
-
flag: "wx",
|
|
34
23
|
signal: options.signal
|
|
35
24
|
});
|
|
36
25
|
} catch (error) {
|
|
37
|
-
if (
|
|
38
|
-
|
|
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;
|
|
26
|
+
if (options.signal?.aborted) throw error;
|
|
27
|
+
return { kind: "absent" };
|
|
52
28
|
}
|
|
53
|
-
|
|
54
|
-
|
|
29
|
+
let legacy;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(content);
|
|
32
|
+
const buildCommand = parsed.buildCommand === null || typeof parsed.buildCommand === "string" ? typeof parsed.buildCommand === "string" ? parsed.buildCommand.trim() || null : null : void 0;
|
|
33
|
+
const outputDirectory = typeof parsed.outputDirectory === "string" ? normalizeRelativePath(parsed.outputDirectory) : void 0;
|
|
34
|
+
if (buildCommand === void 0 || !outputDirectory) return {
|
|
35
|
+
kind: "invalid",
|
|
36
|
+
configPath
|
|
37
|
+
};
|
|
38
|
+
legacy = {
|
|
39
|
+
buildCommand,
|
|
40
|
+
outputDirectory
|
|
41
|
+
};
|
|
42
|
+
} catch {
|
|
43
|
+
return {
|
|
44
|
+
kind: "invalid",
|
|
45
|
+
configPath
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return legacy.buildCommand === options.effective.buildCommand && legacy.outputDirectory === options.effective.outputDirectory ? {
|
|
49
|
+
kind: "matching",
|
|
50
|
+
configPath
|
|
51
|
+
} : {
|
|
52
|
+
kind: "custom",
|
|
55
53
|
configPath,
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
...legacy
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Resolves build settings purely from framework inference; nothing is read or written. */
|
|
58
|
+
async function resolveInferredPreviewBuildSettings(options) {
|
|
59
|
+
return {
|
|
60
|
+
status: "inferred",
|
|
61
|
+
configPath: null,
|
|
62
|
+
relativeConfigPath: null,
|
|
63
|
+
settings: await resolvePreviewBuildSettings(options)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolves build settings when the compute config owns them: configured
|
|
68
|
+
* fields win, omitted fields fall back to framework defaults.
|
|
69
|
+
*/
|
|
70
|
+
async function resolveConfiguredPreviewBuildSettings(options) {
|
|
71
|
+
const configFilename = path.basename(options.configPath);
|
|
72
|
+
const source = `set by ${configFilename}`;
|
|
73
|
+
const fallback = options.configured.command === void 0 || options.configured.outputDirectory === void 0 ? await resolvePreviewBuildSettings(options) : null;
|
|
74
|
+
return {
|
|
75
|
+
status: "config",
|
|
76
|
+
configPath: options.configPath,
|
|
77
|
+
relativeConfigPath: configFilename,
|
|
78
|
+
settings: {
|
|
79
|
+
buildCommand: options.configured.command !== void 0 ? options.configured.command : fallback.buildCommand,
|
|
80
|
+
buildCommandSource: options.configured.command !== void 0 ? source : fallback.buildCommandSource,
|
|
81
|
+
outputDirectory: options.configured.outputDirectory ?? fallback.outputDirectory,
|
|
82
|
+
outputDirectorySource: options.configured.outputDirectory !== void 0 ? source : fallback.outputDirectorySource
|
|
83
|
+
}
|
|
58
84
|
};
|
|
59
85
|
}
|
|
60
86
|
async function resolvePreviewBuildSettings(options) {
|
|
61
87
|
switch (options.buildType) {
|
|
88
|
+
case "nuxt": return {
|
|
89
|
+
buildCommand: "nuxt build",
|
|
90
|
+
buildCommandSource: "Nuxt default",
|
|
91
|
+
outputDirectory: ".output",
|
|
92
|
+
outputDirectorySource: "Nuxt output"
|
|
93
|
+
};
|
|
94
|
+
case "astro": return {
|
|
95
|
+
buildCommand: "astro build",
|
|
96
|
+
buildCommandSource: "Astro default",
|
|
97
|
+
outputDirectory: "dist",
|
|
98
|
+
outputDirectorySource: "Astro output"
|
|
99
|
+
};
|
|
62
100
|
case "nextjs": {
|
|
63
101
|
const packageJson = await readBunPackageJson(options.appPath, options.signal);
|
|
64
102
|
const buildCommand = await resolveFrameworkBuildCommand(options.appPath, packageJson, {
|
|
@@ -104,57 +142,6 @@ async function resolvePreviewBuildSettings(options) {
|
|
|
104
142
|
}
|
|
105
143
|
}
|
|
106
144
|
}
|
|
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
145
|
async function hasRootFile(appPath, filenames, signal) {
|
|
159
146
|
let entries;
|
|
160
147
|
try {
|
|
@@ -203,12 +190,14 @@ function readBuildScript(packageJson) {
|
|
|
203
190
|
return buildScript.length > 0 ? buildScript : null;
|
|
204
191
|
}
|
|
205
192
|
async function resolvePackageManager(appPath, packageJson, signal) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
193
|
+
for (const directory of await sourceRootLineage(appPath, signal)) {
|
|
194
|
+
const fromPackageManager = packageManagerFromPackageJson((directory === path.resolve(appPath) ? packageJson : await readBunPackageJson(directory, signal))?.packageManager);
|
|
195
|
+
if (fromPackageManager) return fromPackageManager;
|
|
196
|
+
if (await pathExists(path.join(directory, "bun.lock"), signal) || await pathExists(path.join(directory, "bun.lockb"), signal)) return "bun";
|
|
197
|
+
if (await pathExists(path.join(directory, "pnpm-lock.yaml"), signal)) return "pnpm";
|
|
198
|
+
if (await pathExists(path.join(directory, "yarn.lock"), signal)) return "yarn";
|
|
199
|
+
if (await pathExists(path.join(directory, "package-lock.json"), signal)) return "npm";
|
|
200
|
+
}
|
|
212
201
|
}
|
|
213
202
|
function packageManagerFromPackageJson(value) {
|
|
214
203
|
if (typeof value !== "string") return null;
|
|
@@ -217,15 +206,16 @@ function packageManagerFromPackageJson(value) {
|
|
|
217
206
|
}
|
|
218
207
|
async function runResolvedBuildCommand(appPath, settings, failurePrefix, signal) {
|
|
219
208
|
if (!settings.buildCommand) return;
|
|
220
|
-
await
|
|
209
|
+
const binDirs = (await sourceRootLineage(appPath, signal)).map((directory) => path.join(directory, "node_modules", ".bin"));
|
|
210
|
+
await execBuildCommand(settings.buildCommand, appPath, binDirs, failurePrefix, signal);
|
|
221
211
|
}
|
|
222
|
-
function execBuildCommand(command, cwd, failurePrefix, signal) {
|
|
212
|
+
function execBuildCommand(command, cwd, binDirs, failurePrefix, signal) {
|
|
223
213
|
return new Promise((resolve, reject) => {
|
|
224
214
|
const child = exec(command, {
|
|
225
215
|
cwd,
|
|
226
216
|
env: {
|
|
227
217
|
...process.env,
|
|
228
|
-
PATH: [
|
|
218
|
+
PATH: [...binDirs, process.env.PATH].filter(Boolean).join(path.delimiter)
|
|
229
219
|
},
|
|
230
220
|
maxBuffer: 10 * 1024 * 1024,
|
|
231
221
|
signal
|
|
@@ -365,6 +355,7 @@ function propertyKeyName(value) {
|
|
|
365
355
|
function normalizeRelativePath(value) {
|
|
366
356
|
const raw = value.trim().replace(/\\/g, "/");
|
|
367
357
|
if (raw.length === 0 || raw.split("/").includes("..")) return;
|
|
358
|
+
if (/^[A-Za-z]:/.test(raw)) return;
|
|
368
359
|
const normalized = path.posix.normalize(raw);
|
|
369
360
|
const segments = normalized.split("/");
|
|
370
361
|
if (path.win32.isAbsolute(value) || path.posix.isAbsolute(normalized) || segments.includes("..")) return;
|
|
@@ -382,4 +373,4 @@ async function pathExists(targetPath, signal) {
|
|
|
382
373
|
}
|
|
383
374
|
}
|
|
384
375
|
//#endregion
|
|
385
|
-
export { PRISMA_APP_CONFIG_FILENAME,
|
|
376
|
+
export { PRISMA_APP_CONFIG_FILENAME, detectLegacyBuildSettings, hasAnyPackageDependency, hasPackageDependency, hasRootFile, joinPosix, nextOutputRootFromStandaloneDirectory, resolveConfiguredPreviewBuildSettings, resolveInferredPreviewBuildSettings, resolvePreviewBuildSettings, runResolvedBuildCommand };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readBunPackageJson, resolveBunEntrypoint } from "./bun-project.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { hasAnyPackageDependency, hasPackageDependency, hasRootFile, joinPosix, nextOutputRootFromStandaloneDirectory, resolvePreviewBuildSettings, runResolvedBuildCommand } from "./preview-build-settings.js";
|
|
3
|
+
import { resolveSourceRoot } from "@prisma/compute-sdk/config";
|
|
4
|
+
import { chmod, copyFile, cp, lstat, mkdir, mkdtemp, readdir, readlink, rm, stat, writeFile } from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import os from "node:os";
|
|
6
7
|
import { AstroBuild, BunBuild, NuxtBuild } from "@prisma/compute-sdk";
|
|
@@ -246,7 +247,7 @@ var PreviewTanstackStartBuild = class {
|
|
|
246
247
|
const outputDir = path.join(this.#appPath, settings.outputDirectory);
|
|
247
248
|
const entrypoint = "server/index.mjs";
|
|
248
249
|
const entryPath = path.join(outputDir, entrypoint);
|
|
249
|
-
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
|
|
250
|
+
if (!(await unsupportedFilesystemBoundary(signal, () => stat(entryPath).catch(() => null)))?.isFile()) throw new Error(`TanStack Start build did not produce a Nitro node server entrypoint at ${joinPosix(settings.outputDirectory, entrypoint)}. Ensure your vite.config includes the tanstackStart() and nitro() plugins with the default node preset, or set build.outputDirectory in prisma.compute.ts.`);
|
|
250
251
|
const outDir = await unsupportedFilesystemBoundary(signal, () => mkdtemp(path.join(os.tmpdir(), "compute-build-")));
|
|
251
252
|
try {
|
|
252
253
|
const artifactDir = path.join(outDir, "app");
|
|
@@ -315,7 +316,7 @@ async function stageNextjsStandaloneArtifact(options) {
|
|
|
315
316
|
sourceRoot: await resolveSourceRoot(appRoot, options.signal),
|
|
316
317
|
signal: options.signal
|
|
317
318
|
});
|
|
318
|
-
await
|
|
319
|
+
await hoistIsolatedStoreDependencies(path.join(artifactRoot, "node_modules"), options.signal);
|
|
319
320
|
}
|
|
320
321
|
async function copyNextjsStaticAssets(options) {
|
|
321
322
|
const serverSubpath = nextjsServerSubpath(options.entrypoint);
|
|
@@ -357,12 +358,21 @@ function nextjsServerSubpath(entrypoint) {
|
|
|
357
358
|
const dir = path.posix.dirname(entrypoint);
|
|
358
359
|
return dir === "." ? "" : dir;
|
|
359
360
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
361
|
+
/**
|
|
362
|
+
* pnpm and bun (isolated linker) both keep packages in a virtual store with a
|
|
363
|
+
* shared symlink farm (`.pnpm/node_modules`, `.bun/node_modules`). Hoist the
|
|
364
|
+
* farm entries to the artifact's node_modules root so Node-style resolution
|
|
365
|
+
* works after symlinks are materialized.
|
|
366
|
+
*/
|
|
367
|
+
async function hoistIsolatedStoreDependencies(nodeModulesDir, signal) {
|
|
368
|
+
await hoistStoreDependencies(nodeModulesDir, path.join(nodeModulesDir, ".pnpm", "node_modules"), signal);
|
|
369
|
+
await hoistStoreDependencies(nodeModulesDir, path.join(nodeModulesDir, ".bun", "node_modules"), signal);
|
|
370
|
+
}
|
|
371
|
+
async function hoistStoreDependencies(nodeModulesDir, storeNodeModulesDir, signal) {
|
|
372
|
+
if (!await directoryExists(storeNodeModulesDir, signal)) return;
|
|
373
|
+
const entries = await unsupportedFilesystemBoundary(signal, () => readdir(storeNodeModulesDir, { withFileTypes: true }));
|
|
364
374
|
for (const entry of entries) {
|
|
365
|
-
const sourcePath = path.join(
|
|
375
|
+
const sourcePath = path.join(storeNodeModulesDir, entry.name);
|
|
366
376
|
if (entry.name.startsWith("@") && entry.isDirectory()) {
|
|
367
377
|
const scopedEntries = await unsupportedFilesystemBoundary(signal, () => readdir(sourcePath, { withFileTypes: true }));
|
|
368
378
|
for (const scopedEntry of scopedEntries) {
|
|
@@ -370,7 +380,7 @@ async function hoistPnpmDependencies(nodeModulesDir, signal) {
|
|
|
370
380
|
if (await pathExists(scopedDestination, signal)) continue;
|
|
371
381
|
await unsupportedFilesystemBoundary(signal, () => mkdir(path.dirname(scopedDestination), { recursive: true }));
|
|
372
382
|
await copyPathMaterializingSymlinks(path.join(sourcePath, scopedEntry.name), scopedDestination, {
|
|
373
|
-
standaloneRoot:
|
|
383
|
+
standaloneRoot: storeNodeModulesDir,
|
|
374
384
|
appRoot: nodeModulesDir,
|
|
375
385
|
sourceRoot: nodeModulesDir,
|
|
376
386
|
signal
|
|
@@ -381,7 +391,7 @@ async function hoistPnpmDependencies(nodeModulesDir, signal) {
|
|
|
381
391
|
const destinationPath = path.join(nodeModulesDir, entry.name);
|
|
382
392
|
if (await pathExists(destinationPath, signal)) continue;
|
|
383
393
|
await copyPathMaterializingSymlinks(sourcePath, destinationPath, {
|
|
384
|
-
standaloneRoot:
|
|
394
|
+
standaloneRoot: storeNodeModulesDir,
|
|
385
395
|
appRoot: nodeModulesDir,
|
|
386
396
|
sourceRoot: nodeModulesDir,
|
|
387
397
|
signal
|
|
@@ -401,31 +411,23 @@ async function normalizeArtifactSymlinks(artifactDir, appPath, signal) {
|
|
|
401
411
|
continue;
|
|
402
412
|
}
|
|
403
413
|
if (!entry.isSymbolicLink()) continue;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
414
|
+
const target = await unsupportedFilesystemBoundary(signal, () => readlink(fullPath));
|
|
415
|
+
const resolvedTarget = path.resolve(path.dirname(fullPath), target);
|
|
416
|
+
if (isPathWithin(normalizedArtifactDir, resolvedTarget)) continue;
|
|
417
|
+
if (!isPathWithin(normalizedAppPath, resolvedTarget)) throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`);
|
|
418
|
+
const targetStat = await unsupportedFilesystemBoundary(signal, () => stat(resolvedTarget));
|
|
419
|
+
await unsupportedFilesystemBoundary(signal, () => rm(fullPath, {
|
|
420
|
+
force: true,
|
|
421
|
+
recursive: true
|
|
422
|
+
}));
|
|
423
|
+
await unsupportedFilesystemBoundary(signal, () => cp(resolvedTarget, fullPath, {
|
|
424
|
+
recursive: targetStat.isDirectory(),
|
|
425
|
+
dereference: true
|
|
426
|
+
}));
|
|
427
|
+
if (targetStat.isDirectory()) await walkDirectory(fullPath);
|
|
410
428
|
}
|
|
411
429
|
}
|
|
412
430
|
}
|
|
413
|
-
async function materializeArtifactSymlink(options) {
|
|
414
|
-
const target = await unsupportedFilesystemBoundary(options.signal, () => readlink(options.fullPath));
|
|
415
|
-
const resolvedTarget = path.resolve(path.dirname(options.fullPath), target);
|
|
416
|
-
if (isPathWithin(options.normalizedArtifactDir, resolvedTarget)) return "internal";
|
|
417
|
-
if (!isPathWithin(options.normalizedAppPath, resolvedTarget)) throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`);
|
|
418
|
-
const targetStat = await unsupportedFilesystemBoundary(options.signal, () => stat(resolvedTarget));
|
|
419
|
-
await unsupportedFilesystemBoundary(options.signal, () => rm(options.fullPath, {
|
|
420
|
-
force: true,
|
|
421
|
-
recursive: true
|
|
422
|
-
}));
|
|
423
|
-
await unsupportedFilesystemBoundary(options.signal, () => cp(resolvedTarget, options.fullPath, {
|
|
424
|
-
recursive: targetStat.isDirectory(),
|
|
425
|
-
dereference: true
|
|
426
|
-
}));
|
|
427
|
-
return targetStat.isDirectory() ? "directory" : "file";
|
|
428
|
-
}
|
|
429
431
|
function isPathWithin(rootPath, candidatePath) {
|
|
430
432
|
const relativePath = path.relative(rootPath, candidatePath);
|
|
431
433
|
return relativePath === "" || !relativePath.startsWith(`..${path.sep}`) && relativePath !== ".." && !path.isAbsolute(relativePath);
|
|
@@ -489,29 +491,6 @@ async function directoryExists(targetPath, signal) {
|
|
|
489
491
|
return false;
|
|
490
492
|
}
|
|
491
493
|
}
|
|
492
|
-
async function resolveSourceRoot(appRoot, signal) {
|
|
493
|
-
let current = path.resolve(appRoot);
|
|
494
|
-
while (true) {
|
|
495
|
-
if (await pathExists(path.join(current, ".git"), signal) || await pathExists(path.join(current, "pnpm-workspace.yaml"), signal) || await pathExists(path.join(current, "bun.lock"), signal) || await pathExists(path.join(current, "bun.lockb"), signal) || await packageJsonDeclaresWorkspaces(current, signal)) return current;
|
|
496
|
-
const parent = path.dirname(current);
|
|
497
|
-
if (parent === current) return path.resolve(appRoot);
|
|
498
|
-
current = parent;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
async function packageJsonDeclaresWorkspaces(directory, signal) {
|
|
502
|
-
signal?.throwIfAborted();
|
|
503
|
-
try {
|
|
504
|
-
const content = await readFile(path.join(directory, "package.json"), {
|
|
505
|
-
encoding: "utf8",
|
|
506
|
-
signal
|
|
507
|
-
});
|
|
508
|
-
const parsed = JSON.parse(content);
|
|
509
|
-
return Boolean(parsed.workspaces);
|
|
510
|
-
} catch (error) {
|
|
511
|
-
if (signal?.aborted) throw error;
|
|
512
|
-
return false;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
494
|
async function unsupportedFilesystemBoundary(signal, operation) {
|
|
516
495
|
signal?.throwIfAborted();
|
|
517
496
|
const result = await operation();
|
|
@@ -104,12 +104,12 @@ function productionDeployRequiresFlagError() {
|
|
|
104
104
|
"",
|
|
105
105
|
"Production deploys require explicit intent. Re-run with:",
|
|
106
106
|
"",
|
|
107
|
-
" prisma app deploy --prod",
|
|
107
|
+
" prisma-cli app deploy --prod",
|
|
108
108
|
"",
|
|
109
109
|
"Or deploy a preview from a feature branch:",
|
|
110
110
|
"",
|
|
111
111
|
" git checkout -b <branch-name>",
|
|
112
|
-
" prisma app deploy"
|
|
112
|
+
" prisma-cli app deploy"
|
|
113
113
|
]
|
|
114
114
|
});
|
|
115
115
|
}
|
package/dist/lib/diagnostics.js
CHANGED
|
@@ -3,7 +3,7 @@ import { resolveStateDir } from "../shell/runtime.js";
|
|
|
3
3
|
import { readLocalGitState } from "./git/local-status.js";
|
|
4
4
|
//#region src/lib/diagnostics.ts
|
|
5
5
|
async function collectCommandDiagnostics(context, options = {}) {
|
|
6
|
-
const stateDir = resolveStateDir(context.runtime);
|
|
6
|
+
const stateDir = await resolveStateDir(context.runtime);
|
|
7
7
|
return {
|
|
8
8
|
cwd: context.runtime.cwd,
|
|
9
9
|
stateFilePath: resolveLocalStateFilePath(stateDir),
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
//#region src/lib/fs/home-path.ts
|
|
3
|
+
/**
|
|
4
|
+
* Shortens a path under the user's home directory to `~/...` for display,
|
|
5
|
+
* posix-style on every platform. Falls back to the Windows home variables
|
|
6
|
+
* when `HOME` is unset (native cmd/PowerShell sessions).
|
|
7
|
+
*/
|
|
8
|
+
function shortenHomePath(value, env) {
|
|
9
|
+
const resolved = path.resolve(value);
|
|
10
|
+
const home = resolveHomeDirectory(env);
|
|
11
|
+
if (home && (resolved === home || resolved.startsWith(`${home}${path.sep}`))) {
|
|
12
|
+
const relative = path.relative(home, resolved).split(path.sep).join("/");
|
|
13
|
+
return relative ? `~/${relative}` : "~";
|
|
14
|
+
}
|
|
15
|
+
return resolved;
|
|
16
|
+
}
|
|
17
|
+
function resolveHomeDirectory(env) {
|
|
18
|
+
if (env.HOME) return path.resolve(env.HOME);
|
|
19
|
+
if (env.USERPROFILE) return path.resolve(env.USERPROFILE);
|
|
20
|
+
if (env.HOMEDRIVE && env.HOMEPATH) return path.resolve(`${env.HOMEDRIVE}${env.HOMEPATH}`);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { shortenHomePath };
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { access, readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
//#region src/lib/git/local-branch.ts
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the checked-out branch the way git does: the nearest `.git`
|
|
6
|
+
* (directory or worktree file) from `cwd` upward owns the answer, so
|
|
7
|
+
* monorepo commands run from inside a package see the repository branch.
|
|
8
|
+
* Returns null for detached HEAD or when no repository contains `cwd`.
|
|
9
|
+
*/
|
|
4
10
|
async function readLocalGitBranch(cwd, signal) {
|
|
5
|
-
|
|
6
|
-
|
|
11
|
+
for (let directory = path.resolve(cwd);;) {
|
|
12
|
+
const headPath = await resolveGitHeadPath(path.join(directory, ".git"), signal);
|
|
13
|
+
if (headPath) return readBranchFromHead(headPath, signal);
|
|
14
|
+
const parent = path.dirname(directory);
|
|
15
|
+
if (parent === directory) return null;
|
|
16
|
+
directory = parent;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function readBranchFromHead(headPath, signal) {
|
|
7
20
|
try {
|
|
8
21
|
const head = (await readFile(headPath, {
|
|
9
22
|
encoding: "utf8",
|
|
@@ -12,7 +25,6 @@ async function readLocalGitBranch(cwd, signal) {
|
|
|
12
25
|
if (head.startsWith("ref: refs/heads/")) return head.slice(16);
|
|
13
26
|
} catch (error) {
|
|
14
27
|
if (signal.aborted) throw error;
|
|
15
|
-
return null;
|
|
16
28
|
}
|
|
17
29
|
return null;
|
|
18
30
|
}
|
|
@@ -339,7 +339,7 @@ async function resolveBoundProjectTarget(options, projects, settings) {
|
|
|
339
339
|
}
|
|
340
340
|
async function readImplicitLocalPin(options, settings) {
|
|
341
341
|
if (options.explicitProject || settings.allowEnvProjectId && options.envProjectId) return Result.ok(null);
|
|
342
|
-
const localPinResult = await readLocalResolutionPin(options.context.runtime.cwd, options.context.runtime.signal);
|
|
342
|
+
const localPinResult = await readLocalResolutionPin(options.projectDir ?? options.context.runtime.cwd, options.context.runtime.signal);
|
|
343
343
|
if (localPinResult.isErr()) return Result.err(localPinReadErrorToProjectError(localPinResult.error));
|
|
344
344
|
const localPin = localPinResult.value;
|
|
345
345
|
if (localPin.kind === "present" && localPin.pin.workspaceId !== options.workspace.id) return Result.err(new LocalProjectWorkspaceMismatchError({
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { CliError, usageError } from "../../shell/errors.js";
|
|
2
|
+
import { shortenHomePath } from "../fs/home-path.js";
|
|
2
3
|
import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore, writeLocalResolutionPin } from "./local-pin.js";
|
|
3
4
|
import "../../shell/command-arguments.js";
|
|
4
5
|
import { projectAmbiguousError, projectNotFoundError } from "./resolution.js";
|
|
6
|
+
import path from "node:path";
|
|
5
7
|
import { Result, matchError } from "better-result";
|
|
6
8
|
//#region src/lib/project/setup.ts
|
|
7
9
|
function isValidProjectSetupName(projectName) {
|
|
@@ -13,22 +15,21 @@ function validateProjectSetupNameText(value, fallback) {
|
|
|
13
15
|
}
|
|
14
16
|
function resolveProjectForSetup(projectRef, projects, workspace) {
|
|
15
17
|
const matches = projects.filter((project) => project.id === projectRef || project.name === projectRef);
|
|
16
|
-
|
|
17
|
-
if (matches.length === 1 && match) return match;
|
|
18
|
+
if (matches.length === 1) return matches[0];
|
|
18
19
|
if (matches.length > 1) throw projectAmbiguousError(projectRef, matches);
|
|
19
20
|
throw projectNotFoundError(projectRef, workspace);
|
|
20
21
|
}
|
|
21
|
-
async function bindProjectToDirectory(context, workspace, project, action) {
|
|
22
|
+
async function bindProjectToDirectory(context, workspace, project, action, directory = context.runtime.cwd) {
|
|
22
23
|
return Result.gen(async function* () {
|
|
23
|
-
yield* Result.await(writeLocalResolutionPin(
|
|
24
|
+
yield* Result.await(writeLocalResolutionPin(directory, {
|
|
24
25
|
workspaceId: workspace.id,
|
|
25
26
|
projectId: project.id
|
|
26
27
|
}, context.runtime.signal));
|
|
27
|
-
yield* Result.await(ensureLocalResolutionPinGitignore(
|
|
28
|
+
yield* Result.await(ensureLocalResolutionPinGitignore(directory, context.runtime.signal));
|
|
28
29
|
return Result.ok({
|
|
29
30
|
workspace,
|
|
30
31
|
project,
|
|
31
|
-
directory: formatSetupDirectory(context
|
|
32
|
+
directory: formatSetupDirectory(directory, context),
|
|
32
33
|
localPin: {
|
|
33
34
|
path: LOCAL_RESOLUTION_PIN_RELATIVE_PATH,
|
|
34
35
|
written: true
|
|
@@ -110,8 +111,9 @@ function projectCreateFailedError(error, projectName, workspace, options) {
|
|
|
110
111
|
nextSteps: options.nextSteps
|
|
111
112
|
});
|
|
112
113
|
}
|
|
113
|
-
function formatSetupDirectory(
|
|
114
|
-
|
|
114
|
+
function formatSetupDirectory(directory, context) {
|
|
115
|
+
if (path.resolve(directory) !== path.resolve(context.runtime.cwd)) return shortenHomePath(directory, context.runtime.env);
|
|
116
|
+
const basename = directory.split(/[\\/]/).filter(Boolean).pop();
|
|
115
117
|
return basename ? `./${basename}` : ".";
|
|
116
118
|
}
|
|
117
119
|
function extractHttpStatus(error) {
|