|
import fs from "node:fs/promises"; |
|
import path from "node:path"; |
|
import { exec } from "node:child_process"; |
|
import { |
|
intro, |
|
outro, |
|
log, |
|
select, |
|
text, |
|
spinner, |
|
isCancel, |
|
confirm, |
|
} from "@clack/prompts"; |
|
import toml from "toml"; |
|
import parseArgv from "tiny-parse-argv"; |
|
|
|
const args = parseArgv(process.argv.slice(2)); |
|
const command = args._[0]; |
|
|
|
const projectRoot = path.resolve(); |
|
|
|
const asyncExec = (command: string) => |
|
new Promise<string>((resolve, reject) => { |
|
exec(command, (error, stdout, stderr) => { |
|
if (error) { |
|
reject(stderr); |
|
} else { |
|
resolve(stdout); |
|
} |
|
}); |
|
}); |
|
|
|
intro("D1 Prisma Migrate CLI"); |
|
|
|
if (args.help || !command) { |
|
switch (command) { |
|
case "create": |
|
log.message(`migrate create |
|
Create a new migration |
|
Options: |
|
-n, --name - The name of the migration |
|
-d, --database - The name of the D1 database |
|
--create-only - Only create the migration file, do not apply it |
|
--schema - Custom path to the Prisma schema |
|
-h, --help - Show this help message`); |
|
break; |
|
case "apply": |
|
log.message(`migrate apply |
|
Apply pending migrations |
|
Options: |
|
-d, --database - The name of the D1 database |
|
--remote - Apply migrations to your remote database |
|
--schema - Custom path to the Prisma schema |
|
-h, --help - Show this help message`); |
|
break; |
|
default: |
|
log.message(`migrate <command> |
|
Commands: |
|
create - Create a new migration |
|
apply - Apply pending migrations |
|
Options: |
|
-h, --help - Show this help message`); |
|
break; |
|
} |
|
process.exit(0); |
|
} |
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation> |
|
let wranglerConfig: any; |
|
|
|
// Check wrangler.toml to see what D1 namespaces are used |
|
try { |
|
const wranglerToml = await fs.readFile("wrangler.toml", "utf-8"); |
|
wranglerConfig = toml.parse(wranglerToml); |
|
} catch { |
|
log.error("Could not read wrangler.toml"); |
|
process.exit(1); |
|
} |
|
|
|
const databases: { value: string; label: string }[] = |
|
wranglerConfig.d1_databases?.map((db: { database_name: string }) => ({ |
|
value: db.database_name, |
|
label: db.database_name, |
|
})) || []; |
|
|
|
let database = args.d || args.database || databases[0]?.value; |
|
|
|
if (command === "create") { |
|
const database = await getDatabase(); |
|
|
|
const migrationName = |
|
args.name || |
|
args.n || |
|
(await text({ |
|
message: "What is the name of the migration?", |
|
validate: (input) => { |
|
if (input.length === 0) { |
|
return "Migration name cannot be empty"; |
|
} |
|
}, |
|
})); |
|
|
|
if (isCancel(migrationName)) { |
|
process.exit(1); |
|
} |
|
|
|
const s = spinner(); |
|
s.start("Creating migration"); |
|
const result = await asyncExec( |
|
`npx wrangler d1 migrations create ${database} ${migrationName}` |
|
); |
|
|
|
s.stop("Creating migration"); |
|
|
|
const migrationPath = result |
|
.trim() |
|
.split("\n") |
|
.find((line) => line.endsWith(".sql")); |
|
|
|
if (!migrationPath) { |
|
log.error("Could not find migration path"); |
|
process.exit(1); |
|
} |
|
|
|
s.start("Generating migration diff from Prisma schema"); |
|
|
|
await asyncExec( |
|
`npx prisma migrate diff --script --from-local-d1 --to-schema-datamodel ${ |
|
args.schema || "./prisma/schema.prisma" |
|
} >> ${migrationPath}` |
|
); |
|
|
|
s.stop("Generating migration diff from Prisma schema"); |
|
|
|
if (args["create-only"]) { |
|
outro(`Migration created at ${migrationPath.replace(projectRoot, ".")}`); |
|
process.exit(); |
|
} |
|
} |
|
|
|
if (command === "apply" || command === "create") { |
|
const database = await getDatabase(); |
|
|
|
const s = spinner(); |
|
s.start("Applying migrations"); |
|
|
|
await asyncExec( |
|
`npx wrangler d1 migrations apply ${database} ${ |
|
args.remote ? "--remote" : "--local" |
|
}` |
|
); |
|
|
|
s.stop("Applying migrations"); |
|
|
|
s.start("Generating Prisma client"); |
|
|
|
await asyncExec( |
|
`npx prisma generate ${args.schema ? `--schema ${args.schema}` : ""}` |
|
); |
|
|
|
s.stop("Generating Prisma client"); |
|
|
|
outro("Migrations applied"); |
|
} |
|
|
|
async function getDatabase() { |
|
if (databases.length === 0) { |
|
log.error("No D1 databases found in wrangler.toml"); |
|
process.exit(1); |
|
} |
|
|
|
database = |
|
database || |
|
(await select({ |
|
message: "Select a database", |
|
options: databases, |
|
initialValue: databases[0].value, |
|
})); |
|
|
|
if (isCancel(database)) { |
|
process.exit(1); |
|
} |
|
return database; |
|
} |
Thanks for sharing this! Really helpful approach for integrating Prisma with D1 migrations.
I ended up building a CLI tool that automates this workflow with a different approach. Here's what it does:
The Process
.wrangler/state/v3/d1/miniflare-D1DatabaseObject/(which uses hash-based filenames)wrangler d1 migrations create <database> <name>prisma migrate diff:DATABASE_URLenv var to point to the local D1 SQLite file--from-config-datasource) against Prisma schema (--to-schema)Key differences from your approach
prisma migrate diffinstead ofprisma migrate dev --create-onlyThe trick was creating a
prisma.config.tsthat readsDATABASE_URLfrom environment variables, then injecting that path at runtime:DATABASE_URL="file:/path/to/local.sqlite" npx prisma migrate diff ...package.json{ "scripts": { "prisma-d1-cli": "node -r esbuild-register ./prisma-d1-cli.ts" }, "devDependencies": { "@clack/prompts": "^0.11.0", "esbuild-register": "^3.6.0", "jsonc-parse": "^2.0.0", "prisma": "^7.1.0" }, "dependencies": { "@prisma/adapter-d1": "^7.2.0", "@prisma/client": "^7.1.0", } }prisma.config.tsprisma-d1-cli.ts