Skip to content

Commit 28399f1

Browse files
fix(cli): address init review feedback
- --entry now fails with a usage error for frameworks that derive their entrypoint from build output, instead of being silently dropped - entry resolution moved after the interactive adjust step and validates against the final framework, so a framework switch cannot write a stale or missing entry - the post-write types step degrades to a skip with a warning when package.json is unreadable, so a malformed file cannot fail the command after the config was written (retry no longer gets stuck on INIT_CONFIG_EXISTS) - init presenter drops its bespoke warning lines now that the runner renders success warnings (cherry-picked from the project PR) - INIT_CONFIG_EXISTS and INIT_DETECTION_FAILED registered in error-conventions.md
1 parent 6a0f2b3 commit 28399f1

4 files changed

Lines changed: 105 additions & 23 deletions

File tree

docs/product/error-conventions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ These codes are the minimum stable set for the MVP:
187187
- `BUILD_SETTINGS_MIGRATION_REQUIRED`
188188
- `BUILD_SETTINGS_UNSUPPORTED`
189189
- `FRAMEWORK_NOT_DETECTED`
190+
- `INIT_CONFIG_EXISTS`
191+
- `INIT_DETECTION_FAILED`
190192
- `DEPLOYMENT_NOT_FOUND`
191193
- `NO_DEPLOYMENTS`
192194
- `NO_PREVIOUS_DEPLOYMENT`
@@ -258,6 +260,8 @@ Recommended meanings:
258260
- `BUILD_SETTINGS_MIGRATION_REQUIRED`: a legacy `prisma.app.json` contains custom build settings that must move into the `build` block of `prisma.compute.ts`
259261
- `BUILD_SETTINGS_UNSUPPORTED`: a compute config `build` block targets a framework whose SDK strategy does not consume committed build settings
260262
- `FRAMEWORK_NOT_DETECTED`: app deploy could not detect a supported Beta framework and no explicit framework/build type was provided
263+
- `INIT_CONFIG_EXISTS`: a compute config already exists in this directory or an ancestor; init never overwrites or merges
264+
- `INIT_DETECTION_FAILED`: no supported framework detected and no --framework passed; `meta.frameworks` lists the valid values
261265
- `DEPLOYMENT_NOT_FOUND`: requested deployment id does not exist
262266
- `NO_DEPLOYMENTS`: command resolved a branch or app but found no deployments
263267
- `NO_PREVIOUS_DEPLOYMENT`: rollback could not find an earlier deployment for the selected app

packages/cli/src/controllers/init.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,16 @@ export async function runInit(
8383
value: defaultHttpPortForBuildType(frameworkByKey(framework.key).buildType),
8484
source: "framework default",
8585
};
86-
const entry = await resolveInitEntry(cwd, framework.key, flags.entry, signal);
87-
8886
const adjusted = await maybeAdjustSettings(context, framework, httpPort, {
8987
portExplicit: flags.httpPort !== undefined,
9088
});
9189
framework = adjusted.framework;
9290
httpPort = adjusted.httpPort;
9391

92+
// Entry resolves against the FINAL framework so an interactive framework
93+
// switch cannot leave a stale (or missing) entry in the written config.
94+
const entry = await resolveInitEntry(cwd, framework, flags.entry, signal);
95+
9496
const settings: InitSettingRow[] = [
9597
{ key: "app", value: name.value, source: name.source },
9698
{
@@ -203,7 +205,25 @@ async function resolveInitTypes(
203205
hooks: { onWarning: (message: string) => void },
204206
): Promise<InitTypesState> {
205207
const cwd = context.runtime.cwd;
206-
const packageJson = await readBunPackageJson(cwd, context.runtime.signal);
208+
// This step runs after prisma.compute.ts is written; an unreadable
209+
// package.json (malformed JSON, permissions) must not turn the already
210+
// successful write into a command failure, so it degrades to a skip.
211+
let packageJson: Awaited<ReturnType<typeof readBunPackageJson>>;
212+
try {
213+
packageJson = await readBunPackageJson(cwd, context.runtime.signal);
214+
} catch (error) {
215+
if (context.runtime.signal.aborted) {
216+
throw error;
217+
}
218+
hooks.onWarning(
219+
`Skipped the ${COMPUTE_SDK_PACKAGE} types install: package.json could not be read (${error instanceof Error ? error.message.split("\n")[0] : String(error)}).`,
220+
);
221+
return {
222+
status: "skipped",
223+
package: COMPUTE_SDK_PACKAGE,
224+
installCommand: null,
225+
};
226+
}
207227
if (hasComputeSdkDependency(packageJson)) {
208228
return {
209229
status: "already-installed",
@@ -496,16 +516,25 @@ function parseInitRegion(
496516

497517
async function resolveInitEntry(
498518
cwd: string,
499-
frameworkKey: ComputeFramework,
519+
resolvedFramework: ResolvedInitFramework,
500520
explicitEntry: string | undefined,
501521
signal: AbortSignal,
502522
): Promise<{ value: string; source: string } | undefined> {
503-
const framework = frameworkByKey(frameworkKey);
523+
const framework = frameworkByKey(resolvedFramework.key);
524+
const trimmed = explicitEntry?.trim();
504525
if (!framework.usesEntrypoint) {
526+
if (trimmed) {
527+
throw usageError(
528+
"--entry is not supported for this framework",
529+
`${resolvedFramework.displayName} derives its entrypoint from build output; --entry applies only to frameworks that run a source entrypoint (Bun, Hono).`,
530+
"Drop --entry, or pass an entrypoint framework with --framework.",
531+
[],
532+
"app",
533+
);
534+
}
505535
return undefined;
506536
}
507537

508-
const trimmed = explicitEntry?.trim();
509538
if (trimmed) {
510539
return { value: trimmed, source: "flag" };
511540
}

packages/cli/src/presenters/init.ts

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export function renderInit(
1717
renderSummaryLine(ui, "success", `Wrote ${result.configPath}`),
1818
];
1919

20+
// Failed steps surface through the runner's success-warning rendering, so
21+
// this block only covers the success and quiet-hint cases.
2022
if (result.types.status === "installed") {
2123
lines.push(
2224
renderSummaryLine(
@@ -25,16 +27,8 @@ export function renderInit(
2527
`Installed ${result.types.package} (config types)`,
2628
),
2729
);
28-
} else if (result.types.status === "failed" && result.types.installCommand) {
29-
lines.push(
30-
renderSummaryLine(
31-
ui,
32-
"warning",
33-
`Could not install ${result.types.package}; install later with ${result.types.installCommand}`,
34-
),
35-
);
3630
} else if (
37-
result.types.status !== "already-installed" &&
31+
(result.types.status === "skipped" || result.types.status === "declined") &&
3832
result.types.installCommand
3933
) {
4034
lines.push(
@@ -61,14 +55,7 @@ export function renderInit(
6155
);
6256
break;
6357
case "failed":
64-
// Human mode does not render success.warnings, so surface it here.
65-
lines.push(
66-
renderSummaryLine(
67-
ui,
68-
"warning",
69-
`Project link failed; link later with ${formatCommand(["project", "link"])}`,
70-
),
71-
);
58+
// The failure detail renders via the runner's success-warning lines.
7259
break;
7360
}
7461

packages/cli/tests/init.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,3 +411,65 @@ describe("init types install", () => {
411411
expect(payload.warnings[0]).toContain("npm install -D @prisma/compute-sdk");
412412
});
413413
});
414+
415+
describe("init edge cases", () => {
416+
it("rejects --entry for frameworks that derive their entrypoint", async () => {
417+
const cwd = await createTempCwd();
418+
const stateDir = path.join(cwd, ".state");
419+
await writePackageJson(cwd, { name: "web" });
420+
421+
const result = await executeCli({
422+
argv: [
423+
"init",
424+
"--framework",
425+
"nextjs",
426+
"--entry",
427+
"src/index.ts",
428+
"--json",
429+
],
430+
cwd,
431+
stateDir,
432+
fixturePath,
433+
});
434+
const payload = JSON.parse(result.stdout);
435+
436+
expect(result.exitCode).toBe(2);
437+
expect(payload).toMatchObject({
438+
ok: false,
439+
command: "init",
440+
error: { code: "USAGE_ERROR" },
441+
});
442+
await expect(readConfig(cwd)).rejects.toMatchObject({ code: "ENOENT" });
443+
});
444+
445+
it("keeps the written config when package.json is unreadable in the types step", async () => {
446+
const cwd = await createTempCwd();
447+
const stateDir = path.join(cwd, ".state");
448+
await writeFile(path.join(cwd, "package.json"), "{ not json", "utf8");
449+
450+
const result = await executeCli({
451+
argv: [
452+
"init",
453+
"--framework",
454+
"hono",
455+
"--entry",
456+
"src/index.ts",
457+
"--name",
458+
"api",
459+
"--no-link",
460+
"--install",
461+
"--json",
462+
],
463+
cwd,
464+
stateDir,
465+
fixturePath,
466+
});
467+
const payload = JSON.parse(result.stdout);
468+
469+
expect(result.exitCode).toBe(0);
470+
expect(payload.ok).toBe(true);
471+
expect(payload.result.types.status).toBe("skipped");
472+
expect(payload.warnings[0]).toContain("package.json could not be read");
473+
await expect(readConfig(cwd)).resolves.toContain('framework: "hono"');
474+
});
475+
});

0 commit comments

Comments
 (0)