From c60d2f51dfff73d306585de657f6a42454f03be8 Mon Sep 17 00:00:00 2001 From: swyxio Date: Thu, 7 Aug 2025 08:41:23 -0700 Subject: [PATCH] dragonfruit --- docs/config/extensions/prismaExtension.mdx | 89 +++++- packages/build/src/extensions/prisma.ts | 311 ++++++++++++++++----- 2 files changed, 330 insertions(+), 70 deletions(-) diff --git a/docs/config/extensions/prismaExtension.mdx b/docs/config/extensions/prismaExtension.mdx index 73bc94b16a..a9d4a7cedf 100644 --- a/docs/config/extensions/prismaExtension.mdx +++ b/docs/config/extensions/prismaExtension.mdx @@ -10,7 +10,9 @@ If you are using Prisma, you should use the prisma build extension. - Generates the Prisma client during the deploy process - Optionally will migrate the database during the deploy process - Support for TypedSQL and multiple schema files -- You can use `prismaSchemaFolder` to specify just the directory containing your schema file, instead of the full path +- **Full support for Prisma 6.6.0+ with new client generators and output paths** +- Supports both single-file and multi-file schemas +- Automatically detects schema configuration from `package.json` and `prisma.config.ts` - You can add the extension twice if you have multiple separate schemas in the same project (example below) You can use it for a simple Prisma setup like this: @@ -65,6 +67,91 @@ export default defineConfig({ If you have multiple `generator` statements defined in your schema file, you can pass in the `clientGenerator` option to specify the `prisma-client-js` generator, which will prevent other generators from being generated. Some examples where you may need to do this include when using the `prisma-kysely` or `prisma-json-types-generator` generators. +## Prisma 6.6.0+ Compatibility + +Starting with Prisma 6.6.0, Prisma introduced significant changes to client generation: + +- The new `prisma-client` generator requires an `output` path +- Multi-file schemas are now generally available +- Schema location detection has changed + +### Using with Prisma 6.6.0+ + +The extension automatically detects and handles these changes. For best compatibility: + +1. **Specify an output path in your generator:** + +```prisma +generator client { + provider = "prisma-client-js" + output = "../src/generated/client" +} +``` + +2. **For the new `prisma-client` generator:** + +```prisma +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} +``` + +3. **Configure schema location in `package.json` (optional):** + +```json +{ + "prisma": { + "schema": "prisma/schema" + } +} +``` + +### Advanced Configuration Options + +For Prisma 6.6.0+ projects with complex setups: + +```ts +import { defineConfig } from "@trigger.dev/sdk/v3"; +import { prismaExtension } from "@trigger.dev/build/extensions/prisma"; + +export default defineConfig({ + project: "", + build: { + extensions: [ + prismaExtension({ + schema: "prisma/schema.prisma", + clientOutput: "../src/generated/client", // Override detected output path + configFile: "prisma.config.ts", // Path to prisma config file + migrate: true, + }), + ], + }, +}); +``` + +### Multi-file Schema Support + +The extension fully supports multi-file schemas: + +``` +prisma/ +├── schema/ +│ ├── schema.prisma # Main file with datasource/generator +│ ├── user.prisma # User models +│ └── post.prisma # Post models +└── migrations/ +``` + +Configure it like this: + +```ts +prismaExtension({ + schema: "prisma/schema", // Point to the directory + migrate: true, +}) +``` + ```prisma schema.prisma diff --git a/packages/build/src/extensions/prisma.ts b/packages/build/src/extensions/prisma.ts index b67adc7efc..3cbbac5113 100644 --- a/packages/build/src/extensions/prisma.ts +++ b/packages/build/src/extensions/prisma.ts @@ -1,9 +1,9 @@ import { BuildManifest, BuildTarget } from "@trigger.dev/core/v3"; import { binaryForRuntime, BuildContext, BuildExtension } from "@trigger.dev/core/v3/build"; import assert from "node:assert"; -import { existsSync } from "node:fs"; +import { existsSync, readFileSync, statSync } from "node:fs"; import { cp, readdir } from "node:fs/promises"; -import { dirname, join, resolve } from "node:path"; +import { basename, dirname, join, resolve } from "node:path"; export type PrismaExtensionOptions = { schema: string; @@ -42,10 +42,142 @@ export type PrismaExtensionOptions = { */ clientGenerator?: string; directUrlEnvVarName?: string; + /** + * Custom client output path for Prisma 6.6.0+ compatibility. + * If not provided, will attempt to detect from schema or use default. + */ + clientOutput?: string; + /** + * Path to prisma.config.ts file for advanced configuration. + * If provided, will use this to determine schema location and other settings. + */ + configFile?: string; }; const BINARY_TARGET = "linux-arm64-openssl-3.0.x"; +type SchemaConfig = { + schemaPath: string; + isMultiFile: boolean; + hasSchemaFolder: boolean; + clientOutput?: string; + prismaDir: string; +}; + +/** + * Attempts to read and parse package.json to find Prisma schema configuration + */ +function getSchemaFromPackageJson(workingDir: string): string | undefined { + const packageJsonPath = join(workingDir, "package.json"); + if (!existsSync(packageJsonPath)) { + return undefined; + } + + try { + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); + return packageJson.prisma?.schema; + } catch { + return undefined; + } +} + +/** + * Detects schema configuration and handles both single-file and multi-file schemas + */ +function detectSchemaConfig( + workingDir: string, + schemaOption: string, + configFile?: string +): SchemaConfig { + let resolvedSchemaPath: string; + let clientOutput: string | undefined; + + // First try to resolve the schema path + if (configFile) { + // TODO: In the future, we could parse prisma.config.ts to get schema location + // For now, we'll use the schema option as fallback + resolvedSchemaPath = resolve(workingDir, schemaOption); + } else { + // Check package.json for schema configuration + const packageJsonSchema = getSchemaFromPackageJson(workingDir); + if (packageJsonSchema) { + resolvedSchemaPath = resolve(workingDir, packageJsonSchema); + } else { + resolvedSchemaPath = resolve(workingDir, schemaOption); + } + } + + const isDirectory = existsSync(resolvedSchemaPath) && + statSync(resolvedSchemaPath).isDirectory(); + + if (isDirectory) { + // Multi-file schema - look for schema.prisma in the directory + const schemaFile = join(resolvedSchemaPath, "schema.prisma"); + if (existsSync(schemaFile)) { + return { + schemaPath: schemaFile, + isMultiFile: true, + hasSchemaFolder: true, + clientOutput, + prismaDir: resolvedSchemaPath, + }; + } else { + throw new Error( + `Multi-file schema directory found at ${resolvedSchemaPath} but no schema.prisma file found. ` + + `For multi-file schemas, you need a main schema.prisma file that contains your datasource and generator blocks.` + ); + } + } else { + // Single file schema + const schemaDir = dirname(resolvedSchemaPath); + const schemaFileName = basename(resolvedSchemaPath); + + // Check if this is in a "schema" folder structure (but don't assume it's multi-file) + const hasSchemaFolder = schemaDir.endsWith("schema") && schemaFileName === "schema.prisma"; + + return { + schemaPath: resolvedSchemaPath, + isMultiFile: false, + hasSchemaFolder, + clientOutput, + prismaDir: hasSchemaFolder ? dirname(schemaDir) : schemaDir, + }; + } +} + +/** + * Reads the schema file and attempts to detect the client output path from generator blocks + */ +function detectClientOutputFromSchema(schemaPath: string): string | undefined { + if (!existsSync(schemaPath)) { + return undefined; + } + + try { + const schemaContent = readFileSync(schemaPath, "utf-8"); + + // Look for generator client blocks with output paths + const generatorMatches = schemaContent.match(/generator\s+\w+\s*{[^}]+}/g); + + if (generatorMatches) { + for (const generator of generatorMatches) { + // Check if it's a client generator (prisma-client-js or prisma-client) + if (generator.includes('provider = "prisma-client') || generator.includes("provider = 'prisma-client")) { + // Look for output path + const outputMatch = generator.match(/output\s*=\s*["']([^"']+)["']/); + if (outputMatch) { + return outputMatch[1]; + } + } + } + } + } catch { + // If we can't read or parse the schema, continue without client output detection + } + + return undefined; +} + export function prismaExtension(options: PrismaExtensionOptions): PrismaExtension { return new PrismaExtension(options); } @@ -55,7 +187,7 @@ export class PrismaExtension implements BuildExtension { public readonly name = "PrismaExtension"; - private _resolvedSchemaPath?: string; + private _schemaConfig?: SchemaConfig; constructor(private options: PrismaExtensionOptions) { this.moduleExternals = ["@prisma/client", "@prisma/engines"]; @@ -74,15 +206,44 @@ export class PrismaExtension implements BuildExtension { return; } - // Resolve the path to the prisma schema, relative to the config.directory - this._resolvedSchemaPath = resolve(context.workingDir, this.options.schema); + // Detect schema configuration using enhanced logic + try { + this._schemaConfig = detectSchemaConfig( + context.workingDir, + this.options.schema, + this.options.configFile + ); + + context.logger.debug(`Detected Prisma schema configuration:`, { + schemaPath: this._schemaConfig.schemaPath, + isMultiFile: this._schemaConfig.isMultiFile, + hasSchemaFolder: this._schemaConfig.hasSchemaFolder, + prismaDir: this._schemaConfig.prismaDir, + }); + + // Check that the resolved schema file exists + if (!existsSync(this._schemaConfig.schemaPath)) { + throw new Error( + `PrismaExtension could not find the prisma schema at ${this._schemaConfig.schemaPath}. ` + + `Make sure the path is correct: ${this.options.schema}, relative to the working dir ${context.workingDir}. ` + + `For multi-file schemas, ensure you have a main schema.prisma file with your datasource and generator blocks.` + ); + } - context.logger.debug(`Resolved the prisma schema to: ${this._resolvedSchemaPath}`); + // Try to detect client output from schema if not provided in options + if (!this.options.clientOutput) { + const detectedOutput = detectClientOutputFromSchema(this._schemaConfig.schemaPath); + if (detectedOutput) { + this._schemaConfig.clientOutput = detectedOutput; + context.logger.debug(`Detected client output path from schema: ${detectedOutput}`); + } + } else { + this._schemaConfig.clientOutput = this.options.clientOutput; + } - // Check that the prisma schema exists - if (!existsSync(this._resolvedSchemaPath)) { + } catch (error) { throw new Error( - `PrismaExtension could not find the prisma schema at ${this._resolvedSchemaPath}. Make sure the path is correct: ${this.options.schema}, relative to the working dir ${context.workingDir}` + `PrismaExtension failed to configure schema: ${error instanceof Error ? error.message : String(error)}` ); } } @@ -92,7 +253,7 @@ export class PrismaExtension implements BuildExtension { return; } - assert(this._resolvedSchemaPath, "Resolved schema path is not set"); + assert(this._schemaConfig, "Schema configuration is not set"); context.logger.debug("Looking for @prisma/client in the externals", { externals: manifest.externals, @@ -112,11 +273,8 @@ export class PrismaExtension implements BuildExtension { context.logger.debug(`PrismaExtension is generating the Prisma client for version ${version}`); - const usingSchemaFolder = dirname(this._resolvedSchemaPath).endsWith("schema"); - const commands: string[] = []; - - let prismaDir: string | undefined; + const { schemaPath, isMultiFile, hasSchemaFolder, prismaDir, clientOutput } = this._schemaConfig; const generatorFlags: string[] = []; @@ -127,54 +285,53 @@ export class PrismaExtension implements BuildExtension { if (this.options.typedSql) { generatorFlags.push(`--sql`); - const prismaDir = usingSchemaFolder - ? dirname(dirname(this._resolvedSchemaPath)) - : dirname(this._resolvedSchemaPath); - - context.logger.debug(`Using typedSql`); + context.logger.debug(`Using typedSql with prismaDir: ${prismaDir}`); // Find all the files prisma/sql/*.sql - const sqlFiles = await readdir(join(prismaDir, "sql")).then((files) => - files.filter((file) => file.endsWith(".sql")) - ); + const sqlDir = join(prismaDir, "sql"); + if (existsSync(sqlDir)) { + const sqlFiles = await readdir(sqlDir).then((files) => + files.filter((file) => file.endsWith(".sql")) + ); - context.logger.debug(`Found sql files`, { - sqlFiles, - }); + context.logger.debug(`Found sql files`, { + sqlFiles, + }); - const sqlDestinationPath = join(manifest.outputPath, "prisma", "sql"); + const sqlDestinationPath = join(manifest.outputPath, "prisma", "sql"); - for (const file of sqlFiles) { - const destination = join(sqlDestinationPath, file); - const source = join(prismaDir, "sql", file); + for (const file of sqlFiles) { + const destination = join(sqlDestinationPath, file); + const source = join(sqlDir, file); - context.logger.debug(`Copying the sql from ${source} to ${destination}`); + context.logger.debug(`Copying the sql from ${source} to ${destination}`); - await cp(source, destination); + await cp(source, destination); + } + } else { + context.logger.warn(`TypedSQL enabled but no sql directory found at ${sqlDir}`); } } - if (usingSchemaFolder) { - const schemaDir = dirname(this._resolvedSchemaPath); - - prismaDir = dirname(schemaDir); + // Handle schema copying based on configuration + if (isMultiFile || hasSchemaFolder) { + const schemaDir = dirname(schemaPath); + + context.logger.debug(`Using multi-file or schema folder setup: ${schemaDir}`); - context.logger.debug(`Using the schema folder: ${schemaDir}`); - - // Find all the files in schemaDir that end with .prisma (excluding the schema.prisma file) + // Find all the files in schemaDir that end with .prisma const prismaFiles = await readdir(schemaDir).then((files) => files.filter((file) => file.endsWith(".prisma")) ); - context.logger.debug(`Found prisma files in the schema folder`, { + context.logger.debug(`Found prisma files in the schema directory`, { prismaFiles, + schemaDir, }); const schemaDestinationPath = join(manifest.outputPath, "prisma", "schema"); - const allPrismaFiles = [...prismaFiles]; - - for (const file of allPrismaFiles) { + for (const file of prismaFiles) { const destination = join(schemaDestinationPath, file); const source = join(schemaDir, file); @@ -183,31 +340,39 @@ export class PrismaExtension implements BuildExtension { await cp(source, destination); } - commands.push( - `${binaryForRuntime( - manifest.runtime - )} node_modules/prisma/build/index.js generate ${generatorFlags.join(" ")}` // Don't add the --schema flag or this will fail - ); + // For multi-file schemas, don't specify --schema flag + const generateCommand = `${binaryForRuntime( + manifest.runtime + )} node_modules/prisma/build/index.js generate ${generatorFlags.join(" ")}`; + + commands.push(generateCommand); + } else { - prismaDir = dirname(this._resolvedSchemaPath); - // Now we need to add a layer that: - // Copies the prisma schema to the build outputPath - // Adds the `prisma` CLI dependency to the dependencies - // Adds the `prisma generate` command, which generates the Prisma client + // Single file schema const schemaDestinationPath = join(manifest.outputPath, "prisma", "schema.prisma"); - // Copy the prisma schema to the build output path + context.logger.debug( - `Copying the prisma schema from ${this._resolvedSchemaPath} to ${schemaDestinationPath}` + `Copying single prisma schema from ${schemaPath} to ${schemaDestinationPath}` ); - await cp(this._resolvedSchemaPath, schemaDestinationPath); + await cp(schemaPath, schemaDestinationPath); - commands.push( - `${binaryForRuntime( - manifest.runtime - )} node_modules/prisma/build/index.js generate --schema=./prisma/schema.prisma ${generatorFlags.join( - " " - )}` + // For single file schemas, specify the schema path + const generateCommand = `${binaryForRuntime( + manifest.runtime + )} node_modules/prisma/build/index.js generate --schema=./prisma/schema.prisma ${generatorFlags.join( + " " + )}`; + + commands.push(generateCommand); + } + + // Add warning for Prisma 6.6.0+ if no client output is detected + if (!clientOutput) { + context.logger.warn( + `No client output path detected in your Prisma schema. ` + + `Starting with Prisma 6.6.0, you should specify an 'output' path in your generator block ` + + `to avoid potential issues. See: https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/generating-prisma-client` ); } @@ -216,17 +381,25 @@ export class PrismaExtension implements BuildExtension { if (this.options.migrate) { // Copy the migrations directory to the build output path const migrationsDir = join(prismaDir, "migrations"); - const migrationsDestinationPath = join(manifest.outputPath, "prisma", "migrations"); + + if (existsSync(migrationsDir)) { + const migrationsDestinationPath = join(manifest.outputPath, "prisma", "migrations"); - context.logger.debug( - `Copying the prisma migrations from ${migrationsDir} to ${migrationsDestinationPath}` - ); + context.logger.debug( + `Copying the prisma migrations from ${migrationsDir} to ${migrationsDestinationPath}` + ); - await cp(migrationsDir, migrationsDestinationPath, { recursive: true }); + await cp(migrationsDir, migrationsDestinationPath, { recursive: true }); - commands.push( - `${binaryForRuntime(manifest.runtime)} node_modules/prisma/build/index.js migrate deploy` - ); + commands.push( + `${binaryForRuntime(manifest.runtime)} node_modules/prisma/build/index.js migrate deploy` + ); + } else { + context.logger.warn( + `Migration enabled but no migrations directory found at ${migrationsDir}. ` + + `Make sure you have run 'prisma migrate dev' to create migrations.` + ); + } } env.DATABASE_URL = manifest.deploy.env?.DATABASE_URL;