From 16ecfd8a32fce3f52769554e071a7dbcec4d153f Mon Sep 17 00:00:00 2001 From: Sohan Kshirsagar Date: Thu, 26 Mar 2026 22:01:09 -0700 Subject: [PATCH] fix: improve Next.js monorepo compatibility --- .../libraries/pg/Instrumentation.ts | 36 ++++++++++++++----- src/nextjs/utils.ts | 15 ++++++-- src/nextjs/withTuskDrift.ts | 4 +++ 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/instrumentation/libraries/pg/Instrumentation.ts b/src/instrumentation/libraries/pg/Instrumentation.ts index d9c728f3..69d9b0a5 100644 --- a/src/instrumentation/libraries/pg/Instrumentation.ts +++ b/src/instrumentation/libraries/pg/Instrumentation.ts @@ -83,7 +83,9 @@ export class PgInstrumentation extends TdInstrumentationBase { // Submittable queries use EventEmitter pattern (row, end, error events) // and return the Query object itself, not a Promise if (self.isSubmittable(args[0])) { - logger.debug(`[PgInstrumentation] Submittable query detected, passing through uninstrumented`); + logger.debug( + `[PgInstrumentation] Submittable query detected, passing through uninstrumented`, + ); return originalQuery.apply(this, args); } @@ -365,9 +367,25 @@ export class PgInstrumentation extends TdInstrumentationBase { return originalQuery.apply(context, args); } else { // Promise-based query - const promise = originalQuery.apply(context, args); + const result = originalQuery.apply(context, args); + + // ORMs like Prisma, TypeORM, etc. may call pg.query() internally in ways + // that return undefined instead of a Promise (e.g. Prisma's driver adapter). + // Without this guard, we'd crash with "Cannot read properties of undefined + // (reading 'then')". + if (!result || typeof result.then !== "function") { + logger.debug( + `[PgInstrumentation] originalQuery returned non-thenable, passing through (${SpanUtils.getTraceInfo()})`, + ); + try { + SpanUtils.endSpan(spanInfo.span, { code: SpanStatusCode.OK }); + } catch (error) { + logger.error(`[PgInstrumentation] error ending span:`, error); + } + return result; + } - return promise + return result .then((result: PgResult) => { logger.debug( `[PgInstrumentation] PG query completed successfully (${SpanUtils.getTraceInfo()})`, @@ -497,15 +515,17 @@ export class PgInstrumentation extends TdInstrumentationBase { * * Reference for data type IDs: https://jdbc.postgresql.org/documentation/publicapi/constant-values.html */ - private convertPostgresTypes(result: any, rowMode?: 'array'): any { + private convertPostgresTypes(result: any, rowMode?: "array"): any { // Handle multi-statement results (wrapped format from _addOutputAttributesToSpan) if (result && result.isMultiStatement && Array.isArray(result.results)) { - return result.results.map((singleResult: any) => this.convertPostgresTypes(singleResult, rowMode)); + return result.results.map((singleResult: any) => + this.convertPostgresTypes(singleResult, rowMode), + ); } // Handle multi-statement results (array of Results - legacy/direct format) if (Array.isArray(result)) { - return result.map(singleResult => this.convertPostgresTypes(singleResult, rowMode)); + return result.map((singleResult) => this.convertPostgresTypes(singleResult, rowMode)); } if (!result || !result.fields || !result.rows) { @@ -513,7 +533,7 @@ export class PgInstrumentation extends TdInstrumentationBase { } // If rowMode is 'array', handle arrays differently - if (rowMode === 'array') { + if (rowMode === "array") { // For array mode, rows are arrays of values indexed by column position const convertedRows = result.rows.map((row: any) => { if (!Array.isArray(row)) return row; // Safety check @@ -655,7 +675,7 @@ export class PgInstrumentation extends TdInstrumentationBase { // Wrap in object with 'results' key to ensure CLI can handle it properly outputValue = { isMultiStatement: true, - results: result.map(r => ({ + results: result.map((r) => ({ command: r.command, rowCount: r.rowCount, oid: r.oid, diff --git a/src/nextjs/utils.ts b/src/nextjs/utils.ts index f834a673..2513f42e 100644 --- a/src/nextjs/utils.ts +++ b/src/nextjs/utils.ts @@ -9,16 +9,25 @@ import type { ParsedVersion } from "./types"; * @returns The Next.js version string, or undefined if not found */ export function getNextjsVersion(): string | undefined { + // Try cwd-based lookup first (works for standard project layouts) try { - // Try to read from node_modules/next/package.json const nextPackageJsonPath = path.join(process.cwd(), "node_modules", "next", "package.json"); if (fs.existsSync(nextPackageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(nextPackageJsonPath, "utf-8")); return packageJson.version; } - } catch (error) { - // Silent failure - we'll warn the user in the main function + } catch { + // Fall through to require.resolve + } + + // Fallback: use Node's module resolution, which handles hoisted packages in monorepos + try { + const resolvedPath = require.resolve("next/package.json", { paths: [process.cwd()] }); + const packageJson = JSON.parse(fs.readFileSync(resolvedPath, "utf-8")); + return packageJson.version; + } catch { + // next is not resolvable } return undefined; diff --git a/src/nextjs/withTuskDrift.ts b/src/nextjs/withTuskDrift.ts index accb995d..a6de9c70 100644 --- a/src/nextjs/withTuskDrift.ts +++ b/src/nextjs/withTuskDrift.ts @@ -140,6 +140,10 @@ export function withTuskDrift( // This ensures packages remain external regardless of which bundler is used serverExternalPackages: [...(config.serverExternalPackages || []), ...coreExternals], + // Preserve user's turbopack config (or set empty) so Next.js 16+ doesn't error + // when it sees our webpack config without a turbopack counterpart + turbopack: (config as any).turbopack || {}, + webpack: (webpackConfig: any, webpackOptions: any) => { if (webpackOptions.isServer) { // Safely handle different externals formats (array, function, object, or undefined)