Skip to content

Commit 3a25c73

Browse files
feat(cli): support custom compute build config (#98)
## Summary Adds Prisma CLI support for custom Compute build config. This lets users describe how an app should be built and staged in `prisma.compute.ts` when the app does not fit one of the first-class framework strategies yet. Companion PRs: - prisma/project-compute#97 - prisma/pdp-control-plane#4310 The SDK support has been released, so this PR depends on `@prisma/compute-sdk@^0.29.0` and includes the lockfile update. ## What changed - Uses the SDK framework registry for `app build --build-type` choices instead of duplicating the list in the CLI. - Adds `custom` to CLI build result typing. - Passes `prisma.compute.ts` `build.entrypoint` into configured build settings. - Shows configured build entrypoints in deploy build-settings output. - Reports the effective deploy entrypoint from configured build settings when no source `--entry` is used. - Updates config tests for `custom`, framework-owned build blocks, and `build.entrypoint` normalization. - Updates product docs for `build.entrypoint`, `framework: "custom"`, and custom artifact builds. ## User-facing behavior A custom artifact can be deployed through `prisma.compute.ts` by providing: - `framework: "custom"` - `build.outputDirectory` - `build.entrypoint` `build.command` is optional. Omit it when the artifact is already built, or set it when the CLI should run a build before staging the artifact. Example single-app config: ```ts import { defineComputeConfig } from "@prisma/compute-sdk/config"; export default defineComputeConfig({ app: { name: "web", framework: "custom", httpPort: 3000, build: { command: "npm run build", outputDirectory: "dist", entrypoint: "server.js", }, }, }); ``` Users can then run: ```bash prisma-cli app build prisma-cli app deploy ``` Example prebuilt artifact config: ```ts import { defineComputeConfig } from "@prisma/compute-sdk/config"; export default defineComputeConfig({ app: { name: "web", framework: "custom", build: { outputDirectory: "dist", entrypoint: "server.js", }, }, }); ``` Example monorepo config: ```ts import { defineComputeConfig } from "@prisma/compute-sdk/config"; export default defineComputeConfig({ apps: { frontend: { root: "apps/frontend", framework: "custom", build: { command: "npm run build", outputDirectory: "dist", entrypoint: "server.js", }, }, api: { root: "apps/api", framework: "hono", entry: "src/index.ts", }, }, }); ``` ```bash prisma-cli app build frontend prisma-cli app deploy frontend ``` ## Validation - `pnpm --filter @prisma/cli test` - `pnpm --recursive exec tsc --noEmit` - `pnpm lint` `pnpm lint` exits 0 with existing warnings.
1 parent ca26012 commit 3a25c73

12 files changed

Lines changed: 226 additions & 68 deletions

File tree

docs/product/command-spec.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -879,7 +879,7 @@ Examples:
879879
prisma-cli database connection remove conn_123 --confirm conn_123
880880
```
881881

882-
## `prisma-cli app build [app] --entry <path> --build-type <auto|bun|nextjs|nuxt|astro|tanstack-start>`
882+
## `prisma-cli app build [app] --entry <path> --build-type <auto|bun|nextjs|nuxt|astro|nestjs|tanstack-start|custom>`
883883

884884
Purpose:
885885

@@ -889,7 +889,7 @@ Behavior:
889889

890890
- resolves the optional `[app]` target, app root, framework, and entrypoint from `prisma.compute.ts` exactly like `app deploy`; explicit `--entry` and a non-`auto` `--build-type` override the config
891891
- detects supported project shapes when `--build-type auto` is used and no config framework applies
892-
- supports Bun, Next.js, Nuxt, Astro, and TanStack Start app builds in the beta package
892+
- supports Bun, Next.js, Nuxt, Astro, NestJS, TanStack Start, and custom artifact app builds in the beta package
893893
- fails with `USAGE_ERROR` when framework detection is ambiguous
894894

895895
Examples:
@@ -898,9 +898,11 @@ Examples:
898898
prisma-cli app build --build-type nextjs
899899
prisma-cli app build --build-type nuxt
900900
prisma-cli app build --build-type astro
901+
prisma-cli app build --build-type nestjs
901902
prisma-cli app build --build-type tanstack-start
902903
prisma-cli app build --build-type bun --entry server.ts
903904
prisma-cli app build api
905+
prisma-cli app build frontend
904906
```
905907

906908
## `prisma-cli app run [app] --entry <path> --build-type <auto|bun|nextjs> --port <port>`
@@ -925,7 +927,7 @@ prisma-cli app run --build-type bun --entry server.ts --port 3000
925927
prisma-cli app run api
926928
```
927929

928-
## `prisma-cli app deploy [app] --project <id-or-name> --create-project <name> --app <name> --branch <name> --framework <nextjs|nuxt|astro|hono|tanstack-start|bun> --entry <path> --http-port <port> --env <name=value|file> --db --no-db --prod`
930+
## `prisma-cli app deploy [app] --project <id-or-name> --create-project <name> --app <name> --branch <name> --framework <nextjs|nuxt|astro|hono|nestjs|tanstack-start|custom|bun> --entry <path> --http-port <port> --env <name=value|file> --db --no-db --prod`
929931

930932
Purpose:
931933

@@ -943,8 +945,9 @@ Compute config file (`prisma.compute.ts`):
943945
- `apps` — a multi-app or monorepo repository, keyed by deploy target name
944946
- each app accepts `name`, `root`, `framework`, `entry`, `httpPort`, `env`, and `build`:
945947
- `env` is a dotenv file path, or `{ file, vars }` with file path(s) and inline assignments
946-
- `build` is `{ command, outputDirectory }`; both fields are optional and `command: null` skips the build step
947-
- `build` applies to frameworks whose preview build consumes committed settings (`nextjs`, `hono`, `tanstack-start`, `bun`); `nuxt` and `astro` builds run their framework CLI automatically, so a `build` block with those frameworks is a validation error (`BUILD_SETTINGS_UNSUPPORTED` when only detected at deploy time)
948+
- `build` is `{ command, outputDirectory, entrypoint }`; all fields are optional except where a framework requires enough information to stage a runnable artifact, and `command: null` skips the build step
949+
- `build.entrypoint` is the built artifact entrypoint when `outputDirectory` is set, and is the source entrypoint for Bun/Hono configs that do not set an output directory
950+
- `framework: "custom"` deploys a prebuilt or custom-built artifact and requires `build.outputDirectory` and `build.entrypoint`
948951
- when `build` is present, the compute config owns build settings for that app: fields it sets override framework defaults, fields it omits are inferred; without a `build` block, settings are inferred entirely, with their sources shown
949952
- the compute config does not declare databases in the current beta; database setup stays on the `--db`/`--no-db` flags (a future project-level `database` field is the reserved growth path, since databases are branch resources shared by every app on the branch)
950953

@@ -991,6 +994,15 @@ export default defineComputeConfig({
991994
apps: {
992995
web: { root: "apps/web", framework: "nextjs" },
993996
worker: { root: "apps/worker", framework: "bun", entry: "src/index.ts" },
997+
frontend: {
998+
root: "apps/frontend",
999+
framework: "custom",
1000+
build: {
1001+
command: "npm run build",
1002+
outputDirectory: "build",
1003+
entrypoint: "handler.js",
1004+
},
1005+
},
9941006
},
9951007
});
9961008
```
@@ -1030,6 +1042,7 @@ Behavior:
10301042
- build commands run with every `node_modules/.bin` between the app and the repository or workspace root on `PATH`, so hoisted workspace binaries resolve
10311043
- otherwise `Build Command` falls back to the framework default, such as `next build`
10321044
- `Output Directory` is a literal framework output path, such as `.next/standalone`, `.output`, or `.`
1045+
- `Entrypoint` is shown when the config or framework settings provide a concrete built artifact entrypoint
10331046
- `prisma.app.json` is legacy and never read or written; a leftover file that matches the resolved settings produces a deletion warning, an unparsable one is ignored with a warning, and one with custom values fails with `BUILD_SETTINGS_MIGRATION_REQUIRED` including the exact `build` block to move into `prisma.compute.ts`
10341047
- after setup, deploy prints `Deploying to <Project> / <Branch> / <App>`; later deploys print a compact target header such as `Deploying ./j1 to j1 / main / j1`
10351048
- deploy progress uses short stage copy (`Building locally...`, `Built <size>`, `Uploading...`, `Uploaded`, `Deploying...`, `Deployed`) and never prints `Status: running` or `Deployment is running at ...`
@@ -1052,6 +1065,7 @@ Behavior:
10521065
- `--env DATABASE_URL=...`, `--env DIRECT_URL=...`, or the same keys loaded from an env file suppress automatic database prompting; combining those database env vars with `--db` is rejected
10531066
- maps user-facing framework names to deploy build strategies
10541067
- does not accept `--build-command` or `--output-directory`; custom build settings live in the `build` block of `prisma.compute.ts`
1068+
- deploys arbitrary framework output with `framework: "custom"` when the config provides a built output directory and entrypoint; `build.command` is optional for prebuilt artifacts
10551069
- uses `src/index.ts` as the Hono deploy entrypoint when the app has no `package.json#main` or `package.json#module` and that file exists
10561070
- supports vanilla Bun apps with `--framework bun` using `package.json#main` or `package.json#module`, or with `--entry <path>`
10571071
- treats `--entry <path>` without `--framework` as a Bun app deploy

docs/product/error-conventions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ Recommended meanings:
239239
- `COMPUTE_CONFIG_TARGET_REQUIRED`: a multi-app compute config needs an `[app]` target and none was given or inferred
240240
- `COMPUTE_CONFIG_TARGET_UNKNOWN`: the `[app]` target matches no configured app
241241
- `BUILD_SETTINGS_MIGRATION_REQUIRED`: a legacy `prisma.app.json` contains custom build settings that must move into the `build` block of `prisma.compute.ts`
242-
- `BUILD_SETTINGS_UNSUPPORTED`: a compute config `build` block targets a framework whose strategy owns its build (nuxt, astro)
242+
- `BUILD_SETTINGS_UNSUPPORTED`: a compute config `build` block targets a framework whose SDK strategy does not consume committed build settings
243243
- `FRAMEWORK_NOT_DETECTED`: app deploy could not detect a supported Beta framework and no explicit framework/build type was provided
244244
- `DEPLOYMENT_NOT_FOUND`: requested deployment id does not exist
245245
- `NO_DEPLOYMENTS`: command resolved a branch or app but found no deployments

docs/product/resource-model.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Rules:
104104
- the runtime app service is scoped by branch in the platform model
105105
- the app may be selected or created as part of app deployment workflows
106106
- app selection is local CLI state when needed for the beta package
107-
- app build settings live in the `build` block of `prisma.compute.ts` (`command`, `outputDirectory`); `prisma.app.json` is legacy and no longer read
107+
- app build settings live in the `build` block of `prisma.compute.ts` (`command`, `outputDirectory`, `entrypoint`); `prisma.app.json` is legacy and no longer read
108108

109109
### Deployment
110110

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
},
4545
"dependencies": {
4646
"@clack/prompts": "^1.5.0",
47-
"@prisma/compute-sdk": "^0.28.0",
47+
"@prisma/compute-sdk": "^0.29.0",
4848
"@prisma/credentials-store": "^7.8.0",
4949
"@prisma/management-api-sdk": "^1.37.0",
5050
"better-result": "^2.9.2",

packages/cli/src/controllers/app.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
maybeSetupBranchDatabase,
3030
} from "../lib/app/branch-database-deploy";
3131
import {
32+
APP_BUILD_TYPE_LABELS,
3233
APP_BUILD_TYPES,
3334
type AppBuildSettings,
3435
type AppBuildSettingsResolution,
@@ -845,7 +846,8 @@ async function runSingleAppDeploy(
845846
name: framework.displayName,
846847
source: framework.annotation,
847848
},
848-
entrypoint: entrypoint ?? null,
849+
entrypoint:
850+
entrypoint ?? buildSettingsResolution.settings.entrypoint ?? null,
849851
httpPort: runtime.port,
850852
region: selectedApp.region ?? null,
851853
envVars: envVarNames(envVars),
@@ -4079,11 +4081,6 @@ function frameworkFromUserFacingValue(
40794081
};
40804082
}
40814083

4082-
/**
4083-
* The nuxt and astro strategies build with their framework CLI and stage
4084-
* fixed output, so a compute config `build` block has nothing to apply to.
4085-
* Erroring beats silently ignoring committed settings.
4086-
*/
40874084
function assertConfigBackedBuildSettings(
40884085
buildType: FrameworkBuildType,
40894086
): asserts buildType is ConfigBackedBuildType {
@@ -4185,6 +4182,15 @@ function maybeRenderDeployBuildSettings(
41854182
value: settings.outputDirectory,
41864183
origin: settings.outputDirectorySource ?? undefined,
41874184
},
4185+
...(settings.entrypoint
4186+
? [
4187+
{
4188+
label: "Entrypoint",
4189+
value: settings.entrypoint,
4190+
origin: settings.entrypointSource ?? undefined,
4191+
},
4192+
]
4193+
: []),
41884194
]).join("\n")}\n\n`,
41894195
);
41904196
}
@@ -4394,7 +4400,7 @@ function normalizeBuildType(
43944400

43954401
throw usageError(
43964402
`Unsupported build type "${requestedBuildType}"`,
4397-
`Only ${APP_BUILD_TYPES.join(", ")} are supported in the current preview.`,
4403+
`Only ${APP_BUILD_TYPE_LABELS} are supported in the current preview.`,
43984404
"Pass a supported --build-type value.",
43994405
getBuildTypeExamples("build"),
44004406
"app",
@@ -4844,22 +4850,18 @@ function isAutoBuildDetectionError(error: unknown): boolean {
48444850
}
48454851

48464852
function formatBuildTypeName(buildType: AppBuildType): string {
4847-
switch (buildType) {
4848-
case "nextjs":
4849-
return "Next.js";
4850-
case "nuxt":
4851-
return "Nuxt";
4852-
case "astro":
4853-
return "Astro";
4854-
case "nestjs":
4855-
return "NestJS";
4856-
case "tanstack-start":
4857-
return "TanStack Start";
4858-
case "bun":
4859-
return "Bun";
4860-
case "auto":
4861-
return "Auto";
4853+
if (buildType === "auto") {
4854+
return "Auto";
4855+
}
4856+
4857+
for (let index = FRAMEWORKS.length - 1; index >= 0; index -= 1) {
4858+
const framework = FRAMEWORKS[index];
4859+
if (framework?.buildType === buildType) {
4860+
return framework.displayName;
4861+
}
48624862
}
4863+
4864+
return buildType;
48634865
}
48644866

48654867
function removeFailedError(

packages/cli/src/lib/app/build-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export async function resolveConfiguredAppBuildSettings(options: {
121121
configured: {
122122
command: string | null | undefined;
123123
outputDirectory: string | undefined;
124+
entrypoint?: string | undefined;
124125
};
125126
/** Absolute path of the compute config file owning these settings. */
126127
configPath: string;

packages/cli/src/lib/app/build.ts

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@ import path from "node:path";
44
import {
55
type BuildArtifact,
66
type BuildStrategy,
7+
type BuildType,
78
normalizeArtifactSymlinks,
89
resolveBuildStrategy,
910
stageStandaloneArtifact,
1011
} from "@prisma/compute-sdk";
12+
import {
13+
FRAMEWORKS,
14+
type FrameworkBuildType,
15+
} from "@prisma/compute-sdk/config";
1116

1217
import type { AppBuildSettings } from "./build-settings";
1318

@@ -25,22 +30,30 @@ export {
2530
resolveInferredAppBuildSettings,
2631
} from "./build-settings";
2732

28-
export const APP_BUILD_TYPES = [
33+
export type AppBuildType = BuildType;
34+
export type ResolvedAppBuildType = FrameworkBuildType;
35+
36+
export const RESOLVED_APP_BUILD_TYPES: readonly ResolvedAppBuildType[] = [
37+
...frameworkBuildTypesByLastOccurrence(),
38+
];
39+
40+
export const APP_BUILD_TYPES: readonly AppBuildType[] = [
2941
"auto",
30-
"bun",
31-
"nextjs",
32-
"nuxt",
33-
"astro",
34-
"nestjs",
35-
"tanstack-start",
36-
] as const;
37-
38-
export type AppBuildType = (typeof APP_BUILD_TYPES)[number];
39-
export type ResolvedAppBuildType = Exclude<AppBuildType, "auto">;
40-
41-
export const RESOLVED_APP_BUILD_TYPES = APP_BUILD_TYPES.filter(
42-
(buildType): buildType is ResolvedAppBuildType => buildType !== "auto",
43-
);
42+
...RESOLVED_APP_BUILD_TYPES,
43+
];
44+
45+
export const APP_BUILD_TYPE_LABELS = APP_BUILD_TYPES.join(", ");
46+
47+
function frameworkBuildTypesByLastOccurrence(): Set<FrameworkBuildType> {
48+
const buildTypes = new Set<FrameworkBuildType>();
49+
for (const framework of FRAMEWORKS) {
50+
if (buildTypes.has(framework.buildType)) {
51+
buildTypes.delete(framework.buildType);
52+
}
53+
buildTypes.add(framework.buildType);
54+
}
55+
return buildTypes;
56+
}
4457

4558
export class AppBuildStrategy implements BuildStrategy {
4659
readonly #appPath: string;

packages/cli/src/types/app.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,14 @@ export interface AppShowResult {
111111
export interface AppBuildResult {
112112
directory: string;
113113
entrypoint: string | null;
114-
buildType: "bun" | "nextjs" | "nuxt" | "astro" | "nestjs" | "tanstack-start";
114+
buildType:
115+
| "bun"
116+
| "nextjs"
117+
| "nuxt"
118+
| "astro"
119+
| "nestjs"
120+
| "tanstack-start"
121+
| "custom";
115122
}
116123

117124
export interface AppShowDeployResult {

packages/cli/tests/app-controller.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,6 +1682,102 @@ describe("app controller", () => {
16821682
);
16831683
});
16841684

1685+
it("uses and renders configured build entrypoints for custom deploys", async () => {
1686+
const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient());
1687+
const listApps = vi.fn().mockResolvedValue([]);
1688+
const deployApp = vi.fn().mockResolvedValue({
1689+
projectId: "proj_123",
1690+
app: {
1691+
id: "app_new",
1692+
name: "frontend",
1693+
region: "eu-central-1",
1694+
liveDeploymentId: "dep_123",
1695+
},
1696+
deployment: {
1697+
id: "dep_123",
1698+
status: "running",
1699+
url: "https://frontend.prisma.app",
1700+
},
1701+
});
1702+
1703+
vi.doMock("../src/lib/auth/guard", () => ({
1704+
requireComputeAuth,
1705+
}));
1706+
vi.doMock("../src/lib/app/app-provider", () => ({
1707+
createAppProvider: vi.fn(() =>
1708+
withBranchDatabaseProviderDefaults({
1709+
resolveBranch: createResolveBranch(),
1710+
listApps,
1711+
deployApp,
1712+
listDeployments: vi.fn(),
1713+
showDeployment: vi.fn(),
1714+
}),
1715+
),
1716+
}));
1717+
1718+
const { createTempCwd, createTestCommandContext } = await import(
1719+
"./helpers"
1720+
);
1721+
const { runAppDeploy } = await import("../src/controllers/app");
1722+
const cwd = await createTempCwd();
1723+
await writeFile(
1724+
path.join(cwd, "prisma.compute.ts"),
1725+
[
1726+
"export default {",
1727+
" app: {",
1728+
' name: "frontend",',
1729+
' framework: "custom",',
1730+
" build: {",
1731+
' outputDirectory: "dist",',
1732+
' entrypoint: "server.js",',
1733+
" },",
1734+
" },",
1735+
"};",
1736+
"",
1737+
].join("\n"),
1738+
"utf8",
1739+
);
1740+
const { context, stderr } = await createTestCommandContext({
1741+
cwd,
1742+
stateDir: path.join(cwd, ".state"),
1743+
isTTY: false,
1744+
env: {
1745+
...process.env,
1746+
PRISMA_CLI_MOCK_FIXTURE_PATH: undefined,
1747+
},
1748+
});
1749+
1750+
const result = await runAppDeploy(context, undefined, {
1751+
projectRef: "proj_123",
1752+
});
1753+
1754+
expect(deployApp).toHaveBeenCalledWith(
1755+
expect.objectContaining({
1756+
appName: "frontend",
1757+
buildType: "custom",
1758+
entrypoint: undefined,
1759+
buildSettings: {
1760+
buildCommand: null,
1761+
buildCommandSource: null,
1762+
outputDirectory: "dist",
1763+
outputDirectorySource: "set by prisma.compute.ts",
1764+
entrypoint: "server.js",
1765+
entrypointSource: "set by prisma.compute.ts",
1766+
},
1767+
}),
1768+
);
1769+
expect(asSingleDeployResult(result).result.deploySettings).toMatchObject({
1770+
config: {
1771+
path: "prisma.compute.ts",
1772+
status: "config",
1773+
},
1774+
entrypoint: "server.js",
1775+
});
1776+
expect(stderr.buffer).toContain("Using prisma.compute.ts");
1777+
expect(stderr.buffer).toContain("Entrypoint");
1778+
expect(stderr.buffer).toContain("server.js");
1779+
});
1780+
16851781
it("returns LOCAL_STATE_WRITE_FAILED when deploy cannot store the local binding", async () => {
16861782
const requireComputeAuth = vi
16871783
.fn()

0 commit comments

Comments
 (0)