11import { binaryForRuntime , BuildContext , BuildExtension , BuildManifest } from '@trigger.dev/build' ;
22import assert from 'node:assert' ;
3+ import { spawn } from 'node:child_process' ;
34import { existsSync } from 'node:fs' ;
4- import { cp } from 'node:fs/promises' ;
5+ import { cp , mkdir } from 'node:fs/promises' ;
56import { dirname , join , resolve } from 'node:path' ;
67
78export type PrismaExtensionOptions = {
@@ -14,6 +15,12 @@ export type PrismaExtensionOptions = {
1415 dbPackageVersion ?: string ;
1516} ;
1617
18+ type ExtendedBuildContext = BuildContext & { workspaceDir ?: string } ;
19+ type SchemaResolution = {
20+ path ?: string ;
21+ searched : string [ ] ;
22+ } ;
23+
1724export function prismaExtension ( options : PrismaExtensionOptions = { } ) : PrismaExtension {
1825 return new PrismaExtension ( options ) ;
1926}
@@ -43,51 +50,47 @@ export class PrismaExtension implements BuildExtension {
4350 return ;
4451 }
4552
46- // Resolve the path to the schema from the published @trycompai /db package
47- // In a monorepo, node_modules are typically hoisted to the workspace root
48- // Walk up the directory tree to find the workspace root (where node_modules/@trycompai/db exists)
49- let workspaceRoot = context . workingDir ;
50- let dbPackagePath = resolve ( workspaceRoot , 'node_modules/@trycompai/db/dist/schema.prisma' ) ;
51-
52- // If not found in working dir, try parent directories
53- while ( ! existsSync ( dbPackagePath ) && workspaceRoot !== dirname ( workspaceRoot ) ) {
54- workspaceRoot = dirname ( workspaceRoot ) ;
55- dbPackagePath = resolve ( workspaceRoot , 'node_modules/@trycompai/db/dist/schema.prisma' ) ;
56- }
57-
58- this . _resolvedSchemaPath = dbPackagePath ;
59-
60- context . logger . debug ( `Workspace root: ${ workspaceRoot } ` ) ;
61- context . logger . debug (
62- `Resolved the prisma schema from @trycompai/db package to: ${ this . _resolvedSchemaPath } ` ,
63- ) ;
53+ const resolution = this . tryResolveSchemaPath ( context as ExtendedBuildContext ) ;
6454
65- // Debug: List contents of the @trycompai /db package directory
66- const dbPackageDir = resolve ( workspaceRoot , 'node_modules/@trycompai/db' ) ;
67- const dbDistDir = resolve ( workspaceRoot , 'node_modules/@trycompai/db/dist' ) ;
68-
69- try {
70- const { readdirSync } = require ( 'node:fs' ) ;
71- context . logger . debug ( `@trycompai/db package directory contents:` , readdirSync ( dbPackageDir ) ) ;
72- context . logger . debug ( `@trycompai/db/dist directory contents:` , readdirSync ( dbDistDir ) ) ;
73- } catch ( err ) {
74- context . logger . debug ( `Failed to list directory contents:` , err ) ;
75- }
76-
77- // Check that the prisma schema exists in the published package
78- if ( ! existsSync ( this . _resolvedSchemaPath ) ) {
79- throw new Error (
80- `PrismaExtension could not find the prisma schema at ${ this . _resolvedSchemaPath } . Make sure @trycompai/db package is installed with version ${ this . options . dbPackageVersion || 'latest' } ` ,
55+ if ( ! resolution . path ) {
56+ context . logger . debug (
57+ 'Prisma schema not found during build start, likely before dependencies are installed.' ,
58+ { searched : resolution . searched } ,
8159 ) ;
60+ return ;
8261 }
62+
63+ this . _resolvedSchemaPath = resolution . path ;
64+ context . logger . debug ( `Resolved prisma schema to ${ resolution . path } ` ) ;
65+ await this . ensureLocalPrismaClient ( context as ExtendedBuildContext , resolution . path ) ;
8366 }
8467
8568 async onBuildComplete ( context : BuildContext , manifest : BuildManifest ) {
8669 if ( context . target === 'dev' ) {
8770 return ;
8871 }
8972
73+ if ( ! this . _resolvedSchemaPath || ! existsSync ( this . _resolvedSchemaPath ) ) {
74+ const resolution = this . tryResolveSchemaPath ( context as ExtendedBuildContext ) ;
75+
76+ if ( ! resolution . path ) {
77+ throw new Error (
78+ [
79+ 'PrismaExtension could not find the prisma schema. Make sure @trycompai/db is installed' ,
80+ `with version ${ this . options . dbPackageVersion || 'latest' } and that its dist files are built.` ,
81+ 'Searched the following locations:' ,
82+ ...resolution . searched . map ( ( candidate ) => ` - ${ candidate } ` ) ,
83+ ] . join ( '\n' ) ,
84+ ) ;
85+ }
86+
87+ this . _resolvedSchemaPath = resolution . path ;
88+ }
89+
9090 assert ( this . _resolvedSchemaPath , 'Resolved schema path is not set' ) ;
91+ const schemaPath = this . _resolvedSchemaPath ;
92+
93+ await this . ensureLocalPrismaClient ( context as ExtendedBuildContext , schemaPath ) ;
9194
9295 context . logger . debug ( 'Looking for @prisma/client in the externals' , {
9396 externals : manifest . externals ,
@@ -114,9 +117,9 @@ export class PrismaExtension implements BuildExtension {
114117 // Copy the prisma schema from the published package to the build output path
115118 const schemaDestinationPath = join ( manifest . outputPath , 'prisma' , 'schema.prisma' ) ;
116119 context . logger . debug (
117- `Copying the prisma schema from ${ this . _resolvedSchemaPath } to ${ schemaDestinationPath } ` ,
120+ `Copying the prisma schema from ${ schemaPath } to ${ schemaDestinationPath } ` ,
118121 ) ;
119- await cp ( this . _resolvedSchemaPath , schemaDestinationPath ) ;
122+ await cp ( schemaPath , schemaDestinationPath ) ;
120123
121124 // Add prisma generate command to generate the client from the copied schema
122125 commands . push (
@@ -176,4 +179,116 @@ export class PrismaExtension implements BuildExtension {
176179 } ,
177180 } ) ;
178181 }
182+
183+ private async ensureLocalPrismaClient (
184+ context : ExtendedBuildContext ,
185+ schemaSourcePath : string ,
186+ ) : Promise < void > {
187+ const schemaDir = resolve ( context . workingDir , 'prisma' ) ;
188+ const schemaDestinationPath = resolve ( schemaDir , 'schema.prisma' ) ;
189+
190+ await mkdir ( schemaDir , { recursive : true } ) ;
191+ await cp ( schemaSourcePath , schemaDestinationPath ) ;
192+
193+ const clientEntryPoint = resolve ( context . workingDir , 'node_modules/.prisma/client/default.js' ) ;
194+
195+ if ( existsSync ( clientEntryPoint ) && ! process . env . TRIGGER_PRISMA_FORCE_GENERATE ) {
196+ context . logger . debug ( 'Prisma client already generated locally, skipping regenerate.' ) ;
197+ return ;
198+ }
199+
200+ const prismaBinary = this . resolvePrismaBinary ( context . workingDir ) ;
201+
202+ if ( ! prismaBinary ) {
203+ context . logger . debug (
204+ 'Prisma CLI not available yet, skipping local generate until install finishes.' ,
205+ ) ;
206+ return ;
207+ }
208+
209+ context . logger . log ( 'Prisma client missing. Generating before Trigger indexing.' ) ;
210+ await this . runPrismaGenerate ( context , prismaBinary , schemaDestinationPath ) ;
211+ }
212+
213+ private runPrismaGenerate (
214+ context : ExtendedBuildContext ,
215+ prismaBinary : string ,
216+ schemaPath : string ,
217+ ) : Promise < void > {
218+ return new Promise ( ( resolvePromise , rejectPromise ) => {
219+ const child = spawn ( prismaBinary , [ 'generate' , `--schema=${ schemaPath } ` ] , {
220+ cwd : context . workingDir ,
221+ env : {
222+ ...process . env ,
223+ PRISMA_HIDE_UPDATE_MESSAGE : '1' ,
224+ } ,
225+ } ) ;
226+
227+ child . stdout ?. on ( 'data' , ( data : Buffer ) => {
228+ context . logger . debug ( data . toString ( ) . trim ( ) ) ;
229+ } ) ;
230+
231+ child . stderr ?. on ( 'data' , ( data : Buffer ) => {
232+ context . logger . warn ( data . toString ( ) . trim ( ) ) ;
233+ } ) ;
234+
235+ child . on ( 'error' , ( error ) => {
236+ rejectPromise ( error ) ;
237+ } ) ;
238+
239+ child . on ( 'close' , ( code ) => {
240+ if ( code === 0 ) {
241+ resolvePromise ( ) ;
242+ } else {
243+ rejectPromise ( new Error ( `prisma generate exited with code ${ code } ` ) ) ;
244+ }
245+ } ) ;
246+ } ) ;
247+ }
248+
249+ private resolvePrismaBinary ( workingDir : string ) : string | undefined {
250+ const binDir = resolve ( workingDir , 'node_modules' , '.bin' ) ;
251+ const executable = process . platform === 'win32' ? 'prisma.cmd' : 'prisma' ;
252+ const binaryPath = resolve ( binDir , executable ) ;
253+
254+ if ( ! existsSync ( binaryPath ) ) {
255+ return undefined ;
256+ }
257+
258+ return binaryPath ;
259+ }
260+
261+ private tryResolveSchemaPath ( context : ExtendedBuildContext ) : SchemaResolution {
262+ const candidates = this . buildSchemaCandidates ( context ) ;
263+ const path = candidates . find ( ( candidate ) => existsSync ( candidate ) ) ;
264+ return { path, searched : candidates } ;
265+ }
266+
267+ private buildSchemaCandidates ( context : ExtendedBuildContext ) : string [ ] {
268+ const candidates = new Set < string > ( ) ;
269+
270+ const addNodeModuleCandidates = ( start : string | undefined ) => {
271+ if ( ! start ) {
272+ return ;
273+ }
274+
275+ let current = start ;
276+ while ( true ) {
277+ candidates . add ( resolve ( current , 'node_modules/@trycompai/db/dist/schema.prisma' ) ) ;
278+ const parent = dirname ( current ) ;
279+ if ( parent === current ) {
280+ break ;
281+ }
282+ current = parent ;
283+ }
284+ } ;
285+
286+ addNodeModuleCandidates ( context . workingDir ) ;
287+ addNodeModuleCandidates ( context . workspaceDir ) ;
288+
289+ candidates . add ( resolve ( context . workingDir , '../../packages/db/dist/schema.prisma' ) ) ;
290+ candidates . add ( resolve ( context . workingDir , '../packages/db/dist/schema.prisma' ) ) ;
291+
292+ return Array . from ( candidates ) ;
293+ }
179294}
0 commit comments