Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/product/command-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ prisma-cli app run --build-type nextjs
prisma-cli app run --build-type bun --entry server.ts --port 3000
```

## `prisma-cli app deploy --project <id-or-name> --create-project <name> --app <name> --branch <name> --framework <nextjs|hono|tanstack-start> --entry <path> --http-port <port> --env <name=value>`
## `prisma-cli app deploy --project <id-or-name> --create-project <name> --app <name> --branch <name> --framework <nextjs|hono|tanstack-start|bun> --entry <path> --http-port <port> --env <name=value>`

Purpose:

Expand Down Expand Up @@ -626,7 +626,9 @@ Behavior:
- success human output prints `Live in <duration>`, the URL on its own line, and `Logs prisma-cli app logs`
- accepts repeated `--env NAME=VALUE` flags
- maps user-facing framework names to deploy build strategies
- accepts `--build-type <auto|bun|nextjs|nuxt|astro|tanstack-start>` as a legacy passthrough, but `--framework` wins when both are passed
- 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
- supports vanilla Bun apps with `--framework bun` using `package.json#main` or `package.json#module`, or with `--entry <path>`
- treats `--entry <path>` without `--framework` as a Bun app deploy
- does not print secret values
- returns app, deployment id, URL, and next steps in `--json` output

Expand All @@ -639,6 +641,8 @@ prisma-cli app deploy --create-project my-app --yes
prisma-cli app deploy --app my-app --env DATABASE_URL=postgresql://example
prisma-cli app deploy --framework nextjs --http-port 3000
prisma-cli app deploy --branch feat-login --framework hono --http-port 3000
prisma-cli app deploy --framework bun --entry src/server.ts --http-port 3000
prisma-cli app deploy --entry src/server.ts --http-port 3000
```

## `prisma-cli project env`
Expand Down
4 changes: 2 additions & 2 deletions examples/next-smoke/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pnpm prisma app deploy --app next-smoke
pnpm prisma app list-deploys
pnpm prisma app show-deploy <deployment-id>
pnpm prisma app deploy
pnpm prisma app deploy --app next-smoke --build-type nextjs --http-port 3000
pnpm prisma app deploy --app next-smoke --framework nextjs --http-port 3000
```

What this validates:
Expand All @@ -26,7 +26,7 @@ What this validates:
- first deploy bootstraps an example-local `prisma.config.ts` when no project is linked
- first deploy can create or reuse the `next-smoke` app
- first deploy uses the Next.js standalone build path without needing `--http-port`
- `--build-type nextjs --http-port 3000` is available as an explicit repair or override path
- `--framework nextjs --http-port 3000` is available as an explicit repair or override path
- second deploy reuses saved local app selection from `.prisma/cli/state.json`

Local files intentionally ignored in this example:
Expand Down
12 changes: 2 additions & 10 deletions packages/cli/src/commands/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,15 +174,9 @@ function createDeployCommand(runtime: CliRuntime): Command {
.addOption(new Option("--branch <name>", "Branch name"))
.addOption(
new Option("--framework <name>", "Framework to deploy")
.choices(["nextjs", "hono", "tanstack-start"]),
)
.addOption(new Option("--entry <path>", "Entrypoint path for Bun or auto deploys"))
.addOption(
new Option("--build-type <type>", "Legacy deploy build type")
.choices([...PREVIEW_BUILD_TYPES])
.default("auto")
.hideHelp(),
.choices(["nextjs", "hono", "tanstack-start", "bun"]),
)
.addOption(new Option("--entry <path>", "Entrypoint path for Bun deploys"))
.addOption(new Option("--http-port <port>", "HTTP port override for the deployed app"))
.addOption(
new Option("--env <name=value>", "Environment variable")
Expand All @@ -193,7 +187,6 @@ function createDeployCommand(runtime: CliRuntime): Command {
command.action(async (options) => {
const appName = (options as { app?: string }).app;
const entry = (options as { entry?: string }).entry;
const buildType = (options as { buildType?: string }).buildType;
const branchName = (options as { branch?: string }).branch;
const framework = (options as { framework?: string }).framework;
const httpPort = (options as { httpPort?: string }).httpPort;
Expand All @@ -210,7 +203,6 @@ function createDeployCommand(runtime: CliRuntime): Command {
createProjectName,
branchName,
entrypoint: entry,
buildType,
framework,
httpPort,
envAssignments,
Expand Down
132 changes: 90 additions & 42 deletions packages/cli/src/controllers/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
resolveLocalBuildType,
runLocalApp,
} from "../lib/app/local-dev";
import { readBunPackageJson, type BunPackageJsonLike } from "../lib/app/bun-project";
import { readBunPackageEntrypoint, readBunPackageJson, type BunPackageJsonLike } from "../lib/app/bun-project";
import {
inferTargetName,
projectNotFoundError,
Expand Down Expand Up @@ -94,9 +94,13 @@ import { listRealWorkspaceProjects } from "./project";
import { createSelectPromptPort } from "./select-prompt-port";

type AppDomainCommand = "add" | "show" | "remove" | "retry" | "wait";
type DeployFramework = "nextjs" | "hono" | "tanstack-start";
type DeployFramework = "nextjs" | "hono" | "tanstack-start" | "bun";

const DEPLOY_FRAMEWORKS = ["nextjs", "hono", "tanstack-start"] as const satisfies readonly DeployFramework[];
const DEPLOY_FRAMEWORKS = ["nextjs", "hono", "tanstack-start", "bun"] as const satisfies readonly DeployFramework[];
const TANSTACK_START_PACKAGES = [
"@tanstack/react-start",
"@tanstack/solid-start",
] as const;
const FRAMEWORK_DEFAULT_HTTP_PORT = 3000;
const PRISMA_PROJECT_ID_ENV_VAR = "PRISMA_PROJECT_ID";
const PRISMA_APP_ID_ENV_VAR = "PRISMA_APP_ID";
Expand Down Expand Up @@ -210,7 +214,6 @@ export async function runAppDeploy(
createProjectName?: string;
branchName?: string;
entrypoint?: string;
buildType?: string;
framework?: string;
httpPort?: string;
envAssignments?: string[];
Expand All @@ -234,18 +237,14 @@ export async function runAppDeploy(
throw localResolutionPinStaleError();
}

const explicitBuildType = Boolean(options?.buildType && options.buildType !== "auto");
const branch = await resolveDeployBranch(context, options?.branchName);
if (options?.httpPort) {
parseDeployHttpPort(options.httpPort);
}
assertSupportedEntrypointForRequestedDeployShape({
requestedFramework: options?.framework,
requestedBuildType: options?.buildType,
explicitBuildType,
entrypoint: options?.entrypoint,
});

const branch = await resolveDeployBranch(context, options?.branchName);
const { provider, target, projectId } = await requireProviderAndDeployProjectContext(context, options?.projectRef, {
branch,
createProjectName: options?.createProjectName,
Expand All @@ -266,8 +265,7 @@ export async function runAppDeploy(

let framework = await resolveDeployFramework(context, {
requestedFramework: options?.framework,
requestedBuildType: options?.buildType,
explicitBuildType,
entrypoint: options?.entrypoint,
});
let runtime = resolveDeployRuntime(options?.httpPort, framework);
assertSupportedEntrypoint(framework.buildType, options?.entrypoint, "deploy");
Expand Down Expand Up @@ -296,7 +294,7 @@ export async function runAppDeploy(
runtime,
firstDeploy: selectedApp.firstDeploy,
explicitFramework: Boolean(options?.framework),
explicitBuildType,
explicitEntrypoint: Boolean(options?.entrypoint),
explicitHttpPort: Boolean(options?.httpPort),
});
framework = customized.framework;
Expand All @@ -306,6 +304,7 @@ export async function runAppDeploy(
// derives its entrypoint from build output, so validate --entry again after it.
const buildType = framework.buildType;
assertSupportedEntrypoint(buildType, options?.entrypoint, "deploy");
const entrypoint = await resolveDeployEntrypoint(context.runtime.cwd, framework, options?.entrypoint);
const portMapping = parseDeployPortMapping(String(runtime.port));

const progressState = createPreviewDeployProgressState();
Expand All @@ -317,7 +316,7 @@ export async function runAppDeploy(
appId: selectedApp.appId,
appName: selectedApp.appName,
region: selectedApp.region,
entrypoint: options?.entrypoint,
entrypoint,
buildType,
portMapping,
envVars,
Expand Down Expand Up @@ -1205,7 +1204,7 @@ async function resolveAppDomainTarget(

const envProjectId = readDeployEnvOverride(context, PRISMA_PROJECT_ID_ENV_VAR);
const envAppId = readDeployEnvOverride(context, PRISMA_APP_ID_ENV_VAR);
const skipLocalPin = Boolean(envProjectId);
const skipLocalPin = Boolean(envProjectId || options?.projectRef);
const localPin = skipLocalPin
? ({ kind: "missing" } satisfies LocalResolutionPinReadResult)
: await readLocalResolutionPin(context.runtime.cwd);
Expand Down Expand Up @@ -2589,24 +2588,20 @@ async function resolveDeployFramework(
context: CommandContext,
options: {
requestedFramework: string | undefined;
requestedBuildType: string | undefined;
explicitBuildType: boolean;
entrypoint: string | undefined;
},
): Promise<ResolvedDeployFramework> {
if (options.requestedFramework) {
return frameworkFromUserFacingValue(options.requestedFramework, "set by --framework");
}

if (options.explicitBuildType) {
const buildType = normalizeBuildType(options.requestedBuildType);
if (buildType !== "auto") {
return {
key: buildType,
buildType,
displayName: formatBuildTypeName(buildType),
annotation: "set by --build-type",
};
}
if (options.entrypoint) {
return {
key: "bun",
buildType: "bun",
displayName: "Bun",
annotation: "set by --entry",
};
}

const detected = await detectDeployFramework(context.runtime.cwd);
Expand Down Expand Up @@ -2636,22 +2631,45 @@ function resolveDeployRuntime(

function assertSupportedEntrypointForRequestedDeployShape(options: {
requestedFramework: string | undefined;
requestedBuildType: string | undefined;
explicitBuildType: boolean;
entrypoint: string | undefined;
}): void {
if (options.requestedFramework) {
const framework = frameworkFromUserFacingValue(options.requestedFramework, "set by --framework");
assertSupportedEntrypoint(framework.buildType, options.entrypoint, "deploy");
if (!options.requestedFramework) {
return;
}

if (!options.explicitBuildType) {
return;
const framework = frameworkFromUserFacingValue(options.requestedFramework, "set by --framework");
assertSupportedEntrypoint(framework.buildType, options.entrypoint, "deploy");
}

async function resolveDeployEntrypoint(
cwd: string,
framework: ResolvedDeployFramework,
explicitEntrypoint: string | undefined,
): Promise<string | undefined> {
if (explicitEntrypoint || framework.buildType !== "bun") {
return explicitEntrypoint;
}

const packageJson = await readBunPackageJson(cwd);
const packageEntrypoint = readBunPackageEntrypoint(packageJson);
if (packageEntrypoint) {
return packageEntrypoint;
}

if (framework.key !== "hono") {
return undefined;
}

const buildType = normalizeBuildType(options.requestedBuildType);
assertSupportedEntrypoint(buildType, options.entrypoint, "deploy");
const defaultEntrypoint = "src/index.ts";
try {
await access(path.join(cwd, defaultEntrypoint));
return defaultEntrypoint;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
throw error;
}
return undefined;
}
}

async function detectDeployFramework(cwd: string): Promise<ResolvedDeployFramework | null> {
Expand Down Expand Up @@ -2680,7 +2698,7 @@ async function detectDeployFramework(cwd: string): Promise<ResolvedDeployFramewo
};
}

if (hasPackageDependency(packageJson, "@tanstack/start")) {
if (hasAnyPackageDependency(packageJson, TANSTACK_START_PACKAGES)) {
return {
key: "tanstack-start",
buildType: "tanstack-start",
Expand All @@ -2698,6 +2716,7 @@ async function detectNextConfig(cwd: string): Promise<{ exists: boolean; standal
"next.config.mjs",
"next.config.cjs",
"next.config.ts",
"next.config.mts",
];

for (const candidate of candidates) {
Expand Down Expand Up @@ -2726,6 +2745,10 @@ function hasPackageDependency(packageJson: BunPackageJsonLike | null, dependency
|| hasDependency(packageJson?.devDependencies, dependencyName);
}

function hasAnyPackageDependency(packageJson: BunPackageJsonLike | null, dependencyNames: readonly string[]): boolean {
return dependencyNames.some((dependencyName) => hasPackageDependency(packageJson, dependencyName));
}

function hasDependency(dependencies: unknown, dependencyName: string): boolean {
return Boolean(
dependencies
Expand All @@ -2752,9 +2775,17 @@ function frameworkFromUserFacingValue(value: string, annotation: string): Resolv
displayName: "Hono",
annotation,
};
case "bun":
return {
key: "bun",
buildType: "bun",
displayName: "Bun",
annotation,
};
case "tanstack":
case "tanstack-start":
case "@tanstack/start":
case "@tanstack/react-start":
case "@tanstack/solid-start":
return {
key: "tanstack-start",
buildType: "tanstack-start",
Expand All @@ -2767,7 +2798,7 @@ function frameworkFromUserFacingValue(value: string, annotation: string): Resolv
}

function frameworkNotDetectedError(cwd: string | undefined, requestedFramework?: string): CliError {
const supported = "Next.js, Hono, TanStack Start";
const supported = "Next.js, Hono, TanStack Start, Bun";
const directory = cwd ? ` in ${formatDeployDirectory(cwd)}` : "";

return new CliError({
Expand All @@ -2777,12 +2808,14 @@ function frameworkNotDetectedError(cwd: string | undefined, requestedFramework?:
? `Unsupported framework "${requestedFramework}"`
: `Cannot detect a supported framework${directory}`,
why: `Supported Beta frameworks: ${supported}.`,
fix: "Add one of these frameworks as a dependency, or pass --framework <nextjs|hono|tanstack-start>.",
fix: "Add one of these frameworks as a dependency, pass --framework <nextjs|hono|tanstack-start|bun>, or pass --entry <path> for a Bun app.",
exitCode: 2,
nextSteps: [
"prisma-cli app deploy --framework nextjs",
"prisma-cli app deploy --framework hono",
"prisma-cli app deploy --framework tanstack-start",
"prisma-cli app deploy --framework bun --entry server.ts",
"prisma-cli app deploy --entry server.ts",
],
});
}
Expand Down Expand Up @@ -2828,15 +2861,15 @@ async function maybeCustomizeDeploySettings(
runtime: ResolvedDeployRuntime;
firstDeploy: boolean;
explicitFramework: boolean;
explicitBuildType: boolean;
explicitEntrypoint: boolean;
explicitHttpPort: boolean;
},
): Promise<{ framework: ResolvedDeployFramework; runtime: ResolvedDeployRuntime }> {
if (
!options.firstDeploy
|| context.flags.yes
|| options.explicitFramework
|| options.explicitBuildType
|| options.explicitEntrypoint
|| options.explicitHttpPort
|| !canPrompt(context)
) {
Expand Down Expand Up @@ -2912,6 +2945,8 @@ function frameworkDisplayName(framework: DeployFramework): string {
return "Hono";
case "tanstack-start":
return "TanStack Start";
case "bun":
return "Bun";
}
}

Expand Down Expand Up @@ -2965,7 +3000,7 @@ function isPreviewBuildType(value: string): value is PreviewBuildType {
return (PREVIEW_BUILD_TYPES as readonly string[]).includes(value);
}

function getBuildTypeExamples(commandName: "build" | "deploy"): string[] {
function getBuildTypeExamples(commandName: "build"): string[] {
return RESOLVED_PREVIEW_BUILD_TYPES.map((buildType) => {
const entrypoint = buildType === "bun" ? " --entry server.ts" : "";
return `prisma-cli app ${commandName} --build-type ${buildType}${entrypoint}`;
Expand All @@ -2980,6 +3015,19 @@ function assertSupportedEntrypoint(
// Framework strategies derive their runtime entrypoints from build output.
// Only Bun consumes a user-provided source entrypoint; auto may fall back to Bun.
if (buildType !== "auto" && buildType !== "bun" && entrypoint) {
if (commandName === "deploy") {
throw usageError(
`App deploy does not accept --entry with ${formatBuildTypeName(buildType)}`,
`${formatBuildTypeName(buildType)} apps derive their runtime entrypoint from build output.`,
"Remove --entry, or use --framework bun when you want to target a Bun entrypoint directly.",
[
`prisma-cli app deploy --framework ${buildType}`,
"prisma-cli app deploy --framework bun --entry server.ts",
],
"app",
);
}

throw usageError(
`App ${commandName} does not accept --entry with --build-type ${buildType}`,
`${formatBuildTypeName(buildType)} apps do not use an entrypoint flag in the current preview.`,
Expand Down
Loading
Loading