Skip to content

Commit 00ab047

Browse files
feat: add dedicated init template for smart init flow
1 parent 80b5eb0 commit 00ab047

9 files changed

Lines changed: 295 additions & 40 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ The create flow supports:
155155
- project name via `--name`
156156
- schema presets via `--schema-preset empty|basic` (default: `basic`)
157157

158-
When initializing an existing project, it adds the standard Prisma setup: `prisma/schema.prisma`, `prisma/seed.ts`, `prisma.config.ts`, and `src/lib/prisma.ts`.
158+
When initializing an existing project, it adds the standard Prisma setup from the dedicated init template and asks where to place the Prisma instance file, defaulting to `src/lib/prisma.ts`.
159159

160160
`create` prompts for database choice, package manager, and whether to install dependencies now.
161161
Supported providers in this flow: `postgresql`, `mysql`, `sqlite`, `sqlserver`, `cockroachdb`.

src/commands/init.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import {
22
cancel,
33
intro,
4+
isCancel,
45
log,
56
spinner,
7+
text,
68
} from "@clack/prompts";
79
import fs from "fs-extra";
810
import path from "node:path";
@@ -17,6 +19,7 @@ import {
1719
import { getCreatePrismaIntro } from "../ui/branding";
1820

1921
const DEFAULT_SCHEMA_PRESET: SchemaPreset = "basic";
22+
const DEFAULT_PRISMA_INSTANCE_PATH = "src/lib/prisma.ts";
2023

2124
async function readProjectPackageJson(projectDir: string): Promise<Record<string, unknown> | undefined> {
2225
const packageJsonPath = path.join(projectDir, "package.json");
@@ -75,6 +78,72 @@ export async function shouldInitCurrentProject(
7578
return hasPackageJson(projectDir);
7679
}
7780

81+
function normalizePrismaInstancePath(input: string): string | undefined {
82+
const trimmed = input.trim();
83+
if (trimmed.length === 0) {
84+
return undefined;
85+
}
86+
87+
if (path.isAbsolute(trimmed)) {
88+
return undefined;
89+
}
90+
91+
const normalizedPath = trimmed.replace(/\\/g, "/");
92+
if (
93+
normalizedPath === "." ||
94+
normalizedPath === ".." ||
95+
normalizedPath.startsWith("../") ||
96+
normalizedPath.includes("/../") ||
97+
normalizedPath.endsWith("/")
98+
) {
99+
return undefined;
100+
}
101+
102+
const existingExtension = path.posix.extname(normalizedPath);
103+
if (existingExtension.length > 0 && existingExtension !== ".ts") {
104+
return undefined;
105+
}
106+
107+
const withExtension =
108+
existingExtension === ".ts" ? normalizedPath : `${normalizedPath}.ts`;
109+
110+
return withExtension;
111+
}
112+
113+
async function promptForPrismaInstancePath(): Promise<string | undefined> {
114+
const prismaInstancePath = await text({
115+
message: "Where should the Prisma instance live?",
116+
placeholder: DEFAULT_PRISMA_INSTANCE_PATH,
117+
initialValue: DEFAULT_PRISMA_INSTANCE_PATH,
118+
validate: (value) => {
119+
const normalizedPath = normalizePrismaInstancePath(String(value ?? ""));
120+
121+
if (!normalizedPath) {
122+
return "Use a relative .ts path inside the project, for example src/lib/prisma.ts.";
123+
}
124+
125+
return undefined;
126+
},
127+
});
128+
129+
if (isCancel(prismaInstancePath)) {
130+
cancel("Operation cancelled.");
131+
return undefined;
132+
}
133+
134+
return normalizePrismaInstancePath(String(prismaInstancePath));
135+
}
136+
137+
async function resolvePrismaInstancePath(
138+
input: PrismaSetupCommandInput
139+
): Promise<string | undefined> {
140+
if (input.yes === true) {
141+
return DEFAULT_PRISMA_INSTANCE_PATH;
142+
}
143+
144+
return promptForPrismaInstancePath();
145+
}
146+
78147
export async function runInitCommand(
79148
rawInput: PrismaSetupCommandInput = {},
80149
options: {
@@ -107,16 +176,21 @@ export async function runInitCommand(
107176
return;
108177
}
109178

179+
const prismaInstancePath = await resolvePrismaInstancePath(input);
180+
if (!prismaInstancePath) {
181+
return;
182+
}
183+
110184
const initSpinner = spinner();
111185
initSpinner.start("Scaffolding Prisma files for the current project...");
112186

113187
try {
114188
const renderResult = await scaffoldInitTemplate({
115189
projectDir,
116-
projectName: path.basename(projectDir),
117190
provider: prismaSetupContext.databaseProvider,
118191
schemaPreset: prismaSetupContext.schemaPreset,
119192
packageManager: prismaSetupContext.packageManager,
193+
singletonPath: prismaInstancePath,
120194
});
121195
const fileAction =
122196
renderResult.writtenFiles.length > 0
@@ -142,6 +216,7 @@ export async function runInitCommand(
142216
await executePrismaSetupContext(prismaSetupContext, {
143217
projectDir,
144218
includeDevNextStep: await hasPackageScript(projectDir, "dev"),
219+
singletonPath: prismaInstancePath,
145220
});
146221
} catch (error) {
147222
cancel(

src/tasks/setup-prisma.ts

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ const requiredPrismaFileGroups = [
6767
],
6868
] as const;
6969

70+
const defaultSingletonCandidates = requiredPrismaFileGroups[3];
71+
7072
async function resolvePrismaProjectDir(projectDir: string): Promise<string> {
7173
const monorepoDbDir = path.join(projectDir, "packages/db");
7274
if (await fs.pathExists(path.join(monorepoDbDir, "prisma/schema.prisma"))) {
@@ -414,10 +416,21 @@ async function ensureGitignoreEntry(
414416
return { gitignorePath, status: "appended" };
415417
}
416418

417-
async function ensureRequiredPrismaFiles(projectDir: string): Promise<void> {
419+
async function ensureRequiredPrismaFiles(
420+
projectDir: string,
421+
singletonPath?: string
422+
): Promise<void> {
418423
const missingFiles: string[] = [];
419-
420-
for (const candidates of requiredPrismaFileGroups) {
424+
const singletonCandidates = singletonPath
425+
? [singletonPath]
426+
: [...defaultSingletonCandidates];
427+
428+
for (const candidates of [
429+
requiredPrismaFileGroups[0],
430+
requiredPrismaFileGroups[1],
431+
requiredPrismaFileGroups[2],
432+
singletonCandidates,
433+
] as const) {
421434
let foundCandidate = false;
422435

423436
for (const relativePath of candidates) {
@@ -448,16 +461,18 @@ async function finalizePrismaFiles(
448461
const schemaPath = path.join(prismaProjectDir, "prisma/schema.prisma");
449462
const configPath = path.join(prismaProjectDir, "prisma.config.ts");
450463

451-
await ensureRequiredPrismaFiles(projectDir);
452-
const singletonPath = (await fs.pathExists(path.join(prismaProjectDir, "src/lib/prisma.ts")))
453-
? path.join(prismaProjectDir, "src/lib/prisma.ts")
454-
: (await fs.pathExists(path.join(prismaProjectDir, "src/lib/prisma.server.ts")))
455-
? path.join(prismaProjectDir, "src/lib/prisma.server.ts")
456-
: (await fs.pathExists(path.join(prismaProjectDir, "src/lib/server/prisma.ts")))
457-
? path.join(prismaProjectDir, "src/lib/server/prisma.ts")
458-
: (await fs.pathExists(path.join(prismaProjectDir, "server/utils/prisma.ts")))
459-
? path.join(prismaProjectDir, "server/utils/prisma.ts")
460-
: path.join(prismaProjectDir, "src/client.ts");
464+
await ensureRequiredPrismaFiles(projectDir, options.singletonPath);
465+
const singletonPath = options.singletonPath
466+
? path.join(projectDir, options.singletonPath)
467+
: (await fs.pathExists(path.join(prismaProjectDir, "src/lib/prisma.ts")))
468+
? path.join(prismaProjectDir, "src/lib/prisma.ts")
469+
: (await fs.pathExists(path.join(prismaProjectDir, "src/lib/prisma.server.ts")))
470+
? path.join(prismaProjectDir, "src/lib/prisma.server.ts")
471+
: (await fs.pathExists(path.join(prismaProjectDir, "src/lib/server/prisma.ts")))
472+
? path.join(prismaProjectDir, "src/lib/server/prisma.ts")
473+
: (await fs.pathExists(path.join(prismaProjectDir, "server/utils/prisma.ts")))
474+
? path.join(prismaProjectDir, "server/utils/prisma.ts")
475+
: path.join(prismaProjectDir, "src/client.ts");
461476
const generatedDir = (await fs.pathExists(path.join(prismaProjectDir, "server/utils/prisma.ts")))
462477
? "server/generated"
463478
: "src/generated";
@@ -597,7 +612,8 @@ async function installDependenciesForContext(
597612
async function finalizePrismaFilesForContext(
598613
context: PrismaSetupContext,
599614
projectDir: string,
600-
provisionResult: PrismaPostgresProvisionResult
615+
provisionResult: PrismaPostgresProvisionResult,
616+
singletonPath?: string
601617
): Promise<FinalizePrismaResult | undefined> {
602618
const initSpinner = spinner();
603619
initSpinner.start("Preparing Prisma files...");
@@ -608,6 +624,7 @@ async function finalizePrismaFilesForContext(
608624
databaseUrl: provisionResult.databaseUrl,
609625
claimUrl: provisionResult.claimUrl,
610626
projectDir,
627+
singletonPath,
611628
});
612629

613630
initSpinner.stop("Prisma files ready.");
@@ -739,7 +756,8 @@ export async function executePrismaSetupContext(
739756
const finalizeResult = await finalizePrismaFilesForContext(
740757
context,
741758
projectDir,
742-
provisionResult
759+
provisionResult,
760+
options.singletonPath
743761
);
744762
if (!finalizeResult) {
745763
return;

src/templates/render-init-template.ts

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import fs from "fs-extra";
22
import path from "node:path";
33

44
import type {
5-
CreateTemplateContext,
65
DatabaseProvider,
76
PackageManager,
87
SchemaPreset,
@@ -12,59 +11,78 @@ import {
1211
resolveTemplatesDir,
1312
} from "./shared";
1413

15-
const initTemplateFiles = [
14+
type InitTemplateContext = {
15+
provider: DatabaseProvider;
16+
schemaPreset: SchemaPreset;
17+
packageManager?: PackageManager;
18+
generatedClientImportPath: string;
19+
singletonImportPathFromSeed: string;
20+
};
21+
22+
const initTemplateRoot = resolveTemplatesDir("templates/init");
23+
24+
const staticInitTemplateFiles = [
1625
"prisma/schema.prisma.hbs",
1726
"prisma/seed.ts.hbs",
1827
"prisma.config.ts",
19-
"src/lib/prisma.ts.hbs",
2028
] as const;
21-
const initTemplateRoot = resolveTemplatesDir("templates/create/hono");
2229

2330
function stripHbsExtension(filePath: string): string {
2431
return filePath.endsWith(".hbs") ? filePath.slice(0, -4) : filePath;
2532
}
2633

27-
function createTemplateContext(
28-
projectName: string,
29-
provider: DatabaseProvider,
30-
schemaPreset: SchemaPreset,
31-
packageManager?: PackageManager
32-
): CreateTemplateContext {
33-
return {
34-
projectName,
35-
provider,
36-
schemaPreset,
37-
packageManager,
38-
};
34+
function toImportSpecifier(fromPath: string, toPath: string): string {
35+
const relativePath = path.relative(path.dirname(fromPath), toPath);
36+
const normalizedPath = relativePath.split(path.sep).join("/");
37+
const withoutExtension = normalizedPath.endsWith(".ts")
38+
? normalizedPath.slice(0, -3)
39+
: normalizedPath;
40+
41+
return withoutExtension.startsWith(".")
42+
? withoutExtension
43+
: `./${withoutExtension}`;
3944
}
4045

4146
export async function scaffoldInitTemplate(opts: {
4247
projectDir: string;
43-
projectName: string;
4448
provider: DatabaseProvider;
4549
schemaPreset: SchemaPreset;
4650
packageManager?: PackageManager;
51+
singletonPath: string;
4752
}): Promise<{
4853
writtenFiles: string[];
4954
skippedFiles: string[];
5055
}> {
5156
const {
5257
projectDir,
53-
projectName,
5458
provider,
5559
schemaPreset,
5660
packageManager,
61+
singletonPath,
5762
} = opts;
58-
const context = createTemplateContext(
59-
projectName,
63+
const singletonOutputPath = path.join(projectDir, singletonPath);
64+
const generatedClientOutputPath = path.join(
65+
projectDir,
66+
"src/generated/prisma/client.ts"
67+
);
68+
const seedOutputPath = path.join(projectDir, "prisma/seed.ts");
69+
const context: InitTemplateContext = {
6070
provider,
6171
schemaPreset,
62-
packageManager
63-
);
72+
packageManager,
73+
generatedClientImportPath: toImportSpecifier(
74+
singletonOutputPath,
75+
generatedClientOutputPath
76+
),
77+
singletonImportPathFromSeed: toImportSpecifier(
78+
seedOutputPath,
79+
singletonOutputPath
80+
),
81+
};
6482
const writtenFiles: string[] = [];
6583
const skippedFiles: string[] = [];
6684

67-
for (const relativeTemplatePath of initTemplateFiles) {
85+
for (const relativeTemplatePath of staticInitTemplateFiles) {
6886
const templateFilePath = path.join(initTemplateRoot, relativeTemplatePath);
6987
const outputPath = path.join(
7088
projectDir,
@@ -86,6 +104,23 @@ export async function scaffoldInitTemplate(opts: {
86104
}
87105
}
88106

107+
const singletonTemplatePath = path.join(
108+
initTemplateRoot,
109+
"prisma-instance.ts.hbs"
110+
);
111+
if (await fs.pathExists(singletonOutputPath)) {
112+
skippedFiles.push(singletonOutputPath);
113+
} else {
114+
await renderTemplateFile({
115+
templateFilePath: singletonTemplatePath,
116+
outputPath: singletonOutputPath,
117+
context,
118+
});
119+
if (await fs.pathExists(singletonOutputPath)) {
120+
writtenFiles.push(singletonOutputPath);
121+
}
122+
}
123+
89124
return {
90125
writtenFiles,
91126
skippedFiles,

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export type PrismaSetupRunOptions = {
136136
prependNextSteps?: string[];
137137
projectDir?: string;
138138
includeDevNextStep?: boolean;
139+
singletonPath?: string;
139140
};
140141

141142
export type PrismaSetupResult = {
@@ -219,6 +220,7 @@ export type FinalizePrismaOptions = {
219220
databaseUrl?: string;
220221
claimUrl?: string;
221222
projectDir?: string;
223+
singletonPath?: string;
222224
};
223225

224226
export type FinalizePrismaResult = {

0 commit comments

Comments
 (0)