@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.
Files changed (55) hide show
  1. package/README.md +1 -16
  2. package/dist/adapters/git.js +49 -0
  3. package/dist/adapters/local-state.js +39 -1
  4. package/dist/cli2.js +60 -4
  5. package/dist/commands/app/index.js +43 -30
  6. package/dist/commands/auth/index.js +3 -2
  7. package/dist/commands/branch/index.js +2 -1
  8. package/dist/commands/env.js +87 -0
  9. package/dist/commands/git/index.js +36 -0
  10. package/dist/commands/project/index.js +12 -14
  11. package/dist/commands/version/index.js +18 -0
  12. package/dist/controllers/app-env.js +223 -0
  13. package/dist/controllers/app.js +1051 -173
  14. package/dist/controllers/auth.js +9 -9
  15. package/dist/controllers/branch.js +6 -6
  16. package/dist/controllers/project.js +451 -161
  17. package/dist/controllers/version.js +12 -0
  18. package/dist/lib/app/bun-project.js +1 -1
  19. package/dist/lib/app/deploy-output.js +15 -0
  20. package/dist/lib/app/env-config.js +57 -0
  21. package/dist/lib/app/env-vars.js +4 -4
  22. package/dist/lib/app/local-dev.js +2 -1
  23. package/dist/lib/app/preview-build.js +130 -144
  24. package/dist/lib/app/preview-interaction.js +2 -35
  25. package/dist/lib/app/preview-progress.js +43 -58
  26. package/dist/lib/app/preview-provider.js +125 -24
  27. package/dist/lib/auth/auth-ops.js +58 -13
  28. package/dist/lib/auth/client.js +1 -1
  29. package/dist/lib/auth/guard.js +1 -1
  30. package/dist/lib/auth/login.js +115 -4
  31. package/dist/lib/project/local-pin.js +51 -0
  32. package/dist/lib/project/resolution.js +201 -0
  33. package/dist/lib/version.js +55 -0
  34. package/dist/output/patterns.js +15 -18
  35. package/dist/presenters/app-env.js +129 -0
  36. package/dist/presenters/app.js +16 -29
  37. package/dist/presenters/auth.js +2 -2
  38. package/dist/presenters/branch.js +6 -6
  39. package/dist/presenters/project.js +87 -44
  40. package/dist/presenters/version.js +29 -0
  41. package/dist/shell/command-meta.js +150 -84
  42. package/dist/shell/command-runner.js +32 -2
  43. package/dist/shell/errors.js +8 -3
  44. package/dist/shell/global-flags.js +13 -1
  45. package/dist/shell/help.js +8 -7
  46. package/dist/shell/output.js +29 -12
  47. package/dist/shell/prompt.js +12 -2
  48. package/dist/shell/runtime.js +1 -1
  49. package/dist/shell/ui.js +19 -1
  50. package/dist/use-cases/auth.js +9 -12
  51. package/dist/use-cases/branch.js +20 -20
  52. package/dist/use-cases/create-cli-gateways.js +3 -13
  53. package/dist/use-cases/project.js +2 -48
  54. package/package.json +3 -3
  55. package/dist/adapters/config.js +0 -74
@@ -1,5 +1,5 @@
1
- import path from "node:path";
2
1
  import { access, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
3
  //#region src/lib/app/bun-project.ts
4
4
  async function readBunPackageJson(appPath) {
5
5
  const packageJsonPath = path.join(appPath, "package.json");
@@ -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 };
@@ -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 { chmod, copyFile, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm, stat } from "node:fs/promises";
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 NEXT_CONFIG_FILENAMES = [
9
- "next.config.js",
10
- "next.config.mjs",
11
- "next.config.ts",
12
- "next.config.mts"
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 === "nextjs") return {
60
- buildType: "nextjs",
61
- strategy: new PreviewNextjsBuild({ appPath: options.appPath })
62
- };
63
- if (options.buildType === "bun") {
64
- const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint);
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: "bun",
67
- strategy: new BunBuild({
68
- appPath: options.appPath,
69
- entrypoint
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: new BunBuild({
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
- var PreviewNextjsBuild = class {
88
- #appPath;
89
- constructor(options) {
90
- this.#appPath = options.appPath;
91
- }
92
- async canBuild() {
93
- return await this.#hasNextConfig() || await this.#hasNextDependency();
94
- }
95
- async execute() {
96
- await this.#runBuild();
97
- const standaloneDir = path.join(this.#appPath, ".next", "standalone");
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
- async #hasNextDependency() {
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
- await copyPathMaterializingSymlinks(standaloneRoot, path.resolve(options.artifactDir), {
110
+ const artifactRoot = path.resolve(options.artifactDir);
111
+ const appRoot = path.resolve(options.appPath);
112
+ await copyPathMaterializingSymlinks(standaloneRoot, artifactRoot, {
183
113
  standaloneRoot,
184
- appRoot: path.resolve(options.appPath)
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 { selectPrompt, textPrompt } from "../../shell/prompt.js";
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, createPreviewDeployInteraction };
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 createPreviewDeployProgress(output, enabled) {
3
- if (!enabled) return;
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
- write("Building application...");
26
+ state.buildStarted = true;
27
+ write("Building locally...");
10
28
  },
11
29
  onBuildComplete() {
12
- write("Build complete.");
13
- },
14
- onArchiveCreating() {
15
- write("Creating deployment artifact...");
30
+ state.buildCompleted = true;
16
31
  },
17
32
  onArchiveReady(byteLength) {
18
- write(`Artifact ready (${(byteLength / 1024).toFixed(1)} KB).`);
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
- write(`Deployment ${versionId} created.`);
43
+ state.versionId = versionId;
22
44
  },
23
45
  onUploadComplete() {
24
- write("Upload complete.");
46
+ state.uploadCompleted = true;
47
+ writeRows([{ label: "Uploaded" }]);
25
48
  },
26
49
  onStartRequested() {
27
- write("Starting deployment...");
28
- },
29
- onStatusChange(status) {
30
- write(`Status: ${status}`);
50
+ state.startRequested = true;
51
+ write("Deploying...");
31
52
  },
32
53
  onRunning(url) {
33
- if (url) {
34
- write(`Deployment is running at ${url}.`);
35
- return;
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
- if (url) {
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 };