Skip to content

Commit fc3edb4

Browse files
fix: apply committed build blocks under auto detection, doc fixes from review
app build with a config build block but no declared framework now detects the framework the way deploy does instead of silently ignoring the block (FRAMEWORK_NOT_DETECTED when nothing is detectable). Docs: retire APP_CONFIG_INVALID in favor of the COMPUTE_CONFIG_*/BUILD_SETTINGS_* codes and scope auto-creation (resolution step 7) to app deploy only.
1 parent af6ae14 commit fc3edb4

4 files changed

Lines changed: 95 additions & 4 deletions

File tree

docs/product/command-spec.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ Preview app commands that need an app resolve it in this order:
113113
4. locally selected app for non-deploy commands when it still exists in the resolved branch
114114
5. inferred app name from `package.json#name`
115115
6. current directory name
116-
7. create the inferred app in the resolved branch when no existing app matches
116+
7. `app deploy` only: create the inferred app in the resolved branch when no existing app matches
117117
8. interactive picker only when multiple matching apps make the target ambiguous
118118
9. `APP_AMBIGUOUS` in non-interactive or `--json` mode when unresolved
119119

@@ -123,7 +123,8 @@ upward config discovery as `app deploy`: the project binding is read from the
123123
config file's directory, and the config target is an additional app-name
124124
source. Unlike deploy, management commands never require a target: with
125125
multiple targets, no argument, and nothing inferred from the invocation
126-
directory, they fall back to the selection order above.
126+
directory, they fall back to the selection order above — except step 7;
127+
management commands never create apps or mutate remote state to resolve one.
127128

128129
`.prisma/local.json` pins the directory to a Workspace and Project only. It does
129130
not pin an App ID. App services are branch-scoped; a service ID from `main`

docs/product/error-conventions.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,11 @@ These codes are the minimum stable set for the MVP:
170170
- `LOCAL_STATE_WRITE_FAILED`
171171
- `LOCAL_STATE_STALE`
172172
- `BRANCH_NOT_DEPLOYABLE`
173-
- `APP_CONFIG_INVALID`
173+
- `COMPUTE_CONFIG_INVALID`
174+
- `COMPUTE_CONFIG_TARGET_REQUIRED`
175+
- `COMPUTE_CONFIG_TARGET_UNKNOWN`
176+
- `BUILD_SETTINGS_MIGRATION_REQUIRED`
177+
- `BUILD_SETTINGS_UNSUPPORTED`
174178
- `FRAMEWORK_NOT_DETECTED`
175179
- `DEPLOYMENT_NOT_FOUND`
176180
- `NO_DEPLOYMENTS`

packages/cli/src/controllers/app.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,17 @@ export async function runAppBuild(
161161
target: compute.target,
162162
});
163163
const appDir = await resolveComputeAppDir(context, compute);
164-
const buildType = normalizeBuildType(merged.buildType);
164+
let buildType = normalizeBuildType(merged.buildType);
165+
if (compute.target?.build && buildType === "auto") {
166+
// A committed build block must never be silently ignored, so resolve the
167+
// framework the same way deploy does instead of deferring to the
168+
// strategy's auto detection.
169+
const detected = await detectDeployFramework(appDir, context.runtime.signal);
170+
if (!detected) {
171+
throw frameworkNotDetectedError(appDir);
172+
}
173+
buildType = detected.buildType;
174+
}
165175
assertSupportedEntrypoint(buildType, merged.entrypoint, "build");
166176

167177
if (compute.target?.build && buildType !== "auto") {

packages/cli/tests/app-local-dev.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,82 @@ describe("app local dev commands", () => {
151151
});
152152
});
153153

154+
it("build applies a committed build block by detecting the framework instead of ignoring it", async () => {
155+
const executePreviewBuild = vi.fn().mockResolvedValue({
156+
artifact: {
157+
directory: "/tmp/compute-build/app",
158+
entrypoint: "server.js",
159+
},
160+
buildType: "nextjs",
161+
});
162+
163+
vi.doMock("../src/lib/app/preview-build", async () => {
164+
const actual = await vi.importActual<typeof import("../src/lib/app/preview-build")>(
165+
"../src/lib/app/preview-build",
166+
);
167+
return {
168+
...actual,
169+
executePreviewBuild,
170+
};
171+
});
172+
173+
const { createTempCwd, createTestCommandContext } = await import("./helpers");
174+
const { runAppBuild } = await import("../src/controllers/app");
175+
const cwd = await createTempCwd();
176+
await mkdir(path.join(cwd, "apps", "web"), { recursive: true });
177+
await writeFile(path.join(cwd, "apps", "web", "package.json"), JSON.stringify({
178+
name: "web",
179+
dependencies: { next: "15.0.0" },
180+
}), "utf8");
181+
// No framework declared: the build block still applies via detection.
182+
await writeFile(path.join(cwd, "prisma.compute.ts"), [
183+
"export default {",
184+
' apps: {',
185+
' web: { root: "apps/web", build: { command: "echo custom-build", outputDirectory: "out" } },',
186+
" },",
187+
"};",
188+
"",
189+
].join("\n"), "utf8");
190+
const { context } = await createTestCommandContext({
191+
cwd,
192+
stateDir: path.join(cwd, ".state"),
193+
});
194+
195+
await runAppBuild(context, { configTarget: "web" });
196+
197+
expect(executePreviewBuild).toHaveBeenCalledWith(expect.objectContaining({
198+
appPath: path.join(cwd, "apps", "web"),
199+
buildType: "nextjs",
200+
buildSettings: expect.objectContaining({
201+
buildCommand: "echo custom-build",
202+
outputDirectory: "out",
203+
}),
204+
}));
205+
});
206+
207+
it("build fails clearly when a build block exists but no framework is detectable", async () => {
208+
const { createTempCwd, createTestCommandContext } = await import("./helpers");
209+
const { runAppBuild } = await import("../src/controllers/app");
210+
const cwd = await createTempCwd();
211+
await mkdir(path.join(cwd, "apps", "mystery"), { recursive: true });
212+
await writeFile(path.join(cwd, "prisma.compute.ts"), [
213+
"export default {",
214+
' apps: {',
215+
' mystery: { root: "apps/mystery", build: { command: "make build", outputDirectory: "out" } },',
216+
" },",
217+
"};",
218+
"",
219+
].join("\n"), "utf8");
220+
const { context } = await createTestCommandContext({
221+
cwd,
222+
stateDir: path.join(cwd, ".state"),
223+
});
224+
225+
await expect(runAppBuild(context, { configTarget: "mystery" })).rejects.toMatchObject({
226+
code: "FRAMEWORK_NOT_DETECTED",
227+
});
228+
});
229+
154230
it("build accepts explicit SDK framework strategies", async () => {
155231
const executePreviewBuild = vi.fn().mockResolvedValue({
156232
artifact: {

0 commit comments

Comments
 (0)