@@ -14,10 +14,10 @@ import type {
1414import { normalize } from '@sentry/core' ;
1515import { createBasicSentryServer } from '@sentry-internal/test-utils' ;
1616import { execSync , spawn , spawnSync } from 'child_process' ;
17- import { existsSync , readFileSync , unlinkSync , writeFileSync } from 'fs' ;
18- import { join } from 'path' ;
17+ import { existsSync , mkdirSync , readFileSync , rmSync , writeFileSync } from 'fs' ;
18+ import { basename , join } from 'path' ;
1919import { inspect } from 'util' ;
20- import { afterAll , beforeAll , describe , test } from 'vitest' ;
20+ import { afterAll , describe , test } from 'vitest' ;
2121import {
2222 assertEnvelopeHeader ,
2323 assertSentryCheckIn ,
@@ -174,7 +174,7 @@ export function createEsmAndCjsTests(
174174 testFn : typeof test | typeof test . fails ,
175175 mode : 'esm' | 'cjs' ,
176176 ) => void ,
177- options ?: { failsOnCjs ?: boolean ; failsOnEsm ?: boolean } ,
177+ options ?: { failsOnCjs ?: boolean ; failsOnEsm ?: boolean ; additionalDependencies ?: Record < string , string > } ,
178178) : void {
179179 const mjsScenarioPath = join ( cwd , scenarioPath ) ;
180180 const mjsInstrumentPath = join ( cwd , instrumentPath ) ;
@@ -187,31 +187,97 @@ export function createEsmAndCjsTests(
187187 throw new Error ( `Instrument file not found: ${ mjsInstrumentPath } ` ) ;
188188 }
189189
190- const cjsScenarioPath = join ( cwd , `tmp_${ scenarioPath . replace ( '.mjs' , '.cjs' ) } ` ) ;
191- const cjsInstrumentPath = join ( cwd , `tmp_${ instrumentPath . replace ( '.mjs' , '.cjs' ) } ` ) ;
190+ // Create a dedicated tmp directory that includes copied ESM & CJS scenario/instrument files.
191+ // If additionalDependencies are provided, we also create a nested package.json and install them there.
192+ const uniqueId = `${ Date . now ( ) . toString ( 36 ) } _${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ;
193+ const tmpDirPath = join ( cwd , `tmp_${ uniqueId } ` ) ;
194+ mkdirSync ( tmpDirPath ) ;
195+
196+ // Copy ESM files as-is into tmp dir
197+ const esmScenarioBasename = basename ( scenarioPath ) ;
198+ const esmInstrumentBasename = basename ( instrumentPath ) ;
199+ const esmScenarioPathForRun = join ( tmpDirPath , esmScenarioBasename ) ;
200+ const esmInstrumentPathForRun = join ( tmpDirPath , esmInstrumentBasename ) ;
201+ writeFileSync ( esmScenarioPathForRun , readFileSync ( mjsScenarioPath , 'utf8' ) ) ;
202+ writeFileSync ( esmInstrumentPathForRun , readFileSync ( mjsInstrumentPath , 'utf8' ) ) ;
203+
204+ // Pre-create CJS converted files inside tmp dir
205+ const cjsScenarioPath = join ( tmpDirPath , esmScenarioBasename . replace ( '.mjs' , '.cjs' ) ) ;
206+ const cjsInstrumentPath = join ( tmpDirPath , esmInstrumentBasename . replace ( '.mjs' , '.cjs' ) ) ;
207+ convertEsmFileToCjs ( esmScenarioPathForRun , cjsScenarioPath ) ;
208+ convertEsmFileToCjs ( esmInstrumentPathForRun , cjsInstrumentPath ) ;
209+
210+ // Create a minimal package.json with requested dependencies (if any) and install them
211+ const additionalDependencies = options ?. additionalDependencies ?? { } ;
212+ if ( Object . keys ( additionalDependencies ) . length > 0 ) {
213+ const packageJson = {
214+ name : 'tmp-integration-test' ,
215+ private : true ,
216+ version : '0.0.0' ,
217+ dependencies : additionalDependencies ,
218+ } as const ;
219+
220+ writeFileSync ( join ( tmpDirPath , 'package.json' ) , JSON . stringify ( packageJson , null , 2 ) ) ;
221+
222+ try {
223+ const deps = Object . entries ( additionalDependencies ) . map ( ( [ name , range ] ) => {
224+ if ( ! range || typeof range !== 'string' ) {
225+ throw new Error ( `Invalid version range for "${ name } ": ${ String ( range ) } ` ) ;
226+ }
227+ return `${ name } @${ range } ` ;
228+ } ) ;
229+
230+ if ( deps . length > 0 ) {
231+ // --ignore-engines is needed to avoid engine mismatches when installing deps in the tmp dir
232+ // (e.g. Vercel AI v5 requires a package that requires Node >= 20 while the system Node is 18)
233+ // https://github.com/vercel/ai/issues/7777
234+ const result = spawnSync ( 'yarn' , [ 'add' , '--non-interactive' , '--ignore-engines' , ...deps ] , {
235+ cwd : tmpDirPath ,
236+ encoding : 'utf8' ,
237+ } ) ;
238+
239+ if ( process . env . DEBUG ) {
240+ // eslint-disable-next-line no-console
241+ console . log ( '[additionalDependencies]' , deps . join ( ' ' ) ) ;
242+ // eslint-disable-next-line no-console
243+ console . log ( '[yarn stdout]' , result . stdout ) ;
244+ // eslint-disable-next-line no-console
245+ console . log ( '[yarn stderr]' , result . stderr ) ;
246+ }
247+
248+ if ( result . error ) {
249+ throw new Error ( `Failed to install additionalDependencies in tmp dir ${ tmpDirPath } : ${ result . error . message } ` ) ;
250+ }
251+ if ( typeof result . status === 'number' && result . status !== 0 ) {
252+ throw new Error (
253+ `Failed to install additionalDependencies in tmp dir ${ tmpDirPath } (exit ${ result . status } ):\n${
254+ result . stderr || result . stdout || '(no output)'
255+ } `,
256+ ) ;
257+ }
258+ }
259+ } catch ( e ) {
260+ // eslint-disable-next-line no-console
261+ console . error ( 'Failed to install additionalDependencies:' , e ) ;
262+ throw e ;
263+ }
264+ }
192265
193266 describe ( 'esm' , ( ) => {
194267 const testFn = options ?. failsOnEsm ? test . fails : test ;
195- callback ( ( ) => createRunner ( mjsScenarioPath ) . withFlags ( '--import' , mjsInstrumentPath ) , testFn , 'esm' ) ;
268+ callback ( ( ) => createRunner ( esmScenarioPathForRun ) . withFlags ( '--import' , esmInstrumentPathForRun ) , testFn , 'esm' ) ;
196269 } ) ;
197270
198271 describe ( 'cjs' , ( ) => {
199- beforeAll ( ( ) => {
200- // For the CJS runner, we create some temporary files...
201- convertEsmFileToCjs ( mjsScenarioPath , cjsScenarioPath ) ;
202- convertEsmFileToCjs ( mjsInstrumentPath , cjsInstrumentPath ) ;
203- } ) ;
204-
272+ // Clean up the tmp directory once CJS tests are finished
205273 afterAll ( ( ) => {
206274 try {
207- unlinkSync ( cjsInstrumentPath ) ;
275+ rmSync ( tmpDirPath , { recursive : true , force : true } ) ;
208276 } catch {
209- // Ignore errors here
210- }
211- try {
212- unlinkSync ( cjsScenarioPath ) ;
213- } catch {
214- // Ignore errors here
277+ if ( process . env . DEBUG ) {
278+ // eslint-disable-next-line no-console
279+ console . error ( `Failed to remove tmp dir: ${ tmpDirPath } ` ) ;
280+ }
215281 }
216282 } ) ;
217283
0 commit comments