import { binaryForRuntime, BuildContext, BuildExtension, BuildManifest } from '@trigger.dev/build'; import assert from 'node:assert'; import { spawn } from 'node:child_process'; import { existsSync } from 'node:fs'; import { cp, mkdir } from 'node:fs/promises'; import { dirname, join, resolve } from 'node:path'; export type PrismaExtensionOptions = { version?: string; migrate?: boolean; directUrlEnvVarName?: string; /** * The version of the @trycompai/db package to use */ dbPackageVersion?: string; }; type ExtendedBuildContext = BuildContext & { workspaceDir?: string }; type SchemaResolution = { path?: string; searched: string[]; }; export function prismaExtension(options: PrismaExtensionOptions = {}): PrismaExtension { return new PrismaExtension(options); } export class PrismaExtension implements BuildExtension { moduleExternals: string[]; public readonly name = 'PrismaExtension'; private _resolvedSchemaPath?: string; constructor(private options: PrismaExtensionOptions) { this.moduleExternals = [ '@prisma/client', '@trycompai/db', ]; } externalsForTarget(target: any) { if (target === 'dev') { return []; } return this.moduleExternals; } async onBuildStart(context: BuildContext) { if (context.target === 'dev') { return; } const resolution = this.tryResolveSchemaPath(context as ExtendedBuildContext); if (!resolution.path) { context.logger.debug( 'Prisma schema not found during build start, likely before dependencies are installed.', { searched: resolution.searched }, ); return; } this._resolvedSchemaPath = resolution.path; context.logger.debug(`Resolved prisma schema to ${resolution.path}`); await this.ensureLocalPrismaClient(context as ExtendedBuildContext, resolution.path); } async onBuildComplete(context: BuildContext, manifest: BuildManifest) { if (context.target === 'dev') { return; } if (!this._resolvedSchemaPath || !existsSync(this._resolvedSchemaPath)) { const resolution = this.tryResolveSchemaPath(context as ExtendedBuildContext); if (!resolution.path) { throw new Error( [ 'PrismaExtension could not find the prisma schema. Make sure @trycompai/db is installed', `with version ${this.options.dbPackageVersion || 'latest'} and that its dist files are built.`, 'Searched the following locations:', ...resolution.searched.map((candidate) => ` - ${candidate}`), ].join('\n'), ); } this._resolvedSchemaPath = resolution.path; } assert(this._resolvedSchemaPath, 'Resolved schema path is not set'); const schemaPath = this._resolvedSchemaPath; await this.ensureLocalPrismaClient(context as ExtendedBuildContext, schemaPath); context.logger.debug('Looking for @prisma/client in the externals', { externals: manifest.externals, }); const prismaExternal = manifest.externals?.find( (external) => external.name === '@prisma/client', ); const version = prismaExternal?.version ?? this.options.version; if (!version) { throw new Error( `PrismaExtension could not determine the version of @prisma/client. It's possible that the @prisma/client was not used in the project. If this isn't the case, please provide a version in the PrismaExtension options.`, ); } context.logger.debug( `PrismaExtension is generating the Prisma client for version ${version} from @trycompai/db package`, ); const commands: string[] = []; const env: Record = {}; // Copy the prisma schema from the published package to the build output path // Copy the entire schema directory (multi-file schema) const sourceDir = dirname(schemaPath); const schemaDestinationDir = join(manifest.outputPath, 'prisma', 'schema'); context.logger.debug( `Copying the prisma schema directory from ${sourceDir} to ${schemaDestinationDir}`, ); await mkdir(schemaDestinationDir, { recursive: true }); await cp(sourceDir, schemaDestinationDir, { recursive: true }); // Patch schema.prisma to use prisma-client-js (populates @prisma/client at runtime) commands.push( `sed -i 's/provider.*=.*"prisma-client"/provider = "prisma-client-js"/' ./prisma/schema/schema.prisma && sed -i '/output.*=.*"/d' ./prisma/schema/schema.prisma`, ); // Generate client from the multi-file schema directory commands.push( `${binaryForRuntime(manifest.runtime)} node_modules/prisma/build/index.js generate --schema=./prisma/schema`, ); // Only handle migrations if requested if (this.options.migrate) { context.logger.debug( 'Migration support not implemented for published package - please handle migrations separately', ); // You could add migration commands here if needed // commands.push(`${binaryForRuntime(manifest.runtime)} npx prisma migrate deploy`); } // Set up environment variables env.DATABASE_URL = manifest.deploy.env?.DATABASE_URL; if (this.options.directUrlEnvVarName) { env[this.options.directUrlEnvVarName] = manifest.deploy.env?.[this.options.directUrlEnvVarName] ?? process.env[this.options.directUrlEnvVarName]; if (!env[this.options.directUrlEnvVarName]) { context.logger.warn( `prismaExtension could not resolve the ${this.options.directUrlEnvVarName} environment variable. Make sure you add it to your environment variables or provide it as an environment variable to the deploy CLI command. See our docs for more info: https://trigger.dev/docs/deploy-environment-variables`, ); } } else { env.DIRECT_URL = manifest.deploy.env?.DIRECT_URL; env.DIRECT_DATABASE_URL = manifest.deploy.env?.DIRECT_DATABASE_URL; } if (!env.DATABASE_URL) { context.logger.warn( 'prismaExtension could not resolve the DATABASE_URL environment variable. Make sure you add it to your environment variables. See our docs for more info: https://trigger.dev/docs/deploy-environment-variables', ); } context.logger.debug('Adding the prisma layer with the following commands', { commands, env, dependencies: { prisma: version, '@trycompai/db': this.options.dbPackageVersion || 'latest', }, }); context.addLayer({ id: 'prisma', commands, dependencies: { prisma: version, '@trycompai/db': this.options.dbPackageVersion || 'latest', }, build: { env, }, }); } private async ensureLocalPrismaClient( context: ExtendedBuildContext, schemaSourcePath: string, ): Promise { // schemaSourcePath points to a file inside the schema directory. // Copy the entire directory (multi-file schema) to the local prisma/schema/ dir. const sourceDir = dirname(schemaSourcePath); const localSchemaDir = resolve(context.workingDir, 'prisma', 'schema'); await mkdir(localSchemaDir, { recursive: true }); await cp(sourceDir, localSchemaDir, { recursive: true }); // Patch schema.prisma to use prisma-client-js (default output → @prisma/client) const localSchemaFile = resolve(localSchemaDir, 'schema.prisma'); const { readFileSync, writeFileSync } = await import('node:fs'); let schemaContent = readFileSync(localSchemaFile, 'utf8'); schemaContent = schemaContent .replace(/provider\s*=\s*"prisma-client"/g, 'provider = "prisma-client-js"') .replace(/\s*output\s*=\s*"[^"]*"\n?/g, '\n'); writeFileSync(localSchemaFile, schemaContent); const clientEntryPoint = resolve(context.workingDir, 'node_modules/.prisma/client/default.js'); if (existsSync(clientEntryPoint) && !process.env.TRIGGER_PRISMA_FORCE_GENERATE) { context.logger.debug('Prisma client already generated locally, skipping regenerate.'); return; } const prismaBinary = this.resolvePrismaBinary(context.workingDir); if (!prismaBinary) { context.logger.debug( 'Prisma CLI not available yet, skipping local generate until install finishes.', ); return; } context.logger.log('Prisma client missing. Generating before Trigger indexing.'); await this.runPrismaGenerate(context, prismaBinary, localSchemaDir); } private runPrismaGenerate( context: ExtendedBuildContext, prismaBinary: string, schemaPath: string, ): Promise { return new Promise((resolvePromise, rejectPromise) => { const child = spawn(prismaBinary, ['generate', `--schema=${schemaPath}`], { cwd: context.workingDir, env: { ...process.env, PRISMA_HIDE_UPDATE_MESSAGE: '1', }, }); child.stdout?.on('data', (data: Buffer) => { context.logger.debug(data.toString().trim()); }); child.stderr?.on('data', (data: Buffer) => { context.logger.warn(data.toString().trim()); }); child.on('error', (error) => { rejectPromise(error); }); child.on('close', (code) => { if (code === 0) { resolvePromise(); } else { rejectPromise(new Error(`prisma generate exited with code ${code}`)); } }); }); } private resolvePrismaBinary(workingDir: string): string | undefined { const binDir = resolve(workingDir, 'node_modules', '.bin'); const executable = process.platform === 'win32' ? 'prisma.cmd' : 'prisma'; const binaryPath = resolve(binDir, executable); if (!existsSync(binaryPath)) { return undefined; } return binaryPath; } private tryResolveSchemaPath(context: ExtendedBuildContext): SchemaResolution { const candidates = this.buildSchemaCandidates(context); const path = candidates.find((candidate) => existsSync(candidate)); return { path, searched: candidates }; } private buildSchemaCandidates(context: ExtendedBuildContext): string[] { const candidates: string[] = []; const seen = new Set(); const add = (p: string) => { const resolved = resolve(p); if (!seen.has(resolved)) { seen.add(resolved); candidates.push(resolved); } }; // Strategy 1: Resolve @trycompai/db via Node module resolution (follows workspace symlinks) try { const dbPkgJson = require.resolve('@trycompai/db/package.json', { paths: [context.workingDir], }); const dbRoot = dirname(dbPkgJson); add(join(dbRoot, 'dist', 'schema.prisma')); add(join(dbRoot, 'prisma', 'schema', 'schema.prisma')); } catch { // Package not resolvable yet (pre-install), fall through to other strategies } // Strategy 2: Walk up node_modules hierarchy from workingDir and workspaceDir const addNodeModuleCandidates = (start: string | undefined) => { if (!start) return; let current = start; while (true) { const dbDir = resolve(current, 'node_modules', '@trycompai', 'db'); add(join(dbDir, 'dist', 'schema.prisma')); add(join(dbDir, 'prisma', 'schema', 'schema.prisma')); const parent = dirname(current); if (parent === current) break; current = parent; } }; addNodeModuleCandidates(context.workingDir); addNodeModuleCandidates(context.workspaceDir); // Strategy 3: Relative monorepo paths (apps/api → packages/db, apps/app → packages/db) for (const rel of ['../../packages/db', '../packages/db']) { const dbDir = resolve(context.workingDir, rel); add(join(dbDir, 'dist', 'schema.prisma')); add(join(dbDir, 'prisma', 'schema', 'schema.prisma')); } return candidates; } }