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
5 changes: 3 additions & 2 deletions docs/product/command-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1217,7 +1217,7 @@ Purpose:
Behavior:

- 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
- detects supported project shapes when `--build-type auto` is used and no config framework applies
- detects supported project shapes when `--build-type auto` is used and no config framework applies; an explicit `--entry` targets a Bun build and wins over that detection, exactly like `app deploy` resolves `--entry`
- supports Bun, Next.js, Nuxt, Astro, NestJS, TanStack Start, and custom artifact app builds in the beta package
- fails with `USAGE_ERROR` when framework detection is ambiguous

Expand All @@ -1244,7 +1244,7 @@ Behavior:

- resolves the optional `[app]` target, app root, framework, entrypoint, and port from `prisma.compute.ts` exactly like `app deploy`; explicit `--entry`, `--port`, and a non-`auto` `--build-type` override the config
- fails with `USAGE_ERROR` when the configured framework has no local dev server in the current preview
- detects supported project shapes when `--build-type auto` is used and no config framework applies
- detects supported project shapes when `--build-type auto` is used and no config framework applies; an explicit `--entry` targets a Bun app and wins over that detection, exactly like `app deploy` resolves `--entry`
- starts the local framework command
- reports `RUN_FAILED` when the local process cannot start or exits unsuccessfully

Expand Down Expand Up @@ -1306,6 +1306,7 @@ same reasons they are excluded today.
- `[app]` without any compute config file is a usage error
- a config that fails to load or validate fails with `COMPUTE_CONFIG_INVALID` before any remote work
- settings sourced from the config are annotated `set by prisma.compute.ts` in human output and deploy settings metadata
- `deploySettings.config.path` reports the compute config file in effect whenever one loaded, even when it has no `build` block, so `path: null` means "no config loaded" rather than "no build block"; `deploySettings.config.status` stays `"config"` when the build block owned the build settings and `"inferred"` when they came from framework defaults

```ts
import { defineComputeConfig } from "@prisma/compute-sdk/config";
Expand Down
14 changes: 13 additions & 1 deletion packages/cli/src/controllers/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@
requestedBuildType: buildType,
configFramework: compute.target?.framework ?? null,
appDir,
entrypoint: merged.entrypoint,
});
// Hono apps get the same src/index.ts entrypoint default as deploy.
const entrypoint =
Expand Down Expand Up @@ -583,7 +584,7 @@
});
}

async function runSingleAppDeploy(

Check notice on line 587 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/complexity/noExcessiveCognitiveComplexity

Excessive complexity of 18 detected (max: 15).
context: CommandContext,
appName: string | undefined,
options: AppDeployOptions | undefined,
Expand Down Expand Up @@ -852,7 +853,11 @@
promoted: deployResult.promoted,
deploySettings: {
config: {
path: buildSettingsResolution.relativeConfigPath,
// The compute config in effect, even when it has no build block, so
// `path: null` means "no config loaded" rather than "no build-settings
// block". `status` still says whether the build block owned the
// build settings ("config") or they were inferred ("inferred").
path: computeConfig.config?.relativeConfigPath ?? null,
status: buildSettingsResolution.status,
},
buildCommand: {
Expand Down Expand Up @@ -1174,9 +1179,9 @@
...deployment.deployment,
live: providerLiveDeploymentId
? deployment.deployment.id === providerLiveDeploymentId
: knownLiveDeploymentId
? deployment.deployment.id === knownLiveDeploymentId
: deployment.deployment.live,

Check notice on line 1184 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNestedTernary

Do not nest ternary expressions.
},
},
warnings: [],
Expand Down Expand Up @@ -1555,10 +1560,10 @@
});
}

await sleep(
Math.min(pollIntervalMs, Math.max(deadline - Date.now(), 0)),
context.runtime.signal,
);

Check notice on line 1566 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/noAwaitInLoops

Avoid using await inside loops.
current = await target.provider
.showDomain(current.id, { signal: context.runtime.signal })
.catch((error) => {
Expand Down Expand Up @@ -2120,7 +2125,7 @@
const compute = await resolveComputeManagementContext(
context,
options?.configTarget,
commandName.replace(/^app /, ""),

Check warning on line 2128 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
);
const branch = resolveDomainBranch(options?.branchName);
if (toBranchKind(branch.name) !== "production") {
Expand Down Expand Up @@ -2253,7 +2258,7 @@
}

function normalizeDomainHostname(hostname: string): string {
const normalized = hostname.trim().replace(/\.$/, "").toLowerCase();

Check warning on line 2261 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
if (!isValidDomainHostname(normalized)) {
throw new CliError({
code: "DOMAIN_HOSTNAME_INVALID",
Expand Down Expand Up @@ -2288,14 +2293,14 @@
}

return labels.every((label) =>
/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label),

Check warning on line 2296 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
);
}

function sameDomainHostname(left: string, right: string): boolean {
return (
left.trim().replace(/\.$/, "").toLowerCase() ===

Check warning on line 2302 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
right.trim().replace(/\.$/, "").toLowerCase()

Check warning on line 2303 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
);
}

Expand Down Expand Up @@ -2383,7 +2388,7 @@
}
}

function domainCommandError(

Check notice on line 2391 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/complexity/noExcessiveCognitiveComplexity

Excessive complexity of 26 detected (max: 15).
command: AppDomainCommand,
error: unknown,
hostname: string,
Expand Down Expand Up @@ -2549,7 +2554,7 @@

function extractDomainDnsTarget(error: DomainApiError): string | null {
const text = `${error.hint ?? ""} ${error.message}`;
const match = /\b((?:[a-z0-9-]+\.)+prisma\.build)\b/i.exec(text);

Check warning on line 2557 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
return match?.[1]?.toLowerCase() ?? null;
}

Expand Down Expand Up @@ -2585,7 +2590,7 @@
return 0;
}

const match = /^(\d+)(ms|s|m|h)$/.exec(trimmed);

Check warning on line 2593 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
if (!match) {
throw usageError(
`Invalid timeout "${value}"`,
Expand All @@ -2601,11 +2606,11 @@
const multiplier =
unit === "h"
? 60 * 60 * 1000
: unit === "m"
? 60 * 1000
: unit === "s"
? 1000
: 1;

Check notice on line 2613 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNestedTernary

Do not nest ternary expressions.

Check notice on line 2613 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNestedTernary

Do not nest ternary expressions.
return amount * multiplier;
}

Expand Down Expand Up @@ -3379,7 +3384,7 @@
listProjects: () =>
listRealWorkspaceProjects(
client,
authState.workspace!,

Check warning on line 3387 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.
context.runtime.signal,
),
commandName: options?.commandName,
Expand Down Expand Up @@ -3413,7 +3418,7 @@
};
}

async function resolveDeployProjectContext(

Check notice on line 3421 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/complexity/noExcessiveCognitiveComplexity

Excessive complexity of 18 detected (max: 15).
context: CommandContext,
client: ManagementApiClient,
provider: ReturnType<typeof createAppProvider>,
Expand Down Expand Up @@ -4088,7 +4093,7 @@
continue;
}

const configFile = await detectFrameworkConfigFile(cwd, framework, signal);

Check notice on line 4096 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/noAwaitInLoops

Avoid using await inside loops.
if (
!configFile.exists &&
!hasAnyPackageDependency(packageJson, framework.detectPackages)
Expand All @@ -4101,8 +4106,8 @@
const annotation =
framework.key === "nextjs" && configFile.standalone
? "standalone output detected"
: configFile.exists
? `detected from ${path.basename(configFile.path!)}`

Check warning on line 4110 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.
: "detected from package.json";

Check notice on line 4111 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNestedTernary

Do not nest ternary expressions.

return {
Expand All @@ -4125,10 +4130,10 @@
const filePath = path.join(cwd, candidate);
signal.throwIfAborted();
try {
const content = await readFile(filePath, { encoding: "utf8", signal });

Check notice on line 4133 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/noAwaitInLoops

Avoid using await inside loops.
return {
exists: true,
standalone: /\boutput\s*:\s*["'`]standalone["'`]/.test(content),

Check warning on line 4136 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
path: filePath,
};
} catch (error) {
Expand Down Expand Up @@ -4641,8 +4646,15 @@
requestedBuildType: AppBuildType;
configFramework: ComputeFramework | null;
appDir: string;
entrypoint?: string;
},
): Promise<ResolvedDeployFramework> {
// An explicit entrypoint targets a Bun app, so honor it over framework
// auto-detection, matching deploy and local build.
if (options.requestedBuildType === "auto" && options.entrypoint) {
return frameworkFromUserFacingValue("bun", "set by --entry");
}

if (
(LOCAL_DEV_BUILD_TYPES as readonly string[]).includes(
options.requestedBuildType,
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/src/lib/app/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,21 @@ export async function resolveAppBuildStrategy(options: {
strategy: BuildStrategy;
buildType: ResolvedAppBuildType;
}> {
// An explicit entrypoint targets a Bun build, so honor it over framework
// auto-detection instead of letting a detected framework (e.g. Next.js)
// silently ignore --entry. This mirrors how deploy resolves --entry and the
// "auto may fall back to Bun" contract in assertSupportedEntrypoint.
const buildType =
options.buildType === "auto" && options.entrypoint
? "bun"
: options.buildType;

// Detection, per-framework construction, and Bun entrypoint resolution
// (package.json `main`) all live in the SDK now; the CLI forwards. The CLI's
// build types map 1:1 to the SDK's, and `auto` is the SDK default.
return resolveBuildStrategy({
appPath: options.appPath,
buildType: options.buildType,
buildType,
entrypoint: options.entrypoint,
buildSettings: options.buildSettings,
signal: options.signal,
Expand Down
28 changes: 28 additions & 0 deletions packages/cli/tests/app-build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,34 @@ describe("preview build strategy", () => {
});
});

it("resolves an explicit entrypoint to Bun even when Next.js is detectable", async () => {
const { resolveAppBuildStrategy } = await import("../src/lib/app/build");
const cwd = await createTempCwd();
const appPath = path.join(cwd, "app");

await mkdir(appPath, { recursive: true });
await writeFile(
path.join(appPath, "package.json"),
`${JSON.stringify({ dependencies: { next: "15.0.0" } }, null, 2)}\n`,
"utf8",
);
await writeFile(
path.join(appPath, "server.ts"),
"export default { fetch: () => new Response('ok') };\n",
"utf8",
);

await expect(
resolveAppBuildStrategy({
appPath,
entrypoint: "server.ts",
buildType: "auto",
}),
).resolves.toMatchObject({
buildType: "bun",
});
});

it("runs package.json build scripts before staging Next.js output", async () => {
const cwd = await createTempCwd();
const appPath = path.join(cwd, "app");
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/tests/app-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,12 @@ describe("app controller", () => {
expect(asSingleDeployResult(result).result.deploySettings.region).toBe(
"us-west-1",
);
// A loaded config with no build block still reports its path; only the
// build-settings ownership stays "inferred".
expect(asSingleDeployResult(result).result.deploySettings.config).toEqual({
path: "prisma.compute.ts",
status: "inferred",
});
});

it("uses --region when creating a new app", async () => {
Expand Down
47 changes: 47 additions & 0 deletions packages/cli/tests/app-local-dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,4 +480,51 @@ describe("app local dev commands", () => {
command: "bun --watch server.ts",
});
});

it("run resolves an explicit entrypoint to Bun even when Next.js is detectable", async () => {
const runLocalApp = vi.fn().mockResolvedValue({
framework: "bun",
entrypoint: "server.ts",
port: 3000,
command: "bun --watch server.ts",
exitCode: 0,
signal: null,
});

vi.doMock("../src/lib/app/local-dev", async () => {
const actual = await vi.importActual<
typeof import("../src/lib/app/local-dev")
>("../src/lib/app/local-dev");
return {
...actual,
runLocalApp,
};
});

const { createTempCwd, createTestCommandContext } = await import(
"./helpers"
);
const { runAppRun } = await import("../src/controllers/app");
const cwd = await createTempCwd();
await writeFile(
path.join(cwd, "package.json"),
`${JSON.stringify({ dependencies: { next: "15.0.0" } }, null, 2)}\n`,
"utf8",
);
await writeFile(
path.join(cwd, "server.ts"),
"export default { fetch: () => new Response('ok') };\n",
"utf8",
);
const { context } = await createTestCommandContext({
cwd,
stateDir: path.join(cwd, ".state"),
});

await runAppRun(context, { entrypoint: "server.ts", buildType: "auto" });

expect(runLocalApp).toHaveBeenCalledWith(
expect.objectContaining({ buildType: "bun", entrypoint: "server.ts" }),
);
});
});
Loading