From e58514f7b2c60f6927cc3624c27cc63bc546a2a7 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Tue, 5 May 2026 21:50:16 +0200 Subject: [PATCH 1/2] Add compute SDK build strategies to app workflows --- docs/product/command-spec.md | 13 +- packages/cli/package.json | 2 +- packages/cli/src/commands/app/index.ts | 5 +- packages/cli/src/controllers/app.ts | 76 +++++-- packages/cli/src/lib/app/local-dev.ts | 14 +- packages/cli/src/lib/app/preview-build.ts | 233 ++++++---------------- packages/cli/src/shell/command-meta.ts | 15 +- packages/cli/src/types/app.ts | 2 +- packages/cli/tests/app-bun-compat.test.ts | 85 ++++++++ packages/cli/tests/app-local-dev.test.ts | 54 ++++- pnpm-lock.yaml | 10 +- 11 files changed, 300 insertions(+), 209 deletions(-) diff --git a/docs/product/command-spec.md b/docs/product/command-spec.md index 57e5cbd..920a2c9 100644 --- a/docs/product/command-spec.md +++ b/docs/product/command-spec.md @@ -299,7 +299,7 @@ prisma branch use prisma branch use production ``` -## `prisma app build --entry --build-type ` +## `prisma app build --entry --build-type ` Purpose: @@ -308,13 +308,16 @@ Purpose: Behavior: - detects supported project shapes when `--build-type auto` is used -- supports Bun and Next.js app builds in the preview package +- supports Bun, Next.js, Nuxt, Astro, and TanStack Start app builds in the preview package - fails with `USAGE_ERROR` when framework detection is ambiguous Examples: ```bash prisma app build --build-type nextjs +prisma app build --build-type nuxt +prisma app build --build-type astro +prisma app build --build-type tanstack-start prisma app build --build-type bun --entry server.ts ``` @@ -337,7 +340,7 @@ prisma app run --build-type nextjs prisma app run --build-type bun --entry server.ts --port 3000 ``` -## `prisma app deploy --app --entry --build-type --http-port --env ` +## `prisma app deploy --app --entry --build-type --http-port --env ` Purpose: @@ -349,6 +352,7 @@ Behavior: - resolves or creates project context - resolves or creates app context when required - accepts repeated `--env NAME=VALUE` flags +- uses the same supported build strategies as `app build` - does not print secret values - returns app, deployment id, URL, and next steps @@ -358,6 +362,9 @@ Examples: prisma app deploy prisma app deploy --app hello-world --env DATABASE_URL=postgresql://example prisma app deploy --app hello-world --build-type nextjs --http-port 3000 +prisma app deploy --app hello-world --build-type nuxt +prisma app deploy --app hello-world --build-type astro +prisma app deploy --app hello-world --build-type tanstack-start ``` ## `prisma app update-env --app --env ` diff --git a/packages/cli/package.json b/packages/cli/package.json index f4959ea..87bfc96 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -41,7 +41,7 @@ }, "dependencies": { "@clack/prompts": "^1.2.0", - "@prisma/compute-sdk": "^0.14.0", + "@prisma/compute-sdk": "^0.17.0", "c12": "4.0.0-beta.4", "@prisma/credentials-store": "^7.7.0", "@prisma/management-api-sdk": "^1.24.0", diff --git a/packages/cli/src/commands/app/index.ts b/packages/cli/src/commands/app/index.ts index 1038665..1edf4f6 100644 --- a/packages/cli/src/commands/app/index.ts +++ b/packages/cli/src/commands/app/index.ts @@ -45,6 +45,7 @@ import { attachCommandDescriptor } from "../../shell/command-meta"; import { addGlobalFlags } from "../../shell/global-flags"; import { runCommand } from "../../shell/command-runner"; import { configureRuntimeCommand, type CliRuntime } from "../../shell/runtime"; +import { PREVIEW_BUILD_TYPES } from "../../lib/app/preview-build"; import type { AppBuildResult, AppDeployResult, @@ -90,7 +91,7 @@ function createBuildCommand(runtime: CliRuntime): Command { .addOption(new Option("--entry ", "Entrypoint path for Bun or auto builds")) .addOption( new Option("--build-type ", "Local build type") - .choices(["auto", "bun", "nextjs"]) + .choices([...PREVIEW_BUILD_TYPES]) .default("auto"), ); addGlobalFlags(command); @@ -161,7 +162,7 @@ function createDeployCommand(runtime: CliRuntime): Command { .addOption(new Option("--entry ", "Entrypoint path for Bun or auto deploys")) .addOption( new Option("--build-type ", "Deploy build type") - .choices(["auto", "bun", "nextjs"]) + .choices([...PREVIEW_BUILD_TYPES]) .default("auto"), ) .addOption(new Option("--http-port ", "HTTP port override for the deployed app")) diff --git a/packages/cli/src/controllers/app.ts b/packages/cli/src/controllers/app.ts index 7307503..c58c9d6 100644 --- a/packages/cli/src/controllers/app.ts +++ b/packages/cli/src/controllers/app.ts @@ -31,7 +31,7 @@ import { runLocalApp, } from "../lib/app/local-dev"; import { projectNotFoundError } from "../use-cases/project"; -import { executePreviewBuild, type PreviewBuildType } from "../lib/app/preview-build"; +import { executePreviewBuild, PREVIEW_BUILD_TYPES, type PreviewBuildType } from "../lib/app/preview-build"; import { createPreviewDeployInteraction, PREVIEW_DEFAULT_REGION, @@ -56,13 +56,11 @@ export async function runAppBuild( const buildType = normalizeBuildType(requestedBuildType); assertSupportedEntrypoint(buildType, entrypoint, "build"); - const resolvedBuildType = await requireLocalBuildType(context, buildType, "build"); - try { const { artifact, buildType: actualBuildType } = await executePreviewBuild({ appPath: context.runtime.cwd, entrypoint, - buildType: resolvedBuildType, + buildType, }); return { @@ -76,6 +74,22 @@ export async function runAppBuild( nextSteps: ["prisma app deploy"], }; } catch (error) { + if (buildType === "auto" && isAutoBuildDetectionError(error)) { + throw usageError( + "App build requires an explicit framework when detection is ambiguous", + "This preview auto-detects clear Bun, Next.js, Nuxt, Astro, or TanStack Start project shapes.", + "Pass a supported --build-type value, or pass --entry for a Bun app.", + [ + "prisma app build --build-type nextjs", + "prisma app build --build-type nuxt", + "prisma app build --build-type astro", + "prisma app build --build-type tanstack-start", + "prisma app build --build-type bun --entry server.ts", + ], + "app", + ); + } + throw buildFailedError("Local app build failed", error); } } @@ -1217,31 +1231,43 @@ function normalizeBuildType(requestedBuildType: string | undefined): PreviewBuil return "auto"; } - if (requestedBuildType === "auto" || requestedBuildType === "bun" || requestedBuildType === "nextjs") { + if (isPreviewBuildType(requestedBuildType)) { return requestedBuildType; } throw usageError( `Unsupported build type "${requestedBuildType}"`, - "Only auto, bun, and nextjs are supported in the current preview.", - "Pass --build-type auto, --build-type bun, or --build-type nextjs.", - ["prisma app build --build-type nextjs", "prisma app build --build-type bun --entry server.ts"], + "Only auto, bun, nextjs, nuxt, astro, and tanstack-start are supported in the current preview.", + "Pass a supported --build-type value.", + [ + "prisma app build --build-type nextjs", + "prisma app build --build-type nuxt", + "prisma app build --build-type astro", + "prisma app build --build-type tanstack-start", + "prisma app build --build-type bun --entry server.ts", + ], "app", ); } +function isPreviewBuildType(value: string): value is PreviewBuildType { + return (PREVIEW_BUILD_TYPES as readonly string[]).includes(value); +} + function assertSupportedEntrypoint( buildType: PreviewBuildType, entrypoint: string | undefined, commandName: "build" | "run" | "deploy", ) { - if (buildType === "nextjs" && entrypoint) { + // 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) { throw usageError( - `App ${commandName} does not accept --entry with --build-type nextjs`, - "Next.js apps do not use an entrypoint flag in the current preview.", + `App ${commandName} does not accept --entry with --build-type ${buildType}`, + `${formatBuildTypeName(buildType)} apps do not use an entrypoint flag in the current preview.`, `Remove --entry, or rerun prisma app ${commandName} with --build-type bun when you want to target a Bun entrypoint directly.`, [ - `prisma app ${commandName} --build-type nextjs`, + `prisma app ${commandName} --build-type ${buildType}`, `prisma app ${commandName} --build-type bun --entry server.ts`, ], "app", @@ -1254,6 +1280,9 @@ async function requireLocalBuildType( buildType: PreviewBuildType, commandName: "build" | "run", ) { + // Local dev server support is intentionally narrower than deploy build support. + // Nuxt, Astro, and TanStack Start can deploy via SDK strategies, but app run + // only starts the local dev servers currently documented for the preview. const resolvedBuildType = await resolveLocalBuildType(context.runtime.cwd, buildType); if (resolvedBuildType) { return resolvedBuildType; @@ -1261,7 +1290,7 @@ async function requireLocalBuildType( throw usageError( `App ${commandName} requires an explicit framework when detection is ambiguous`, - "This preview only auto-detects clear Next.js or Bun project shapes.", + "This preview only starts local dev servers for clear Next.js or Bun project shapes.", "Pass --build-type nextjs for a Next.js app, or pass --build-type bun with --entry for a Bun app.", [ `prisma app ${commandName} --build-type nextjs`, @@ -1387,6 +1416,27 @@ function formatFrameworkName(framework: AppRunResult["framework"]): string { return framework === "nextjs" ? "Next.js" : "Bun"; } +function isAutoBuildDetectionError(error: unknown): boolean { + return error instanceof Error && error.message.startsWith("Entrypoint is required."); +} + +function formatBuildTypeName(buildType: PreviewBuildType): string { + switch (buildType) { + case "nextjs": + return "Next.js"; + case "nuxt": + return "Nuxt"; + case "astro": + return "Astro"; + case "tanstack-start": + return "TanStack Start"; + case "bun": + return "Bun"; + case "auto": + return "Auto"; + } +} + function removeFailedError(summary: string, error: unknown, nextSteps: string[]): CliError { return new CliError({ code: "REMOVE_FAILED", diff --git a/packages/cli/src/lib/app/local-dev.ts b/packages/cli/src/lib/app/local-dev.ts index 17f6c34..456f29a 100644 --- a/packages/cli/src/lib/app/local-dev.ts +++ b/packages/cli/src/lib/app/local-dev.ts @@ -5,6 +5,8 @@ import path from "node:path"; import type { PreviewBuildType, ResolvedPreviewBuildType } from "./preview-build"; import { readBunPackageEntrypoint, readBunPackageJson, resolveBunEntrypoint } from "./bun-project"; +export type LocalBuildType = Extract; + const NEXT_CONFIG_FILENAMES = [ "next.config.js", "next.config.mjs", @@ -15,7 +17,7 @@ const NEXT_CONFIG_FILENAMES = [ export const DEFAULT_LOCAL_DEV_PORT = 3000; export interface LocalRunResult { - framework: ResolvedPreviewBuildType; + framework: LocalBuildType; entrypoint: string | null; port: number; command: string; @@ -32,15 +34,19 @@ interface CommandCandidate { export async function resolveLocalBuildType( appPath: string, buildType: PreviewBuildType, -): Promise { +): Promise { if (buildType === "bun" || buildType === "nextjs") { return buildType; } + if (buildType !== "auto") { + return null; + } + return detectLocalBuildType(appPath); } -export async function detectLocalBuildType(appPath: string): Promise { +export async function detectLocalBuildType(appPath: string): Promise { if (await isNextProject(appPath)) { return "nextjs"; } @@ -54,7 +60,7 @@ export async function detectLocalBuildType(appPath: string): Promise; +export const PREVIEW_BUILD_TYPES = [ + "auto", + "bun", + "nextjs", + "nuxt", + "astro", + "tanstack-start", +] as const; -const NEXT_CONFIG_FILENAMES = [ - "next.config.js", - "next.config.mjs", - "next.config.ts", - "next.config.mts", -]; +export type PreviewBuildType = typeof PREVIEW_BUILD_TYPES[number]; +export type ResolvedPreviewBuildType = Exclude; export class PreviewBuildStrategy implements BuildStrategy { readonly #appPath: string; @@ -83,150 +91,65 @@ export async function resolvePreviewBuildStrategy(options: { strategy: BuildStrategy; buildType: ResolvedPreviewBuildType; }> { - if (options.buildType === "nextjs") { - return { - buildType: "nextjs", - strategy: new PreviewNextjsBuild({ appPath: options.appPath }), - }; - } + if (options.buildType !== "auto") { + const strategy = await createPreviewBuildStrategy({ + appPath: options.appPath, + entrypoint: options.entrypoint, + buildType: options.buildType, + }); - if (options.buildType === "bun") { - const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint); return { - buildType: "bun", - strategy: new BunBuild({ - appPath: options.appPath, - entrypoint, - }), + buildType: options.buildType, + strategy, }; } - const nextjsStrategy = new PreviewNextjsBuild({ appPath: options.appPath }); - if (await nextjsStrategy.canBuild()) { - return { - buildType: "nextjs", - strategy: nextjsStrategy, - }; + for (const buildType of ["nextjs", "nuxt", "astro", "tanstack-start"] as const) { + const strategy = await createPreviewBuildStrategy({ + appPath: options.appPath, + entrypoint: options.entrypoint, + buildType, + }); + + if (await strategy.canBuild()) { + return { + buildType, + strategy, + }; + } } - const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint); return { buildType: "bun", - strategy: new BunBuild({ + strategy: await createPreviewBuildStrategy({ appPath: options.appPath, - entrypoint, + entrypoint: options.entrypoint, + buildType: "bun", }), }; } -class PreviewNextjsBuild implements BuildStrategy { - readonly #appPath: string; - - constructor(options: { appPath: string }) { - this.#appPath = options.appPath; - } - - async canBuild(): Promise { - return (await this.#hasNextConfig()) || (await this.#hasNextDependency()); - } - - async execute(): Promise { - await this.#runBuild(); - - const standaloneDir = path.join(this.#appPath, ".next", "standalone"); - const standaloneStat = await stat(standaloneDir).catch(() => null); - if (!standaloneStat?.isDirectory()) { - throw new Error('Next.js build did not produce standalone output. Add output: "standalone" to your next.config file.'); - } - - const outDir = await mkdtemp(path.join(os.tmpdir(), "compute-build-")); - - try { - const artifactDir = path.join(outDir, "app"); - await stageNextjsStandaloneArtifact({ - standaloneDir, - artifactDir, - appPath: this.#appPath, +async function createPreviewBuildStrategy(options: { + appPath: string; + entrypoint?: string; + buildType: ResolvedPreviewBuildType; +}): Promise { + switch (options.buildType) { + case "nextjs": + return new NextjsBuild({ appPath: options.appPath }); + case "nuxt": + return new NuxtBuild({ appPath: options.appPath }); + case "astro": + return new AstroBuild({ appPath: options.appPath }); + case "tanstack-start": + return new TanstackStartBuild({ appPath: options.appPath }); + case "bun": { + const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint); + return new BunBuild({ + appPath: options.appPath, + entrypoint, }); - - const publicDir = path.join(this.#appPath, "public"); - if (await directoryExists(publicDir)) { - await cp(publicDir, path.join(artifactDir, "public"), { recursive: true }); - } - - const staticDir = path.join(this.#appPath, ".next", "static"); - if (await directoryExists(staticDir)) { - await cp(staticDir, path.join(artifactDir, ".next", "static"), { recursive: true }); - } - - return { - directory: artifactDir, - entrypoint: "server.js", - defaultPortMapping: { http: 3000 }, - cleanup: () => rm(outDir, { recursive: true, force: true }), - }; - } catch (error) { - await rm(outDir, { recursive: true, force: true }); - throw error; - } - } - - async #hasNextConfig(): Promise { - let entries: string[]; - try { - entries = await readdir(this.#appPath); - } catch { - return false; - } - - return entries.some((entry) => NEXT_CONFIG_FILENAMES.includes(entry)); - } - - async #hasNextDependency(): Promise { - const packageJsonPath = path.join(this.#appPath, "package.json"); - let content: string; - - try { - content = await readFile(packageJsonPath, "utf8"); - } catch { - return false; - } - - let parsed: Record; - try { - parsed = JSON.parse(content) as Record; - } catch { - return false; } - - const deps = isRecord(parsed.dependencies) ? parsed.dependencies : {}; - const devDeps = isRecord(parsed.devDependencies) ? parsed.devDependencies : {}; - - return "next" in deps || "next" in devDeps; - } - - async #runBuild(): Promise { - const localBin = path.join(this.#appPath, "node_modules", ".bin", "next"); - const candidates = [ - { command: localBin, args: ["build"] }, - { command: "npx", args: ["next", "build"] }, - { command: "bunx", args: ["next", "build"] }, - ]; - - for (const { command, args } of candidates) { - try { - await exec(command, args, this.#appPath); - return; - } catch (error) { - if (error instanceof Error && "code" in error && error.code === "ENOENT") { - continue; - } - - throw error; - } - } - - throw new Error("Could not find the Next.js CLI. Install it with `npm install next` or ensure npx/bunx is available."); } } @@ -377,36 +300,6 @@ async function resolveSymlinkTarget( ); } -async function directoryExists(dirPath: string): Promise { - const dirStat = await stat(dirPath).catch(() => null); - return dirStat?.isDirectory() ?? false; -} - -function exec(command: string, args: string[], cwd: string): Promise { - return new Promise((resolve, reject) => { - execFile(command, args, { cwd }, (error, _stdout, stderr) => { - if (error) { - if ("code" in error && error.code === "ENOENT") { - reject(Object.assign(new Error(`${command} not found`), { - code: "ENOENT", - })); - return; - } - - const message = stderr.trim() || error.message; - reject(new Error(`Next.js build failed:\n${message}`)); - return; - } - - resolve(); - }); - }); -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - async function pathExists(targetPath: string): Promise { try { await stat(targetPath); diff --git a/packages/cli/src/shell/command-meta.ts b/packages/cli/src/shell/command-meta.ts index c770c1f..edd530c 100644 --- a/packages/cli/src/shell/command-meta.ts +++ b/packages/cli/src/shell/command-meta.ts @@ -60,8 +60,12 @@ const DESCRIPTORS: CommandDescriptor[] = [ id: "app", path: ["prisma", "app"], description: "App deployment and release commands.", - docsPath: "docs/product/command-spec.md#prisma-app-deploy---app-name---entry-path---build-type-autobunnextjs---http-port-port---env-namevalue", - examples: ["prisma app build --build-type nextjs", "prisma app deploy --app hello-world --build-type nextjs --http-port 3000"], + docsPath: "docs/product/command-spec.md#prisma-app-deploy---app-name---entry-path---build-type-autobunnextjsnuxtastrotanstack-start---http-port-port---env-namevalue", + examples: [ + "prisma app build --build-type nextjs", + "prisma app deploy --app hello-world --build-type nextjs --http-port 3000", + "prisma app deploy --app hello-world --build-type nuxt", + ], }, { id: "branch", @@ -116,8 +120,8 @@ const DESCRIPTORS: CommandDescriptor[] = [ id: "app.build", path: ["prisma", "app", "build"], description: "Build the local app into a deployable artifact.", - docsPath: "docs/product/command-spec.md#prisma-app-build---entry-path---build-type-autobunnextjs", - examples: ["prisma app build --build-type nextjs", "prisma app build --build-type bun --entry server.ts"], + docsPath: "docs/product/command-spec.md#prisma-app-build---entry-path---build-type-autobunnextjsnuxtastrotanstack-start", + examples: ["prisma app build --build-type nextjs", "prisma app build --build-type nuxt", "prisma app build --build-type bun --entry server.ts"], }, { id: "app.run", @@ -130,11 +134,12 @@ const DESCRIPTORS: CommandDescriptor[] = [ id: "app.deploy", path: ["prisma", "app", "deploy"], description: "Build and release the selected app.", - docsPath: "docs/product/command-spec.md#prisma-app-deploy---app-name---entry-path---build-type-autobunnextjs---http-port-port---env-namevalue", + docsPath: "docs/product/command-spec.md#prisma-app-deploy---app-name---entry-path---build-type-autobunnextjsnuxtastrotanstack-start---http-port-port---env-namevalue", examples: [ "prisma app deploy", "prisma app deploy --app hello-world --env DATABASE_URL=postgresql://example", "prisma app deploy --app hello-world --build-type nextjs --http-port 3000", + "prisma app deploy --app hello-world --build-type nuxt", ], }, { diff --git a/packages/cli/src/types/app.ts b/packages/cli/src/types/app.ts index 6c89d8c..7774eba 100644 --- a/packages/cli/src/types/app.ts +++ b/packages/cli/src/types/app.ts @@ -45,7 +45,7 @@ export interface AppShowResult { export interface AppBuildResult { directory: string; entrypoint: string | null; - buildType: "bun" | "nextjs"; + buildType: "bun" | "nextjs" | "nuxt" | "astro" | "tanstack-start"; } export interface AppShowDeployResult { diff --git a/packages/cli/tests/app-bun-compat.test.ts b/packages/cli/tests/app-bun-compat.test.ts index d9dd304..9047c80 100644 --- a/packages/cli/tests/app-bun-compat.test.ts +++ b/packages/cli/tests/app-bun-compat.test.ts @@ -79,10 +79,17 @@ describe("bun compatibility", () => { canBuild: vi.fn().mockResolvedValue(false), execute: vi.fn(), })); + const otherFrameworkBuild = vi.fn().mockImplementation(() => ({ + canBuild: vi.fn().mockResolvedValue(false), + execute: vi.fn(), + })); vi.doMock("@prisma/compute-sdk", () => ({ + AstroBuild: otherFrameworkBuild, BunBuild: bunBuild, NextjsBuild: nextjsBuild, + NuxtBuild: otherFrameworkBuild, + TanstackStartBuild: otherFrameworkBuild, })); const { resolvePreviewBuildStrategy } = await import("../src/lib/app/preview-build"); @@ -100,6 +107,84 @@ describe("bun compatibility", () => { }); }); + it("auto-detects SDK framework strategies before falling back to Bun", async () => { + const cwd = await createTempCwd(); + + const bunBuild = vi.fn().mockImplementation(() => ({ + canBuild: vi.fn().mockResolvedValue(true), + execute: vi.fn(), + })); + const nextjsBuild = vi.fn().mockImplementation(() => ({ + canBuild: vi.fn().mockResolvedValue(false), + execute: vi.fn(), + })); + const nuxtBuild = vi.fn().mockImplementation(() => ({ + canBuild: vi.fn().mockResolvedValue(true), + execute: vi.fn(), + })); + const astroBuild = vi.fn().mockImplementation(() => ({ + canBuild: vi.fn().mockResolvedValue(true), + execute: vi.fn(), + })); + const tanstackStartBuild = vi.fn().mockImplementation(() => ({ + canBuild: vi.fn().mockResolvedValue(true), + execute: vi.fn(), + })); + + vi.doMock("@prisma/compute-sdk", () => ({ + AstroBuild: astroBuild, + BunBuild: bunBuild, + NextjsBuild: nextjsBuild, + NuxtBuild: nuxtBuild, + TanstackStartBuild: tanstackStartBuild, + })); + + const { resolvePreviewBuildStrategy } = await import("../src/lib/app/preview-build"); + + const result = await resolvePreviewBuildStrategy({ + appPath: cwd, + buildType: "auto", + entrypoint: undefined, + }); + + expect(result.buildType).toBe("nuxt"); + expect(nuxtBuild).toHaveBeenCalledWith({ appPath: cwd }); + expect(bunBuild).not.toHaveBeenCalled(); + expect(astroBuild).not.toHaveBeenCalled(); + expect(tanstackStartBuild).not.toHaveBeenCalled(); + }); + + it("resolves explicit SDK framework build strategies", async () => { + const cwd = await createTempCwd(); + + const buildStrategy = vi.fn().mockImplementation(() => ({ + canBuild: vi.fn(), + execute: vi.fn(), + })); + + vi.doMock("@prisma/compute-sdk", () => ({ + AstroBuild: buildStrategy, + BunBuild: buildStrategy, + NextjsBuild: buildStrategy, + NuxtBuild: buildStrategy, + TanstackStartBuild: buildStrategy, + })); + + const { resolvePreviewBuildStrategy } = await import("../src/lib/app/preview-build"); + + await expect(resolvePreviewBuildStrategy({ + appPath: cwd, + buildType: "astro", + entrypoint: undefined, + })).resolves.toMatchObject({ buildType: "astro" }); + + await expect(resolvePreviewBuildStrategy({ + appPath: cwd, + buildType: "tanstack-start", + entrypoint: undefined, + })).resolves.toMatchObject({ buildType: "tanstack-start" }); + }); + it("still lets an explicit Bun entrypoint override package.json module", async () => { const cwd = await createTempCwd(); diff --git a/packages/cli/tests/app-local-dev.test.ts b/packages/cli/tests/app-local-dev.test.ts index fc25886..61219bd 100644 --- a/packages/cli/tests/app-local-dev.test.ts +++ b/packages/cli/tests/app-local-dev.test.ts @@ -52,16 +52,60 @@ describe("app local dev commands", () => { }); }); + it("build accepts explicit SDK framework strategies", async () => { + const executePreviewBuild = vi.fn().mockResolvedValue({ + artifact: { + directory: "/tmp/compute-build/app", + entrypoint: "server/entry.mjs", + }, + buildType: "astro", + }); + + vi.doMock("../src/lib/app/preview-build", async () => { + const actual = await vi.importActual( + "../src/lib/app/preview-build", + ); + return { + ...actual, + executePreviewBuild, + }; + }); + + const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { runAppBuild } = await import("../src/controllers/app"); + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + const { context } = await createTestCommandContext({ + cwd, + stateDir, + }); + + const result = await runAppBuild(context, undefined, "astro"); + + expect(executePreviewBuild).toHaveBeenCalledWith({ + appPath: cwd, + entrypoint: undefined, + buildType: "astro", + }); + expect(result.result).toEqual({ + directory: "/tmp/compute-build/app", + entrypoint: "server/entry.mjs", + buildType: "astro", + }); + }); + it("build returns USAGE_ERROR when framework detection is ambiguous", async () => { - const resolveLocalBuildType = vi.fn().mockResolvedValue(null); + const executePreviewBuild = vi.fn().mockRejectedValue( + new Error("Entrypoint is required. Pass --entry or define package.json main or module."), + ); - vi.doMock("../src/lib/app/local-dev", async () => { - const actual = await vi.importActual( - "../src/lib/app/local-dev", + vi.doMock("../src/lib/app/preview-build", async () => { + const actual = await vi.importActual( + "../src/lib/app/preview-build", ); return { ...actual, - resolveLocalBuildType, + executePreviewBuild, }; }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e27b3ee..d10a73f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^1.2.0 version: 1.2.0 '@prisma/compute-sdk': - specifier: ^0.14.0 - version: 0.14.0(@prisma/management-api-sdk@1.24.0) + specifier: ^0.17.0 + version: 0.17.0(@prisma/management-api-sdk@1.24.0) '@prisma/credentials-store': specifier: ^7.7.0 version: 7.7.0 @@ -283,8 +283,8 @@ packages: '@oxc-project/types@0.124.0': resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} - '@prisma/compute-sdk@0.14.0': - resolution: {integrity: sha512-xj464hjOs+Yus6ob2qS5vPMh3kY96nFWug5fVpHioZ8qOhd+/3jM8r56+CfdIiO+0bGk7z1yTvPWpTx1gDjVLQ==} + '@prisma/compute-sdk@0.17.0': + resolution: {integrity: sha512-YfCmszlEMYndDyVT17jiAZEHOMXlVq/lJKv9cc6KT9Zn2rPdR/MKvsPhQZg4E44vgJuPXMKieULOKUwFz0aj2g==} engines: {node: '>=18.0.0'} peerDependencies: '@prisma/management-api-sdk': '>=1.23.0' @@ -1312,7 +1312,7 @@ snapshots: '@oxc-project/types@0.124.0': {} - '@prisma/compute-sdk@0.14.0(@prisma/management-api-sdk@1.24.0)': + '@prisma/compute-sdk@0.17.0(@prisma/management-api-sdk@1.24.0)': dependencies: '@prisma/management-api-sdk': 1.24.0 better-result: 2.8.2 From 0ebfa2e3813241829ab31a2e7b7732f954edee00 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Wed, 6 May 2026 14:03:39 +0200 Subject: [PATCH 2/2] Address app build review comments --- packages/cli/src/controllers/app.ts | 34 +++++++++++------------ packages/cli/src/lib/app/preview-build.ts | 9 +++++- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/controllers/app.ts b/packages/cli/src/controllers/app.ts index c58c9d6..2cdda2c 100644 --- a/packages/cli/src/controllers/app.ts +++ b/packages/cli/src/controllers/app.ts @@ -31,7 +31,12 @@ import { runLocalApp, } from "../lib/app/local-dev"; import { projectNotFoundError } from "../use-cases/project"; -import { executePreviewBuild, PREVIEW_BUILD_TYPES, type PreviewBuildType } from "../lib/app/preview-build"; +import { + executePreviewBuild, + PREVIEW_BUILD_TYPES, + RESOLVED_PREVIEW_BUILD_TYPES, + type PreviewBuildType, +} from "../lib/app/preview-build"; import { createPreviewDeployInteraction, PREVIEW_DEFAULT_REGION, @@ -77,15 +82,9 @@ export async function runAppBuild( if (buildType === "auto" && isAutoBuildDetectionError(error)) { throw usageError( "App build requires an explicit framework when detection is ambiguous", - "This preview auto-detects clear Bun, Next.js, Nuxt, Astro, or TanStack Start project shapes.", + `This preview auto-detects clear project shapes for ${RESOLVED_PREVIEW_BUILD_TYPES.map(formatBuildTypeName).join(", ")}.`, "Pass a supported --build-type value, or pass --entry for a Bun app.", - [ - "prisma app build --build-type nextjs", - "prisma app build --build-type nuxt", - "prisma app build --build-type astro", - "prisma app build --build-type tanstack-start", - "prisma app build --build-type bun --entry server.ts", - ], + getBuildTypeExamples("build"), "app", ); } @@ -1237,15 +1236,9 @@ function normalizeBuildType(requestedBuildType: string | undefined): PreviewBuil throw usageError( `Unsupported build type "${requestedBuildType}"`, - "Only auto, bun, nextjs, nuxt, astro, and tanstack-start are supported in the current preview.", + `Only ${PREVIEW_BUILD_TYPES.join(", ")} are supported in the current preview.`, "Pass a supported --build-type value.", - [ - "prisma app build --build-type nextjs", - "prisma app build --build-type nuxt", - "prisma app build --build-type astro", - "prisma app build --build-type tanstack-start", - "prisma app build --build-type bun --entry server.ts", - ], + getBuildTypeExamples("build"), "app", ); } @@ -1254,6 +1247,13 @@ function isPreviewBuildType(value: string): value is PreviewBuildType { return (PREVIEW_BUILD_TYPES as readonly string[]).includes(value); } +function getBuildTypeExamples(commandName: "build" | "deploy"): string[] { + return RESOLVED_PREVIEW_BUILD_TYPES.map((buildType) => { + const entrypoint = buildType === "bun" ? " --entry server.ts" : ""; + return `prisma app ${commandName} --build-type ${buildType}${entrypoint}`; + }); +} + function assertSupportedEntrypoint( buildType: PreviewBuildType, entrypoint: string | undefined, diff --git a/packages/cli/src/lib/app/preview-build.ts b/packages/cli/src/lib/app/preview-build.ts index 451ba9a..a439ced 100644 --- a/packages/cli/src/lib/app/preview-build.ts +++ b/packages/cli/src/lib/app/preview-build.ts @@ -24,6 +24,10 @@ export const PREVIEW_BUILD_TYPES = [ export type PreviewBuildType = typeof PREVIEW_BUILD_TYPES[number]; export type ResolvedPreviewBuildType = Exclude; +export const RESOLVED_PREVIEW_BUILD_TYPES = PREVIEW_BUILD_TYPES.filter( + (buildType): buildType is ResolvedPreviewBuildType => buildType !== "auto", +); + export class PreviewBuildStrategy implements BuildStrategy { readonly #appPath: string; readonly #entrypoint?: string; @@ -104,7 +108,10 @@ export async function resolvePreviewBuildStrategy(options: { }; } - for (const buildType of ["nextjs", "nuxt", "astro", "tanstack-start"] as const) { + for (const buildType of RESOLVED_PREVIEW_BUILD_TYPES) { + // Bun is the fallback because it can build any valid Bun entrypoint. + if (buildType === "bun") continue; + const strategy = await createPreviewBuildStrategy({ appPath: options.appPath, entrypoint: options.entrypoint,