@prisma/cli 3.0.0-alpha.0 → 3.0.0-alpha.10
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/README.md +1 -16
- package/dist/adapters/git.js +49 -0
- package/dist/adapters/local-state.js +39 -1
- package/dist/cli2.js +60 -4
- package/dist/commands/app/index.js +43 -30
- package/dist/commands/auth/index.js +3 -2
- package/dist/commands/branch/index.js +2 -1
- package/dist/commands/env.js +87 -0
- package/dist/commands/git/index.js +36 -0
- package/dist/commands/project/index.js +12 -14
- package/dist/commands/version/index.js +18 -0
- package/dist/controllers/app-env.js +223 -0
- package/dist/controllers/app.js +1051 -173
- package/dist/controllers/auth.js +9 -9
- package/dist/controllers/branch.js +6 -6
- package/dist/controllers/project.js +451 -161
- package/dist/controllers/version.js +12 -0
- package/dist/lib/app/bun-project.js +1 -1
- package/dist/lib/app/deploy-output.js +15 -0
- package/dist/lib/app/env-config.js +57 -0
- package/dist/lib/app/env-vars.js +4 -4
- package/dist/lib/app/local-dev.js +2 -1
- package/dist/lib/app/preview-build.js +130 -144
- package/dist/lib/app/preview-interaction.js +2 -35
- package/dist/lib/app/preview-progress.js +43 -58
- package/dist/lib/app/preview-provider.js +125 -24
- package/dist/lib/auth/auth-ops.js +58 -13
- package/dist/lib/auth/client.js +1 -1
- package/dist/lib/auth/guard.js +1 -1
- package/dist/lib/auth/login.js +115 -4
- package/dist/lib/project/local-pin.js +51 -0
- package/dist/lib/project/resolution.js +201 -0
- package/dist/lib/version.js +55 -0
- package/dist/output/patterns.js +15 -18
- package/dist/presenters/app-env.js +129 -0
- package/dist/presenters/app.js +16 -29
- package/dist/presenters/auth.js +2 -2
- package/dist/presenters/branch.js +6 -6
- package/dist/presenters/project.js +87 -44
- package/dist/presenters/version.js +29 -0
- package/dist/shell/command-meta.js +150 -84
- package/dist/shell/command-runner.js +32 -2
- package/dist/shell/errors.js +8 -3
- package/dist/shell/global-flags.js +13 -1
- package/dist/shell/help.js +8 -7
- package/dist/shell/output.js +29 -12
- package/dist/shell/prompt.js +12 -2
- package/dist/shell/runtime.js +1 -1
- package/dist/shell/ui.js +19 -1
- package/dist/use-cases/auth.js +9 -12
- package/dist/use-cases/branch.js +20 -20
- package/dist/use-cases/create-cli-gateways.js +3 -13
- package/dist/use-cases/project.js +2 -48
- package/package.json +3 -3
- package/dist/adapters/config.js +0 -74
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { padDisplay } from "../../shell/ui.js";
|
|
2
|
+
//#region src/lib/app/deploy-output.ts
|
|
3
|
+
const DEPLOY_OUTPUT_MIN_LABEL_WIDTH = 9;
|
|
4
|
+
const DEPLOY_OUTPUT_MIN_VALUE_WIDTH = 9;
|
|
5
|
+
function renderDeployOutputRows(ui, rows) {
|
|
6
|
+
if (rows.length === 0) return [];
|
|
7
|
+
const labelWidth = Math.max(DEPLOY_OUTPUT_MIN_LABEL_WIDTH, ...rows.map((row) => row.label.length));
|
|
8
|
+
const valueWidth = Math.max(DEPLOY_OUTPUT_MIN_VALUE_WIDTH, ...rows.map((row) => row.value?.length ?? 0));
|
|
9
|
+
return rows.map((row) => {
|
|
10
|
+
if (!row.value) return ` ${row.label}`;
|
|
11
|
+
return ` ${padDisplay(row.label, labelWidth)} ${padDisplay(ui.strong(row.value), valueWidth)}${row.origin ? ` ${ui.dim(`· ${row.origin}`)}` : ""}`.trimEnd();
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { renderDeployOutputRows };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { usageError } from "../../shell/errors.js";
|
|
2
|
+
//#region src/lib/app/env-config.ts
|
|
3
|
+
const VALID_ROLES = new Set(["production", "preview"]);
|
|
4
|
+
function positionalHint(command) {
|
|
5
|
+
if (command === "add" || command === "update") return "KEY=value ";
|
|
6
|
+
if (command === "rm") return "KEY ";
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
function resolveEnvScope(flags, options) {
|
|
10
|
+
if (flags.roleName) {
|
|
11
|
+
if (!VALID_ROLES.has(flags.roleName)) throw usageError(`Unknown role "${flags.roleName}"`, "--role accepts production or preview.", "Pass --role production or --role preview.", [`prisma-cli project env ${options.command} --role production`, `prisma-cli project env ${options.command} --role preview`], "app");
|
|
12
|
+
return {
|
|
13
|
+
kind: "role",
|
|
14
|
+
role: flags.roleName
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (options.requireExplicit) {
|
|
18
|
+
const positional = positionalHint(options.command);
|
|
19
|
+
throw usageError(`prisma-cli project env ${options.command} requires --role`, "Writing without an explicit scope is rejected so the command never silently targets production.", "Pass --role production or --role preview.", [`prisma-cli project env ${options.command} ${positional}--role production`, `prisma-cli project env ${options.command} ${positional}--role preview`], "app");
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function parseKeyValuePositional(raw, command, env = process.env) {
|
|
24
|
+
if (!raw) throw usageError(`prisma-cli project env ${command} requires KEY=VALUE`, "No KEY=VALUE positional argument was supplied.", "Pass the variable as KEY=VALUE, e.g. STRIPE_KEY=sk_test_xxx.", [`prisma-cli project env ${command} STRIPE_KEY=sk_test_xxx --role production`], "app");
|
|
25
|
+
const separatorIndex = raw.indexOf("=");
|
|
26
|
+
if (separatorIndex === -1) {
|
|
27
|
+
if (KEY_SHAPE.test(raw)) {
|
|
28
|
+
validateKey(raw, command);
|
|
29
|
+
const value = env[raw];
|
|
30
|
+
if (typeof value === "string" && value.length > 0) return {
|
|
31
|
+
key: raw,
|
|
32
|
+
value
|
|
33
|
+
};
|
|
34
|
+
throw usageError(`Value for "${raw}" was not provided`, `No KEY=VALUE assignment was supplied, and ${raw} is not set in the current environment.`, "Pass KEY=VALUE or export the variable before running the command.", [`prisma-cli project env ${command} ${raw}=value --role production`, `${raw}=value prisma-cli project env ${command} ${raw} --role production`], "app");
|
|
35
|
+
}
|
|
36
|
+
throw usageError(`KEY=VALUE argument is missing the = separator`, `"${raw}" does not contain an = character.`, "Pass the variable as KEY=VALUE, e.g. STRIPE_KEY=sk_test_xxx.", [`prisma-cli project env ${command} STRIPE_KEY=sk_test_xxx --role production`], "app");
|
|
37
|
+
}
|
|
38
|
+
const key = raw.slice(0, separatorIndex);
|
|
39
|
+
const value = raw.slice(separatorIndex + 1);
|
|
40
|
+
validateKey(key, command);
|
|
41
|
+
if (value.length === 0) throw usageError(`KEY=VALUE argument has an empty value`, `"${raw}" has an empty value after the = separator.`, `Pass a non-empty value, or use prisma-cli project env rm to remove a variable.`, [`prisma-cli project env ${command} ${key}=value --role production`], "app");
|
|
42
|
+
return {
|
|
43
|
+
key,
|
|
44
|
+
value
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const KEY_SHAPE = /^[A-Z_][A-Z0-9_]*$/;
|
|
48
|
+
function validateKey(key, command) {
|
|
49
|
+
if (key.length === 0) throw usageError(`Variable key cannot be empty`, "An empty key was passed.", "Pass an env-var key, e.g. STRIPE_KEY.", [`prisma-cli project env ${command} STRIPE_KEY${command === "rm" ? "" : "=value"} --role production`], "app");
|
|
50
|
+
if (key.length > 256) throw usageError(`Variable key "${key}" exceeds the 256-character limit`, "Env-var keys are capped at 256 characters by the platform.", "Use a shorter key.", [], "app");
|
|
51
|
+
if (!KEY_SHAPE.test(key)) throw usageError(`Variable key "${key}" must match the POSIX env-var shape`, "Keys must start with an uppercase letter or underscore and contain only uppercase letters, digits, and underscores.", "Rename the key to match [A-Z_][A-Z0-9_]*.", [`prisma-cli project env ${command} STRIPE_KEY${command === "rm" ? "" : "=value"} --role production`], "app");
|
|
52
|
+
}
|
|
53
|
+
function formatScopeLabel(scope) {
|
|
54
|
+
return scope.role;
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { formatScopeLabel, parseKeyValuePositional, resolveEnvScope };
|
package/dist/lib/app/env-vars.js
CHANGED
|
@@ -2,15 +2,15 @@ import { usageError } from "../../shell/errors.js";
|
|
|
2
2
|
//#region src/lib/app/env-vars.ts
|
|
3
3
|
function parseEnvAssignments(assignments, options) {
|
|
4
4
|
const values = assignments ?? [];
|
|
5
|
-
if (options.requireAtLeastOne && values.length === 0) throw usageError("At least one environment variable is required", `prisma app ${options.commandName} needs at least one --env NAME=VALUE flag in the current mode.`, `Pass one or more --env NAME=VALUE flags, for example prisma app ${options.commandName} --env DATABASE_URL=postgresql://example.`, [`prisma app ${options.commandName} --env DATABASE_URL=postgresql://example`], "app");
|
|
5
|
+
if (options.requireAtLeastOne && values.length === 0) throw usageError("At least one environment variable is required", `prisma-cli app ${options.commandName} needs at least one --env NAME=VALUE flag in the current mode.`, `Pass one or more --env NAME=VALUE flags, for example prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example.`, [`prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example`], "app");
|
|
6
6
|
const parsed = {};
|
|
7
7
|
const seen = /* @__PURE__ */ new Set();
|
|
8
8
|
for (const assignment of values) {
|
|
9
9
|
const separatorIndex = assignment.indexOf("=");
|
|
10
|
-
if (separatorIndex === -1) throw usageError("Environment variable assignment must use NAME=VALUE", "A provided --env flag is missing the = separator.", `Pass repeated --env NAME=VALUE flags, for example prisma app ${options.commandName} --env DATABASE_URL=postgresql://example.`, [`prisma app ${options.commandName} --env DATABASE_URL=postgresql://example`], "app");
|
|
10
|
+
if (separatorIndex === -1) throw usageError("Environment variable assignment must use NAME=VALUE", "A provided --env flag is missing the = separator.", `Pass repeated --env NAME=VALUE flags, for example prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example.`, [`prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example`], "app");
|
|
11
11
|
const name = assignment.slice(0, separatorIndex);
|
|
12
|
-
if (name.length === 0) throw usageError("Environment variable name is required", "A provided --env flag has an empty variable name.", `Pass repeated --env NAME=VALUE flags, for example prisma app ${options.commandName} --env DATABASE_URL=postgresql://example.`, [`prisma app ${options.commandName} --env DATABASE_URL=postgresql://example`], "app");
|
|
13
|
-
if (seen.has(name)) throw usageError(`Environment variable "${name}" was provided more than once`, "Each environment variable name may be set only once per command invocation.", `Remove the duplicate "${name}" assignment and rerun prisma app ${options.commandName}.`, [`prisma app ${options.commandName} --env ${name}=value`], "app");
|
|
12
|
+
if (name.length === 0) throw usageError("Environment variable name is required", "A provided --env flag has an empty variable name.", `Pass repeated --env NAME=VALUE flags, for example prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example.`, [`prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example`], "app");
|
|
13
|
+
if (seen.has(name)) throw usageError(`Environment variable "${name}" was provided more than once`, "Each environment variable name may be set only once per command invocation.", `Remove the duplicate "${name}" assignment and rerun prisma-cli app ${options.commandName}.`, [`prisma-cli app ${options.commandName} --env ${name}=value`], "app");
|
|
14
14
|
seen.add(name);
|
|
15
15
|
parsed[name] = assignment.slice(separatorIndex + 1);
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readBunPackageEntrypoint, readBunPackageJson, resolveBunEntrypoint } from "./bun-project.js";
|
|
2
|
-
import path from "node:path";
|
|
3
2
|
import { access } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
//#region src/lib/app/local-dev.ts
|
|
6
6
|
const NEXT_CONFIG_FILENAMES = [
|
|
@@ -12,6 +12,7 @@ const NEXT_CONFIG_FILENAMES = [
|
|
|
12
12
|
const DEFAULT_LOCAL_DEV_PORT = 3e3;
|
|
13
13
|
async function resolveLocalBuildType(appPath, buildType) {
|
|
14
14
|
if (buildType === "bun" || buildType === "nextjs") return buildType;
|
|
15
|
+
if (buildType !== "auto") return null;
|
|
15
16
|
return detectLocalBuildType(appPath);
|
|
16
17
|
}
|
|
17
18
|
async function detectLocalBuildType(appPath) {
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { resolveBunEntrypoint } from "./bun-project.js";
|
|
2
|
+
import { chmod, copyFile, cp, lstat, mkdir, readFile, readdir, readlink, rm, stat } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import { execFile } from "node:child_process";
|
|
6
|
-
import { BunBuild } from "@prisma/compute-sdk";
|
|
4
|
+
import { AstroBuild, BunBuild, NextjsBuild, NuxtBuild, TanstackStartBuild } from "@prisma/compute-sdk";
|
|
7
5
|
//#region src/lib/app/preview-build.ts
|
|
8
|
-
const
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
6
|
+
const PREVIEW_BUILD_TYPES = [
|
|
7
|
+
"auto",
|
|
8
|
+
"bun",
|
|
9
|
+
"nextjs",
|
|
10
|
+
"nuxt",
|
|
11
|
+
"astro",
|
|
12
|
+
"tanstack-start"
|
|
13
13
|
];
|
|
14
|
+
const RESOLVED_PREVIEW_BUILD_TYPES = PREVIEW_BUILD_TYPES.filter((buildType) => buildType !== "auto");
|
|
14
15
|
var PreviewBuildStrategy = class {
|
|
15
16
|
#appPath;
|
|
16
17
|
#entrypoint;
|
|
@@ -45,6 +46,7 @@ async function executePreviewBuild(options) {
|
|
|
45
46
|
});
|
|
46
47
|
const artifact = await strategy.execute();
|
|
47
48
|
try {
|
|
49
|
+
if (buildType === "nextjs") await restageNextjsArtifact(artifact.directory, options.appPath);
|
|
48
50
|
await normalizeArtifactSymlinks(artifact.directory, options.appPath);
|
|
49
51
|
return {
|
|
50
52
|
artifact,
|
|
@@ -56,134 +58,115 @@ async function executePreviewBuild(options) {
|
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
async function resolvePreviewBuildStrategy(options) {
|
|
59
|
-
if (options.buildType
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
if (options.buildType !== "auto") {
|
|
62
|
+
const strategy = await createPreviewBuildStrategy({
|
|
63
|
+
appPath: options.appPath,
|
|
64
|
+
entrypoint: options.entrypoint,
|
|
65
|
+
buildType: options.buildType
|
|
66
|
+
});
|
|
65
67
|
return {
|
|
66
|
-
buildType:
|
|
67
|
-
strategy
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
buildType: options.buildType,
|
|
69
|
+
strategy
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
for (const buildType of RESOLVED_PREVIEW_BUILD_TYPES) {
|
|
73
|
+
if (buildType === "bun") continue;
|
|
74
|
+
const strategy = await createPreviewBuildStrategy({
|
|
75
|
+
appPath: options.appPath,
|
|
76
|
+
entrypoint: options.entrypoint,
|
|
77
|
+
buildType
|
|
78
|
+
});
|
|
79
|
+
if (await strategy.canBuild()) return {
|
|
80
|
+
buildType,
|
|
81
|
+
strategy
|
|
71
82
|
};
|
|
72
83
|
}
|
|
73
|
-
const nextjsStrategy = new PreviewNextjsBuild({ appPath: options.appPath });
|
|
74
|
-
if (await nextjsStrategy.canBuild()) return {
|
|
75
|
-
buildType: "nextjs",
|
|
76
|
-
strategy: nextjsStrategy
|
|
77
|
-
};
|
|
78
|
-
const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint);
|
|
79
84
|
return {
|
|
80
85
|
buildType: "bun",
|
|
81
|
-
strategy:
|
|
86
|
+
strategy: await createPreviewBuildStrategy({
|
|
82
87
|
appPath: options.appPath,
|
|
83
|
-
entrypoint
|
|
88
|
+
entrypoint: options.entrypoint,
|
|
89
|
+
buildType: "bun"
|
|
84
90
|
})
|
|
85
91
|
};
|
|
86
92
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!(await stat(standaloneDir).catch(() => null))?.isDirectory()) throw new Error("Next.js build did not produce standalone output. Add output: \"standalone\" to your next.config file.");
|
|
99
|
-
const outDir = await mkdtemp(path.join(os.tmpdir(), "compute-build-"));
|
|
100
|
-
try {
|
|
101
|
-
const artifactDir = path.join(outDir, "app");
|
|
102
|
-
await stageNextjsStandaloneArtifact({
|
|
103
|
-
standaloneDir,
|
|
104
|
-
artifactDir,
|
|
105
|
-
appPath: this.#appPath
|
|
106
|
-
});
|
|
107
|
-
const publicDir = path.join(this.#appPath, "public");
|
|
108
|
-
if (await directoryExists(publicDir)) await cp(publicDir, path.join(artifactDir, "public"), { recursive: true });
|
|
109
|
-
const staticDir = path.join(this.#appPath, ".next", "static");
|
|
110
|
-
if (await directoryExists(staticDir)) await cp(staticDir, path.join(artifactDir, ".next", "static"), { recursive: true });
|
|
111
|
-
return {
|
|
112
|
-
directory: artifactDir,
|
|
113
|
-
entrypoint: "server.js",
|
|
114
|
-
defaultPortMapping: { http: 3e3 },
|
|
115
|
-
cleanup: () => rm(outDir, {
|
|
116
|
-
recursive: true,
|
|
117
|
-
force: true
|
|
118
|
-
})
|
|
119
|
-
};
|
|
120
|
-
} catch (error) {
|
|
121
|
-
await rm(outDir, {
|
|
122
|
-
recursive: true,
|
|
123
|
-
force: true
|
|
93
|
+
async function createPreviewBuildStrategy(options) {
|
|
94
|
+
switch (options.buildType) {
|
|
95
|
+
case "nextjs": return new NextjsBuild({ appPath: options.appPath });
|
|
96
|
+
case "nuxt": return new NuxtBuild({ appPath: options.appPath });
|
|
97
|
+
case "astro": return new AstroBuild({ appPath: options.appPath });
|
|
98
|
+
case "tanstack-start": return new TanstackStartBuild({ appPath: options.appPath });
|
|
99
|
+
case "bun": {
|
|
100
|
+
const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint);
|
|
101
|
+
return new BunBuild({
|
|
102
|
+
appPath: options.appPath,
|
|
103
|
+
entrypoint
|
|
124
104
|
});
|
|
125
|
-
throw error;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
async #hasNextConfig() {
|
|
129
|
-
let entries;
|
|
130
|
-
try {
|
|
131
|
-
entries = await readdir(this.#appPath);
|
|
132
|
-
} catch {
|
|
133
|
-
return false;
|
|
134
105
|
}
|
|
135
|
-
return entries.some((entry) => NEXT_CONFIG_FILENAMES.includes(entry));
|
|
136
106
|
}
|
|
137
|
-
|
|
138
|
-
const packageJsonPath = path.join(this.#appPath, "package.json");
|
|
139
|
-
let content;
|
|
140
|
-
try {
|
|
141
|
-
content = await readFile(packageJsonPath, "utf8");
|
|
142
|
-
} catch {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
let parsed;
|
|
146
|
-
try {
|
|
147
|
-
parsed = JSON.parse(content);
|
|
148
|
-
} catch {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
const deps = isRecord(parsed.dependencies) ? parsed.dependencies : {};
|
|
152
|
-
const devDeps = isRecord(parsed.devDependencies) ? parsed.devDependencies : {};
|
|
153
|
-
return "next" in deps || "next" in devDeps;
|
|
154
|
-
}
|
|
155
|
-
async #runBuild() {
|
|
156
|
-
const candidates = [
|
|
157
|
-
{
|
|
158
|
-
command: path.join(this.#appPath, "node_modules", ".bin", "next"),
|
|
159
|
-
args: ["build"]
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
command: "npx",
|
|
163
|
-
args: ["next", "build"]
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
command: "bunx",
|
|
167
|
-
args: ["next", "build"]
|
|
168
|
-
}
|
|
169
|
-
];
|
|
170
|
-
for (const { command, args } of candidates) try {
|
|
171
|
-
await exec(command, args, this.#appPath);
|
|
172
|
-
return;
|
|
173
|
-
} catch (error) {
|
|
174
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") continue;
|
|
175
|
-
throw error;
|
|
176
|
-
}
|
|
177
|
-
throw new Error("Could not find the Next.js CLI. Install it with `npm install next` or ensure npx/bunx is available.");
|
|
178
|
-
}
|
|
179
|
-
};
|
|
107
|
+
}
|
|
180
108
|
async function stageNextjsStandaloneArtifact(options) {
|
|
181
109
|
const standaloneRoot = path.resolve(options.standaloneDir);
|
|
182
|
-
|
|
110
|
+
const artifactRoot = path.resolve(options.artifactDir);
|
|
111
|
+
const appRoot = path.resolve(options.appPath);
|
|
112
|
+
await copyPathMaterializingSymlinks(standaloneRoot, artifactRoot, {
|
|
183
113
|
standaloneRoot,
|
|
184
|
-
appRoot
|
|
114
|
+
appRoot,
|
|
115
|
+
sourceRoot: await resolveSourceRoot(appRoot)
|
|
116
|
+
});
|
|
117
|
+
await hoistPnpmDependencies(path.join(artifactRoot, "node_modules"));
|
|
118
|
+
}
|
|
119
|
+
async function restageNextjsArtifact(artifactDir, appPath) {
|
|
120
|
+
const standaloneDir = path.join(appPath, ".next", "standalone");
|
|
121
|
+
await rm(artifactDir, {
|
|
122
|
+
recursive: true,
|
|
123
|
+
force: true
|
|
124
|
+
});
|
|
125
|
+
await stageNextjsStandaloneArtifact({
|
|
126
|
+
standaloneDir,
|
|
127
|
+
artifactDir,
|
|
128
|
+
appPath
|
|
129
|
+
});
|
|
130
|
+
const publicDir = path.join(appPath, "public");
|
|
131
|
+
if (await directoryExists(publicDir)) await cp(publicDir, path.join(artifactDir, "public"), {
|
|
132
|
+
recursive: true,
|
|
133
|
+
verbatimSymlinks: true
|
|
134
|
+
});
|
|
135
|
+
const staticDir = path.join(appPath, ".next", "static");
|
|
136
|
+
if (await directoryExists(staticDir)) await cp(staticDir, path.join(artifactDir, ".next", "static"), {
|
|
137
|
+
recursive: true,
|
|
138
|
+
verbatimSymlinks: true
|
|
185
139
|
});
|
|
186
140
|
}
|
|
141
|
+
async function hoistPnpmDependencies(nodeModulesDir) {
|
|
142
|
+
const pnpmNodeModulesDir = path.join(nodeModulesDir, ".pnpm", "node_modules");
|
|
143
|
+
if (!await directoryExists(pnpmNodeModulesDir)) return;
|
|
144
|
+
const entries = await readdir(pnpmNodeModulesDir, { withFileTypes: true });
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
const sourcePath = path.join(pnpmNodeModulesDir, entry.name);
|
|
147
|
+
if (entry.name.startsWith("@") && entry.isDirectory()) {
|
|
148
|
+
const scopedEntries = await readdir(sourcePath, { withFileTypes: true });
|
|
149
|
+
for (const scopedEntry of scopedEntries) {
|
|
150
|
+
const scopedDestination = path.join(nodeModulesDir, entry.name, scopedEntry.name);
|
|
151
|
+
if (await pathExists(scopedDestination)) continue;
|
|
152
|
+
await mkdir(path.dirname(scopedDestination), { recursive: true });
|
|
153
|
+
await copyPathMaterializingSymlinks(path.join(sourcePath, scopedEntry.name), scopedDestination, {
|
|
154
|
+
standaloneRoot: pnpmNodeModulesDir,
|
|
155
|
+
appRoot: nodeModulesDir,
|
|
156
|
+
sourceRoot: nodeModulesDir
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const destinationPath = path.join(nodeModulesDir, entry.name);
|
|
162
|
+
if (await pathExists(destinationPath)) continue;
|
|
163
|
+
await copyPathMaterializingSymlinks(sourcePath, destinationPath, {
|
|
164
|
+
standaloneRoot: pnpmNodeModulesDir,
|
|
165
|
+
appRoot: nodeModulesDir,
|
|
166
|
+
sourceRoot: nodeModulesDir
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
187
170
|
async function normalizeArtifactSymlinks(artifactDir, appPath) {
|
|
188
171
|
const normalizedArtifactDir = path.resolve(artifactDir);
|
|
189
172
|
const normalizedAppPath = path.resolve(appPath);
|
|
@@ -240,7 +223,7 @@ async function resolveSymlinkTarget(symlinkPath, options) {
|
|
|
240
223
|
const linkTarget = await readlink(symlinkPath);
|
|
241
224
|
const resolvedTarget = path.resolve(path.dirname(symlinkPath), linkTarget);
|
|
242
225
|
if (await pathExists(resolvedTarget)) {
|
|
243
|
-
if (!isPathWithin(options.appRoot, resolvedTarget)) throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`);
|
|
226
|
+
if (!isPathWithin(options.appRoot, resolvedTarget) && !isPathWithin(options.sourceRoot, resolvedTarget)) throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`);
|
|
244
227
|
return resolvedTarget;
|
|
245
228
|
}
|
|
246
229
|
if (isPathWithin(options.standaloneRoot, resolvedTarget)) {
|
|
@@ -249,28 +232,6 @@ async function resolveSymlinkTarget(symlinkPath, options) {
|
|
|
249
232
|
}
|
|
250
233
|
throw new Error(`Next.js standalone symlink target is missing: ${symlinkPath} -> ${linkTarget} (resolved to ${resolvedTarget})`);
|
|
251
234
|
}
|
|
252
|
-
async function directoryExists(dirPath) {
|
|
253
|
-
return (await stat(dirPath).catch(() => null))?.isDirectory() ?? false;
|
|
254
|
-
}
|
|
255
|
-
function exec(command, args, cwd) {
|
|
256
|
-
return new Promise((resolve, reject) => {
|
|
257
|
-
execFile(command, args, { cwd }, (error, _stdout, stderr) => {
|
|
258
|
-
if (error) {
|
|
259
|
-
if ("code" in error && error.code === "ENOENT") {
|
|
260
|
-
reject(Object.assign(/* @__PURE__ */ new Error(`${command} not found`), { code: "ENOENT" }));
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
const message = stderr.trim() || error.message;
|
|
264
|
-
reject(/* @__PURE__ */ new Error(`Next.js build failed:\n${message}`));
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
resolve();
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
function isRecord(value) {
|
|
272
|
-
return typeof value === "object" && value !== null;
|
|
273
|
-
}
|
|
274
235
|
async function pathExists(targetPath) {
|
|
275
236
|
try {
|
|
276
237
|
await stat(targetPath);
|
|
@@ -279,5 +240,30 @@ async function pathExists(targetPath) {
|
|
|
279
240
|
return false;
|
|
280
241
|
}
|
|
281
242
|
}
|
|
243
|
+
async function directoryExists(targetPath) {
|
|
244
|
+
try {
|
|
245
|
+
return (await stat(targetPath)).isDirectory();
|
|
246
|
+
} catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async function resolveSourceRoot(appRoot) {
|
|
251
|
+
let current = path.resolve(appRoot);
|
|
252
|
+
while (true) {
|
|
253
|
+
if (await pathExists(path.join(current, ".git")) || await pathExists(path.join(current, "pnpm-workspace.yaml")) || await pathExists(path.join(current, "bun.lock")) || await pathExists(path.join(current, "bun.lockb")) || await packageJsonDeclaresWorkspaces(current)) return current;
|
|
254
|
+
const parent = path.dirname(current);
|
|
255
|
+
if (parent === current) return path.resolve(appRoot);
|
|
256
|
+
current = parent;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function packageJsonDeclaresWorkspaces(directory) {
|
|
260
|
+
try {
|
|
261
|
+
const content = await readFile(path.join(directory, "package.json"), "utf8");
|
|
262
|
+
const parsed = JSON.parse(content);
|
|
263
|
+
return Boolean(parsed.workspaces);
|
|
264
|
+
} catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
282
268
|
//#endregion
|
|
283
|
-
export { PreviewBuildStrategy, executePreviewBuild };
|
|
269
|
+
export { PREVIEW_BUILD_TYPES, PreviewBuildStrategy, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild };
|
|
@@ -1,38 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "../../shell/prompt.js";
|
|
2
2
|
//#region src/lib/app/preview-interaction.ts
|
|
3
|
-
const CREATE_NEW_APP = "__create_new_app__";
|
|
4
3
|
const PREVIEW_DEFAULT_REGION = "eu-central-1";
|
|
5
|
-
function createPreviewDeployInteraction(context) {
|
|
6
|
-
return {
|
|
7
|
-
async selectService(services) {
|
|
8
|
-
const sorted = services.slice().sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id));
|
|
9
|
-
const selection = await selectPrompt({
|
|
10
|
-
input: context.runtime.stdin,
|
|
11
|
-
output: context.runtime.stderr,
|
|
12
|
-
message: "Select an app",
|
|
13
|
-
choices: [...sorted.map((service) => ({
|
|
14
|
-
label: service.name,
|
|
15
|
-
value: service.id
|
|
16
|
-
})), {
|
|
17
|
-
label: "Create a new app",
|
|
18
|
-
value: CREATE_NEW_APP
|
|
19
|
-
}]
|
|
20
|
-
});
|
|
21
|
-
return selection === CREATE_NEW_APP ? null : selection;
|
|
22
|
-
},
|
|
23
|
-
async provideServiceName() {
|
|
24
|
-
return textPrompt({
|
|
25
|
-
input: context.runtime.stdin,
|
|
26
|
-
output: context.runtime.stderr,
|
|
27
|
-
message: "App name",
|
|
28
|
-
placeholder: "hello-world",
|
|
29
|
-
validate: (value) => !value?.trim() ? "App name is required" : void 0
|
|
30
|
-
}).then((value) => value.trim());
|
|
31
|
-
},
|
|
32
|
-
async selectRegion(_regions) {
|
|
33
|
-
return PREVIEW_DEFAULT_REGION;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
4
|
//#endregion
|
|
38
|
-
export { PREVIEW_DEFAULT_REGION
|
|
5
|
+
export { PREVIEW_DEFAULT_REGION };
|
|
@@ -1,83 +1,68 @@
|
|
|
1
|
+
import { renderDeployOutputRows } from "./deploy-output.js";
|
|
1
2
|
//#region src/lib/app/preview-progress.ts
|
|
2
|
-
function
|
|
3
|
-
|
|
3
|
+
function createPreviewDeployProgressState() {
|
|
4
|
+
return {
|
|
5
|
+
buildStarted: false,
|
|
6
|
+
buildCompleted: false,
|
|
7
|
+
archiveReady: false,
|
|
8
|
+
uploadCompleted: false,
|
|
9
|
+
versionId: null,
|
|
10
|
+
startRequested: false,
|
|
11
|
+
containerLive: false,
|
|
12
|
+
deploymentUrl: null,
|
|
13
|
+
promotedUrl: null
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function createPreviewDeployProgress(output, ui, enabled, state = createPreviewDeployProgressState()) {
|
|
4
17
|
const write = (line) => {
|
|
18
|
+
if (!enabled) return;
|
|
5
19
|
output.write(`${line}\n`);
|
|
6
20
|
};
|
|
21
|
+
const writeRows = (rows) => {
|
|
22
|
+
for (const line of renderDeployOutputRows(ui, rows)) write(line);
|
|
23
|
+
};
|
|
7
24
|
return {
|
|
8
25
|
onBuildStart() {
|
|
9
|
-
|
|
26
|
+
state.buildStarted = true;
|
|
27
|
+
write("Building locally...");
|
|
10
28
|
},
|
|
11
29
|
onBuildComplete() {
|
|
12
|
-
|
|
13
|
-
},
|
|
14
|
-
onArchiveCreating() {
|
|
15
|
-
write("Creating deployment artifact...");
|
|
30
|
+
state.buildCompleted = true;
|
|
16
31
|
},
|
|
17
32
|
onArchiveReady(byteLength) {
|
|
18
|
-
|
|
33
|
+
state.archiveReady = true;
|
|
34
|
+
writeRows([{
|
|
35
|
+
label: "Built",
|
|
36
|
+
value: formatArtifactSize(byteLength)
|
|
37
|
+
}]);
|
|
38
|
+
},
|
|
39
|
+
onUploadStart() {
|
|
40
|
+
write("Uploading...");
|
|
19
41
|
},
|
|
20
42
|
onVersionCreated(versionId) {
|
|
21
|
-
|
|
43
|
+
state.versionId = versionId;
|
|
22
44
|
},
|
|
23
45
|
onUploadComplete() {
|
|
24
|
-
|
|
46
|
+
state.uploadCompleted = true;
|
|
47
|
+
writeRows([{ label: "Uploaded" }]);
|
|
25
48
|
},
|
|
26
49
|
onStartRequested() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
onStatusChange(status) {
|
|
30
|
-
write(`Status: ${status}`);
|
|
50
|
+
state.startRequested = true;
|
|
51
|
+
write("Deploying...");
|
|
31
52
|
},
|
|
32
53
|
onRunning(url) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
write("Deployment is running.");
|
|
38
|
-
},
|
|
39
|
-
onPromoteStart() {
|
|
40
|
-
write("Promoting deployment...");
|
|
54
|
+
state.containerLive = true;
|
|
55
|
+
state.deploymentUrl = url;
|
|
56
|
+
writeRows([{ label: "Deployed" }]);
|
|
41
57
|
},
|
|
42
58
|
onPromoted(url) {
|
|
43
|
-
|
|
44
|
-
write(`Promoted to ${url}.`);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
write("Promotion complete.");
|
|
48
|
-
},
|
|
49
|
-
onPromoteFailed(error) {
|
|
50
|
-
write(`Promotion failed${error?.message ? `: ${error.message}` : "."}`);
|
|
51
|
-
},
|
|
52
|
-
onOldVersionStopping(versionId) {
|
|
53
|
-
write(`Stopping previous deployment ${versionId}...`);
|
|
54
|
-
},
|
|
55
|
-
onOldVersionStopped(versionId) {
|
|
56
|
-
write(`Previous deployment ${versionId} stopped.`);
|
|
57
|
-
},
|
|
58
|
-
onOldVersionStopFailed(versionId) {
|
|
59
|
-
write(`Failed to stop previous deployment ${versionId} (non-fatal).`);
|
|
60
|
-
},
|
|
61
|
-
onOldVersionDeleting(versionId) {
|
|
62
|
-
write(`Deleting previous deployment ${versionId}...`);
|
|
63
|
-
},
|
|
64
|
-
onOldVersionDeleted(versionId) {
|
|
65
|
-
write(`Previous deployment ${versionId} deleted.`);
|
|
66
|
-
},
|
|
67
|
-
onOldVersionDeleteFailed(versionId) {
|
|
68
|
-
write(`Failed to delete previous deployment ${versionId} (non-fatal).`);
|
|
69
|
-
},
|
|
70
|
-
onCleanupDanglingVersion(versionId) {
|
|
71
|
-
write(`Cleaning up deployment ${versionId}...`);
|
|
72
|
-
},
|
|
73
|
-
onCleanupDanglingVersionComplete(versionId) {
|
|
74
|
-
write(`Deployment ${versionId} cleaned up.`);
|
|
75
|
-
},
|
|
76
|
-
onCleanupDanglingVersionFailed(versionId) {
|
|
77
|
-
write(`Failed to clean up deployment ${versionId}.`);
|
|
59
|
+
state.promotedUrl = url;
|
|
78
60
|
}
|
|
79
61
|
};
|
|
80
62
|
}
|
|
63
|
+
function formatArtifactSize(byteLength) {
|
|
64
|
+
return `${(byteLength / 1024 / 1024).toFixed(1)} MB`;
|
|
65
|
+
}
|
|
81
66
|
function createPreviewPromoteProgress(output, enabled) {
|
|
82
67
|
if (!enabled) return;
|
|
83
68
|
const write = (line) => {
|
|
@@ -136,4 +121,4 @@ function createPreviewUpdateEnvProgress(output, enabled) {
|
|
|
136
121
|
};
|
|
137
122
|
}
|
|
138
123
|
//#endregion
|
|
139
|
-
export { createPreviewDeployProgress, createPreviewPromoteProgress, createPreviewUpdateEnvProgress };
|
|
124
|
+
export { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress, createPreviewUpdateEnvProgress };
|