@prisma/compute-sdk 0.28.0 → 0.29.0

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 (75) hide show
  1. package/dist/astro-build.d.ts +2 -0
  2. package/dist/astro-build.d.ts.map +1 -1
  3. package/dist/astro-build.js +31 -39
  4. package/dist/astro-build.js.map +1 -1
  5. package/dist/build-settings.d.ts +3 -0
  6. package/dist/build-settings.d.ts.map +1 -1
  7. package/dist/build-settings.js +13 -0
  8. package/dist/build-settings.js.map +1 -1
  9. package/dist/build-strategy.d.ts +20 -0
  10. package/dist/build-strategy.d.ts.map +1 -1
  11. package/dist/build-strategy.js +33 -1
  12. package/dist/build-strategy.js.map +1 -1
  13. package/dist/build.d.ts +2 -1
  14. package/dist/build.d.ts.map +1 -1
  15. package/dist/build.js +16 -4
  16. package/dist/build.js.map +1 -1
  17. package/dist/bun-build.d.ts +1 -1
  18. package/dist/bun-build.d.ts.map +1 -1
  19. package/dist/bun-build.js +25 -9
  20. package/dist/bun-build.js.map +1 -1
  21. package/dist/config/frameworks.d.ts +4 -8
  22. package/dist/config/frameworks.d.ts.map +1 -1
  23. package/dist/config/frameworks.js +17 -5
  24. package/dist/config/frameworks.js.map +1 -1
  25. package/dist/config/normalize.d.ts +6 -0
  26. package/dist/config/normalize.d.ts.map +1 -1
  27. package/dist/config/normalize.js +53 -12
  28. package/dist/config/normalize.js.map +1 -1
  29. package/dist/config/types.d.ts +3 -1
  30. package/dist/config/types.d.ts.map +1 -1
  31. package/dist/config/types.js +1 -0
  32. package/dist/config/types.js.map +1 -1
  33. package/dist/configured-artifact.d.ts +20 -0
  34. package/dist/configured-artifact.d.ts.map +1 -0
  35. package/dist/configured-artifact.js +104 -0
  36. package/dist/configured-artifact.js.map +1 -0
  37. package/dist/custom-build.d.ts +20 -0
  38. package/dist/custom-build.d.ts.map +1 -0
  39. package/dist/custom-build.js +53 -0
  40. package/dist/custom-build.js.map +1 -0
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/nestjs-build.d.ts.map +1 -1
  46. package/dist/nestjs-build.js +28 -29
  47. package/dist/nestjs-build.js.map +1 -1
  48. package/dist/nextjs-build.d.ts +1 -1
  49. package/dist/nextjs-build.d.ts.map +1 -1
  50. package/dist/nextjs-build.js +25 -14
  51. package/dist/nextjs-build.js.map +1 -1
  52. package/dist/nuxt-build.d.ts +2 -0
  53. package/dist/nuxt-build.d.ts.map +1 -1
  54. package/dist/nuxt-build.js +31 -39
  55. package/dist/nuxt-build.js.map +1 -1
  56. package/dist/tanstack-start-build.d.ts +1 -1
  57. package/dist/tanstack-start-build.d.ts.map +1 -1
  58. package/dist/tanstack-start-build.js +26 -47
  59. package/dist/tanstack-start-build.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/astro-build.ts +39 -46
  62. package/src/build-settings.ts +16 -0
  63. package/src/build-strategy.ts +56 -1
  64. package/src/build.ts +16 -4
  65. package/src/bun-build.ts +39 -10
  66. package/src/config/frameworks.ts +18 -5
  67. package/src/config/normalize.ts +74 -13
  68. package/src/config/types.ts +3 -0
  69. package/src/configured-artifact.ts +169 -0
  70. package/src/custom-build.ts +73 -0
  71. package/src/index.ts +1 -0
  72. package/src/nestjs-build.ts +36 -33
  73. package/src/nextjs-build.ts +31 -14
  74. package/src/nuxt-build.ts +39 -46
  75. package/src/tanstack-start-build.ts +31 -53
@@ -1,15 +1,11 @@
1
- import { cp, mkdtemp, rm, stat } from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
-
5
- import { normalizeArtifactSymlinks } from "./artifact-postprocess.ts";
1
+ import { type BuildSettings, resolveBuildSettings } from "./build-settings.ts";
6
2
  import type { BuildArtifact, BuildStrategy } from "./build-strategy.ts";
7
3
  import {
8
4
  hasPackageDependency,
9
5
  hasRootFile,
10
- runPackageCli,
6
+ runBuildSettingsCommand,
11
7
  } from "./build-strategy.ts";
12
- import { defaultHttpPortForBuildType } from "./config/frameworks.ts";
8
+ import { stageConfiguredArtifact } from "./configured-artifact.ts";
13
9
  import type { BuildCommandIo } from "./workspace.ts";
14
10
 
15
11
  const ASTRO_CONFIG_FILENAMES = [
@@ -26,10 +22,16 @@ const ASTRO_CONFIG_FILENAMES = [
26
22
  */
27
23
  export class AstroBuild implements BuildStrategy {
28
24
  readonly #appPath: string;
25
+ readonly #buildSettings?: BuildSettings;
29
26
  readonly #io?: BuildCommandIo;
30
27
 
31
- constructor(options: { appPath: string; io?: BuildCommandIo }) {
28
+ constructor(options: {
29
+ appPath: string;
30
+ buildSettings?: BuildSettings;
31
+ io?: BuildCommandIo;
32
+ }) {
32
33
  this.#appPath = options.appPath;
34
+ this.#buildSettings = options.buildSettings;
33
35
  this.#io = options.io;
34
36
  }
35
37
 
@@ -42,48 +44,39 @@ export class AstroBuild implements BuildStrategy {
42
44
 
43
45
  async execute(signal?: AbortSignal): Promise<BuildArtifact> {
44
46
  signal?.throwIfAborted();
45
- await runPackageCli({
47
+ const settings =
48
+ this.#buildSettings ??
49
+ (await resolveBuildSettings({
50
+ appPath: this.#appPath,
51
+ buildType: "astro",
52
+ signal,
53
+ }));
54
+
55
+ await runBuildSettingsCommand({
46
56
  appPath: this.#appPath,
47
- cliName: "astro",
48
- args: ["build"],
57
+ settings,
49
58
  failurePrefix: "Astro",
50
- missingMessage:
51
- "Could not find the Astro CLI. Install it with `npm install astro` or ensure npx/bunx is available.",
52
- env: this.#io?.env,
53
- onOutput: this.#io?.onOutput,
59
+ defaultCli: {
60
+ source: "Astro default",
61
+ cliName: "astro",
62
+ args: ["build"],
63
+ missingMessage:
64
+ "Could not find the Astro CLI. Install it with `npm install astro` or ensure npx/bunx is available.",
65
+ },
66
+ io: this.#io,
54
67
  signal,
55
68
  });
56
69
 
57
- const distDir = path.join(this.#appPath, "dist");
58
- const entryPath = path.join(distDir, "server", "entry.mjs");
59
- const entryStat = await stat(entryPath).catch(() => null);
60
- if (!entryStat?.isFile()) {
61
- throw new Error(
62
- 'Astro build did not produce a standalone server entrypoint. Install @astrojs/node and configure it with adapter: node({ mode: "standalone" }) in your astro.config file.',
63
- );
64
- }
65
-
66
- const outDir = await mkdtemp(path.join(os.tmpdir(), "compute-build-"));
67
-
68
- try {
69
- const artifactDir = path.join(outDir, "app");
70
- await cp(distDir, artifactDir, {
71
- recursive: true,
72
- verbatimSymlinks: true,
73
- });
74
- // Materialize any symlinks into the app/workspace node_modules so the
75
- // artifact is self-contained once unpacked elsewhere.
76
- await normalizeArtifactSymlinks(artifactDir, this.#appPath, signal);
77
-
78
- return {
79
- directory: artifactDir,
80
- entrypoint: "server/entry.mjs",
81
- defaultPortMapping: { http: defaultHttpPortForBuildType("astro") },
82
- cleanup: () => rm(outDir, { recursive: true, force: true }),
83
- };
84
- } catch (error) {
85
- await rm(outDir, { recursive: true, force: true });
86
- throw error;
87
- }
70
+ return stageConfiguredArtifact({
71
+ appPath: this.#appPath,
72
+ outputDirectory: settings.outputDirectory,
73
+ entrypoint: settings.entrypoint ?? "server/entry.mjs",
74
+ buildType: "astro",
75
+ label: "Astro",
76
+ missingEntrypointMessage: settings.entrypoint
77
+ ? undefined
78
+ : 'Astro build did not produce a standalone server entrypoint. Install @astrojs/node and configure it with adapter: node({ mode: "standalone" }) in your astro.config file.',
79
+ signal,
80
+ });
88
81
  }
89
82
  }
@@ -22,6 +22,8 @@ export interface BuildSettings {
22
22
  buildCommandSource: string | null;
23
23
  outputDirectory: string;
24
24
  outputDirectorySource: string | null;
25
+ entrypoint?: string;
26
+ entrypointSource?: string | null;
25
27
  }
26
28
 
27
29
  /**
@@ -111,6 +113,13 @@ export async function resolveBuildSettings(options: {
111
113
  outputDirectorySource: "TanStack Start output",
112
114
  };
113
115
  }
116
+ case "custom":
117
+ return {
118
+ buildCommand: null,
119
+ buildCommandSource: null,
120
+ outputDirectory: ".",
121
+ outputDirectorySource: "app root",
122
+ };
114
123
  case "bun": {
115
124
  const manifest = await readPackageManifest(
116
125
  options.appPath,
@@ -143,6 +152,7 @@ export async function resolveConfiguredBuildSettings(options: {
143
152
  configured: {
144
153
  command: string | null | undefined;
145
154
  outputDirectory: string | undefined;
155
+ entrypoint?: string | undefined;
146
156
  };
147
157
  /** Label for configured values, e.g. the config file basename. */
148
158
  source: string;
@@ -169,6 +179,12 @@ export async function resolveConfiguredBuildSettings(options: {
169
179
  options.configured.outputDirectory !== undefined
170
180
  ? options.source
171
181
  : (fallback as BuildSettings).outputDirectorySource,
182
+ ...(options.configured.entrypoint !== undefined
183
+ ? {
184
+ entrypoint: options.configured.entrypoint,
185
+ entrypointSource: options.source,
186
+ }
187
+ : {}),
172
188
  };
173
189
  }
174
190
 
@@ -1,7 +1,13 @@
1
1
  import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import type { BuildSettings } from "./build-settings.ts";
3
4
  import type { PortMapping } from "./types.ts";
4
- import { buildCommandEnv, runChildProcess } from "./workspace.ts";
5
+ import {
6
+ type BuildCommandIo,
7
+ buildCommandEnv,
8
+ runBuildCommand,
9
+ runChildProcess,
10
+ } from "./workspace.ts";
5
11
 
6
12
  /**
7
13
  * The result of executing a build strategy.
@@ -207,6 +213,55 @@ export async function runPackageCli(opts: {
207
213
  throw new Error(opts.missingMessage);
208
214
  }
209
215
 
216
+ /**
217
+ * Runs the command described by build settings. A configured shell command runs
218
+ * as-is; a framework default can use the launcher ladder so local binaries,
219
+ * npx, and bunx all work consistently. `null` means "skip build".
220
+ */
221
+ export async function runBuildSettingsCommand(options: {
222
+ appPath: string;
223
+ settings: Pick<BuildSettings, "buildCommand" | "buildCommandSource">;
224
+ failurePrefix: string;
225
+ defaultCli?: {
226
+ source: string;
227
+ cliName: string;
228
+ args: string[];
229
+ missingMessage: string;
230
+ };
231
+ io?: BuildCommandIo;
232
+ signal?: AbortSignal;
233
+ }): Promise<void> {
234
+ if (!options.settings.buildCommand) {
235
+ return;
236
+ }
237
+
238
+ if (
239
+ options.defaultCli &&
240
+ options.settings.buildCommandSource === options.defaultCli.source
241
+ ) {
242
+ await runPackageCli({
243
+ appPath: options.appPath,
244
+ cliName: options.defaultCli.cliName,
245
+ args: options.defaultCli.args,
246
+ failurePrefix: options.failurePrefix,
247
+ missingMessage: options.defaultCli.missingMessage,
248
+ env: options.io?.env,
249
+ onOutput: options.io?.onOutput,
250
+ signal: options.signal,
251
+ });
252
+ return;
253
+ }
254
+
255
+ await runBuildCommand({
256
+ appPath: options.appPath,
257
+ command: options.settings.buildCommand,
258
+ failurePrefix: options.failurePrefix,
259
+ env: options.io?.env,
260
+ onOutput: options.io?.onOutput,
261
+ signal: options.signal,
262
+ });
263
+ }
264
+
210
265
  function isRecord(value: unknown): value is Record<string, unknown> {
211
266
  return typeof value === "object" && value !== null;
212
267
  }
package/src/build.ts CHANGED
@@ -3,6 +3,7 @@ import type { BuildSettings } from "./build-settings.ts";
3
3
  import type { BuildStrategy } from "./build-strategy.ts";
4
4
  import { BunBuild } from "./bun-build.ts";
5
5
  import { FRAMEWORKS, type FrameworkBuildType } from "./config/frameworks.ts";
6
+ import { CustomBuild } from "./custom-build.ts";
6
7
  import { NestjsBuild } from "./nestjs-build.ts";
7
8
  import { NextjsBuild } from "./nextjs-build.ts";
8
9
  import { NuxtBuild } from "./nuxt-build.ts";
@@ -33,12 +34,16 @@ const DETECTION_ORDER: FrameworkBuildType[] = (() => {
33
34
  const buildTypes = [
34
35
  ...new Set(FRAMEWORKS.map((framework) => framework.buildType)),
35
36
  ];
36
- return [...buildTypes.filter((type) => type !== "bun"), "bun"];
37
+ return [
38
+ ...buildTypes.filter((type) => type !== "bun" && type !== "custom"),
39
+ "bun",
40
+ ];
37
41
  })();
38
42
 
39
43
  /**
40
44
  * Constructs the build strategy for an explicit framework build type, wiring
41
- * through optional committed build settings and (for bun) the entrypoint.
45
+ * through optional committed build settings and entrypoints for strategies
46
+ * that need one.
42
47
  */
43
48
  export function createBuildStrategy(
44
49
  options: {
@@ -58,13 +63,20 @@ export function createBuildStrategy(
58
63
  requireStandalone: options.requireStandalone,
59
64
  });
60
65
  case "nuxt":
61
- return new NuxtBuild({ appPath, io });
66
+ return new NuxtBuild({ appPath, buildSettings, io });
62
67
  case "astro":
63
- return new AstroBuild({ appPath, io });
68
+ return new AstroBuild({ appPath, buildSettings, io });
64
69
  case "nestjs":
65
70
  return new NestjsBuild({ appPath, buildSettings, io });
66
71
  case "tanstack-start":
67
72
  return new TanstackStartBuild({ appPath, buildSettings, io });
73
+ case "custom":
74
+ return new CustomBuild({
75
+ appPath,
76
+ entrypoint: options.entrypoint,
77
+ buildSettings,
78
+ io,
79
+ });
68
80
  case "bun":
69
81
  return new BunBuild({
70
82
  appPath,
package/src/bun-build.ts CHANGED
@@ -9,12 +9,16 @@ import {
9
9
  import os from "node:os";
10
10
  import path from "node:path";
11
11
  import type { BuildSettings } from "./build-settings.ts";
12
- import type { BuildArtifact, BuildStrategy } from "./build-strategy.ts";
12
+ import {
13
+ type BuildArtifact,
14
+ type BuildStrategy,
15
+ runBuildSettingsCommand,
16
+ } from "./build-strategy.ts";
13
17
  import { defaultHttpPortForBuildType } from "./config/frameworks.ts";
18
+ import { stageConfiguredArtifact } from "./configured-artifact.ts";
14
19
  import {
15
20
  type BuildCommandIo,
16
21
  buildCommandEnv,
17
- runBuildCommand,
18
22
  runChildProcess,
19
23
  } from "./workspace.ts";
20
24
 
@@ -50,17 +54,36 @@ export class BunBuild implements BuildStrategy {
50
54
 
51
55
  async execute(signal?: AbortSignal): Promise<BuildArtifact> {
52
56
  signal?.throwIfAborted();
53
- if (this.#buildSettings?.buildCommand) {
54
- await runBuildCommand({
57
+ if (this.#buildSettings) {
58
+ await runBuildSettingsCommand({
55
59
  appPath: this.#appPath,
56
- command: this.#buildSettings.buildCommand,
60
+ settings: this.#buildSettings,
57
61
  failurePrefix: "Bun",
58
- env: this.#io?.env,
59
- onOutput: this.#io?.onOutput,
62
+ io: this.#io,
63
+ signal,
64
+ });
65
+ }
66
+
67
+ if (this.#buildSettings && this.#buildSettings.outputDirectory !== ".") {
68
+ if (!this.#buildSettings.entrypoint) {
69
+ throw new Error(
70
+ "Bun build entrypoint is required when outputDirectory is configured",
71
+ );
72
+ }
73
+ return stageConfiguredArtifact({
74
+ appPath: this.#appPath,
75
+ outputDirectory: this.#buildSettings.outputDirectory,
76
+ entrypoint: this.#buildSettings.entrypoint,
77
+ buildType: "bun",
78
+ label: "Bun",
60
79
  signal,
61
80
  });
62
81
  }
63
- const entrypoint = await this.#resolveEntrypoint(signal);
82
+
83
+ const entrypoint = await this.#resolveEntrypoint(
84
+ signal,
85
+ this.#buildSettings?.entrypoint,
86
+ );
64
87
 
65
88
  const outDir = await mkdtemp(path.join(os.tmpdir(), "compute-build-"));
66
89
  const bundleDir = path.join(outDir, "bundle");
@@ -94,9 +117,15 @@ export class BunBuild implements BuildStrategy {
94
117
  };
95
118
  }
96
119
 
97
- async #resolveEntrypoint(signal?: AbortSignal): Promise<string> {
120
+ async #resolveEntrypoint(
121
+ signal?: AbortSignal,
122
+ configuredEntrypoint?: string,
123
+ ): Promise<string> {
98
124
  signal?.throwIfAborted();
99
- const candidate = this.#entrypoint ?? (await this.#readPackageJsonMain());
125
+ const candidate =
126
+ this.#entrypoint ??
127
+ configuredEntrypoint ??
128
+ (await this.#readPackageJsonMain());
100
129
 
101
130
  if (!candidate) {
102
131
  throw new Error(
@@ -13,6 +13,7 @@ export type FrameworkBuildType =
13
13
  | "astro"
14
14
  | "nestjs"
15
15
  | "tanstack-start"
16
+ | "custom"
16
17
  | "bun";
17
18
 
18
19
  export interface FrameworkDescriptor {
@@ -138,6 +139,18 @@ export const FRAMEWORKS: readonly FrameworkDescriptor[] = [
138
139
  hasLocalDevServer: false,
139
140
  defaultHttpPort: 3000,
140
141
  },
142
+ {
143
+ key: "custom",
144
+ displayName: "Custom",
145
+ buildType: "custom",
146
+ aliases: ["custom"],
147
+ detectPackages: [],
148
+ detectConfigFiles: [],
149
+ usesEntrypoint: false,
150
+ defaultEntrypoint: null,
151
+ hasLocalDevServer: false,
152
+ defaultHttpPort: 3000,
153
+ },
141
154
  {
142
155
  key: "bun",
143
156
  displayName: "Bun",
@@ -154,14 +167,14 @@ export const FRAMEWORKS: readonly FrameworkDescriptor[] = [
154
167
 
155
168
  export const FRAMEWORK_KEYS = FRAMEWORKS.map((framework) => framework.key);
156
169
 
157
- /**
158
- * Build types whose preview build consumes committed build settings. The
159
- * others (nuxt, astro) run their framework CLI and stage fixed output, so a
160
- * config `build` block has nothing to apply to.
161
- */
170
+ /** Build types whose preview build consumes committed build settings. */
162
171
  export const CONFIG_BACKED_BUILD_TYPES = [
163
172
  "nextjs",
173
+ "nuxt",
174
+ "astro",
175
+ "nestjs",
164
176
  "tanstack-start",
177
+ "custom",
165
178
  "bun",
166
179
  ] as const satisfies readonly FrameworkBuildType[];
167
180
 
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
 
3
3
  import { Result, TaggedError } from "better-result";
4
4
 
5
- import { frameworkByKey, isConfigBackedBuildType } from "./frameworks.ts";
5
+ import { frameworkByKey } from "./frameworks.ts";
6
6
  import { COMPUTE_FRAMEWORKS, type ComputeFramework } from "./types.ts";
7
7
 
8
8
  export interface ComputeDeployTargetBuild {
@@ -10,6 +10,11 @@ export interface ComputeDeployTargetBuild {
10
10
  command: string | null | undefined;
11
11
  /** Normalized output path relative to the app root, undefined when not configured. */
12
12
  outputDirectory: string | undefined;
13
+ /**
14
+ * Normalized configured entrypoint. With outputDirectory it is relative to
15
+ * that output directory; otherwise it is relative to the app root.
16
+ */
17
+ entrypoint: string | undefined;
13
18
  }
14
19
 
15
20
  export interface ComputeDeployTarget {
@@ -19,6 +24,7 @@ export interface ComputeDeployTarget {
19
24
  /** Normalized app directory relative to the config file, or null for the config directory. */
20
25
  root: string | null;
21
26
  framework: ComputeFramework | null;
27
+ /** Runtime/source entrypoint normalized from `entry` or an entrypoint-backed `build.entrypoint`. */
22
28
  entry: string | null;
23
29
  httpPort: number | null;
24
30
  /** Env inputs in deploy order: dotenv file paths first, then NAME=VALUE assignments. */
@@ -104,7 +110,7 @@ const KNOWN_APP_KEYS = [
104
110
  "build",
105
111
  ] as const;
106
112
  const KNOWN_ENV_KEYS = ["file", "vars"] as const;
107
- const KNOWN_BUILD_KEYS = ["command", "outputDirectory"] as const;
113
+ const KNOWN_BUILD_KEYS = ["command", "outputDirectory", "entrypoint"] as const;
108
114
 
109
115
  /**
110
116
  * Validates and normalizes a config module's default export. Reports every
@@ -343,7 +349,8 @@ function normalizeAppEntry(
343
349
  }
344
350
  }
345
351
 
346
- if (entry && framework && !frameworkByKey(framework).usesEntrypoint) {
352
+ const frameworkDescriptor = framework ? frameworkByKey(framework) : null;
353
+ if (entry && frameworkDescriptor && !frameworkDescriptor.usesEntrypoint) {
347
354
  issues.push(
348
355
  `\`${label}.entry\` is not supported with the ${framework} framework; it derives its entrypoint from build output.`,
349
356
  );
@@ -367,22 +374,53 @@ function normalizeAppEntry(
367
374
 
368
375
  const envInputs = normalizeEnvConfig(value.env, `${label}.env`, issues);
369
376
  const build = normalizeBuildConfig(value.build, `${label}.build`, issues);
377
+ if (framework === "custom") {
378
+ if (!build) {
379
+ issues.push(
380
+ `\`${label}.build\` is required with the custom framework and must set \`outputDirectory\` and \`entrypoint\`.`,
381
+ );
382
+ } else {
383
+ if (!build.outputDirectory) {
384
+ issues.push(
385
+ `\`${label}.build.outputDirectory\` is required with the custom framework.`,
386
+ );
387
+ }
388
+ if (!build.entrypoint) {
389
+ issues.push(
390
+ `\`${label}.build.entrypoint\` is required with the custom framework.`,
391
+ );
392
+ }
393
+ }
394
+ } else if (
395
+ build?.outputDirectory &&
396
+ build.outputDirectory !== "." &&
397
+ !build.entrypoint &&
398
+ frameworkDescriptor?.usesEntrypoint
399
+ ) {
400
+ issues.push(
401
+ `\`${label}.build.entrypoint\` is required when \`${label}.build.outputDirectory\` is set with the ${framework} framework.`,
402
+ );
403
+ }
370
404
  if (
371
- build &&
372
- framework &&
373
- !isConfigBackedBuildType(frameworkByKey(framework).buildType)
405
+ entry &&
406
+ build?.entrypoint &&
407
+ frameworkDescriptor?.usesEntrypoint &&
408
+ entry !== build.entrypoint
374
409
  ) {
375
410
  issues.push(
376
- `\`${label}.build\` is not supported with the ${framework} framework; its build runs automatically during deploy.`,
411
+ `\`${label}.entry\` and \`${label}.build.entrypoint\` both configure the runtime entrypoint; set only one, or make them match.`,
377
412
  );
378
413
  }
379
-
380
414
  return {
381
415
  key,
382
416
  name,
383
417
  root,
384
418
  framework,
385
- entry,
419
+ entry:
420
+ entry ??
421
+ (frameworkDescriptor?.usesEntrypoint || !frameworkDescriptor
422
+ ? (build?.entrypoint ?? null)
423
+ : null),
386
424
  httpPort,
387
425
  envInputs,
388
426
  build,
@@ -400,7 +438,7 @@ function normalizeBuildConfig(
400
438
 
401
439
  if (!isPlainObject(value)) {
402
440
  issues.push(
403
- `\`${label}\` must be an object with \`command\` and/or \`outputDirectory\`.`,
441
+ `\`${label}\` must be an object with \`command\`, \`outputDirectory\`, and/or \`entrypoint\`.`,
404
442
  );
405
443
  return null;
406
444
  }
@@ -444,14 +482,37 @@ function normalizeBuildConfig(
444
482
  }
445
483
  }
446
484
 
447
- if (command === undefined && outputDirectory === undefined) {
485
+ let entrypoint: string | undefined;
486
+ if (value.entrypoint !== undefined) {
487
+ const normalized =
488
+ typeof value.entrypoint === "string"
489
+ ? normalizeRelativePath(value.entrypoint)
490
+ : undefined;
491
+ if (!normalized) {
492
+ issues.push(
493
+ `\`${label}.entrypoint\` must be a relative path that stays inside its base directory.`,
494
+ );
495
+ } else if (normalized === ".") {
496
+ issues.push(
497
+ `\`${label}.entrypoint\` must be a relative file path inside its base directory.`,
498
+ );
499
+ } else {
500
+ entrypoint = normalized;
501
+ }
502
+ }
503
+
504
+ if (
505
+ command === undefined &&
506
+ outputDirectory === undefined &&
507
+ entrypoint === undefined
508
+ ) {
448
509
  issues.push(
449
- `\`${label}\` must set \`command\` and/or \`outputDirectory\`.`,
510
+ `\`${label}\` must set \`command\`, \`outputDirectory\`, and/or \`entrypoint\`.`,
450
511
  );
451
512
  return null;
452
513
  }
453
514
 
454
- return { command, outputDirectory };
515
+ return { command, outputDirectory, entrypoint };
455
516
  }
456
517
 
457
518
  function normalizeEnvConfig(
@@ -13,6 +13,7 @@ export const COMPUTE_FRAMEWORKS = [
13
13
  "hono",
14
14
  "nestjs",
15
15
  "tanstack-start",
16
+ "custom",
16
17
  "bun",
17
18
  ] as const;
18
19
 
@@ -34,6 +35,8 @@ export interface ComputeBuildConfig {
34
35
  command?: string | null;
35
36
  /** Framework output path relative to the app root, e.g. ".next/standalone". */
36
37
  outputDirectory?: string;
38
+ /** Entrypoint for the built artifact, relative to `outputDirectory` when one is set. */
39
+ entrypoint?: string;
37
40
  }
38
41
 
39
42
  export interface ComputeAppConfig {