@@ -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,10 @@ export function createEsmAndCjsTests(
174174 testFn : typeof test | typeof test . fails ,
175175 mode : 'esm' | 'cjs' ,
176176 ) => void ,
177- options ?: { failsOnCjs ?: boolean ; failsOnEsm ?: boolean } ,
177+ // `additionalDependencies` to install in a tmp dir for the esm and cjs tests
178+ // This could be used to override packages that live in the parent package.json for the specific run of the test
179+ // e.g. `{ ai: '^5.0.0' }` to test Vercel AI v5
180+ options ?: { failsOnCjs ?: boolean ; failsOnEsm ?: boolean ; additionalDependencies ?: Record < string , string > } ,
178181) : void {
179182 const mjsScenarioPath = join ( cwd , scenarioPath ) ;
180183 const mjsInstrumentPath = join ( cwd , instrumentPath ) ;
@@ -187,36 +190,107 @@ export function createEsmAndCjsTests(
187190 throw new Error ( `Instrument file not found: ${ mjsInstrumentPath } ` ) ;
188191 }
189192
190- const cjsScenarioPath = join ( cwd , `tmp_${ scenarioPath . replace ( '.mjs' , '.cjs' ) } ` ) ;
191- const cjsInstrumentPath = join ( cwd , `tmp_${ instrumentPath . replace ( '.mjs' , '.cjs' ) } ` ) ;
193+ // Create a dedicated tmp directory that includes copied ESM & CJS scenario/instrument files.
194+ // If additionalDependencies are provided, we also create a nested package.json and install them there.
195+ const uniqueId = `${ Date . now ( ) . toString ( 36 ) } _${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ;
196+ const tmpDirPath = join ( cwd , `tmp_${ uniqueId } ` ) ;
197+ mkdirSync ( tmpDirPath ) ;
198+
199+ // Copy ESM files as-is into tmp dir
200+ const esmScenarioBasename = basename ( scenarioPath ) ;
201+ const esmInstrumentBasename = basename ( instrumentPath ) ;
202+ const esmScenarioPathForRun = join ( tmpDirPath , esmScenarioBasename ) ;
203+ const esmInstrumentPathForRun = join ( tmpDirPath , esmInstrumentBasename ) ;
204+ writeFileSync ( esmScenarioPathForRun , readFileSync ( mjsScenarioPath , 'utf8' ) ) ;
205+ writeFileSync ( esmInstrumentPathForRun , readFileSync ( mjsInstrumentPath , 'utf8' ) ) ;
206+
207+ // Pre-create CJS converted files inside tmp dir
208+ const cjsScenarioPath = join ( tmpDirPath , esmScenarioBasename . replace ( '.mjs' , '.cjs' ) ) ;
209+ const cjsInstrumentPath = join ( tmpDirPath , esmInstrumentBasename . replace ( '.mjs' , '.cjs' ) ) ;
210+ convertEsmFileToCjs ( esmScenarioPathForRun , cjsScenarioPath ) ;
211+ convertEsmFileToCjs ( esmInstrumentPathForRun , cjsInstrumentPath ) ;
212+
213+ // Create a minimal package.json with requested dependencies (if any) and install them
214+ const additionalDependencies = options ?. additionalDependencies ?? { } ;
215+ if ( Object . keys ( additionalDependencies ) . length > 0 ) {
216+ const packageJson = {
217+ name : 'tmp-integration-test' ,
218+ private : true ,
219+ version : '0.0.0' ,
220+ dependencies : additionalDependencies ,
221+ } as const ;
222+
223+ writeFileSync ( join ( tmpDirPath , 'package.json' ) , JSON . stringify ( packageJson , null , 2 ) ) ;
224+
225+ try {
226+ const deps = Object . entries ( additionalDependencies ) . map ( ( [ name , range ] ) => {
227+ if ( ! range || typeof range !== 'string' ) {
228+ throw new Error ( `Invalid version range for "${ name } ": ${ String ( range ) } ` ) ;
229+ }
230+ return `${ name } @${ range } ` ;
231+ } ) ;
192232
193- describe ( 'esm' , ( ) => {
194- const testFn = options ?. failsOnEsm ? test . fails : test ;
195- callback ( ( ) => createRunner ( mjsScenarioPath ) . withFlags ( '--import' , mjsInstrumentPath ) , testFn , 'esm' ) ;
196- } ) ;
233+ if ( deps . length > 0 ) {
234+ // Prefer npm for temp installs to avoid Yarn engine strictness; see https://github.com/vercel/ai/issues/7777
235+ // We rely on the generated package.json dependencies and run a plain install.
236+ const result = spawnSync ( 'npm' , [ 'install' , '--silent' , '--no-audit' , '--no-fund' ] , {
237+ cwd : tmpDirPath ,
238+ encoding : 'utf8' ,
239+ } ) ;
240+
241+ if ( process . env . DEBUG ) {
242+ // eslint-disable-next-line no-console
243+ console . log ( '[additionalDependencies via npm]' , deps . join ( ' ' ) ) ;
244+ // eslint-disable-next-line no-console
245+ console . log ( '[npm stdout]' , result . stdout ) ;
246+ // eslint-disable-next-line no-console
247+ console . log ( '[npm stderr]' , result . stderr ) ;
248+ }
197249
198- describe ( 'cjs' , ( ) => {
199- beforeAll ( ( ) => {
200- // For the CJS runner, we create some temporary files...
201- convertEsmFileToCjs ( mjsScenarioPath , cjsScenarioPath ) ;
202- convertEsmFileToCjs ( mjsInstrumentPath , cjsInstrumentPath ) ;
250+ if ( result . error ) {
251+ throw new Error ( `Failed to install additionalDependencies in tmp dir ${ tmpDirPath } : ${ result . error . message } ` ) ;
252+ }
253+ if ( typeof result . status === 'number' && result . status !== 0 ) {
254+ throw new Error (
255+ `Failed to install additionalDependencies in tmp dir ${ tmpDirPath } (exit ${ result . status } ):\n${
256+ result . stderr || result . stdout || '(no output)'
257+ } `,
258+ ) ;
259+ }
260+ }
261+ } catch ( e ) {
262+ // eslint-disable-next-line no-console
263+ console . error ( 'Failed to install additionalDependencies:' , e ) ;
264+ throw e ;
265+ }
266+ }
267+
268+ describe ( 'esm/cjs' , ( ) => {
269+ const esmTestFn = options ?. failsOnEsm ? test . fails : test ;
270+ describe ( 'esm' , ( ) => {
271+ callback (
272+ ( ) => createRunner ( esmScenarioPathForRun ) . withFlags ( '--import' , esmInstrumentPathForRun ) ,
273+ esmTestFn ,
274+ 'esm' ,
275+ ) ;
276+ } ) ;
277+
278+ const cjsTestFn = options ?. failsOnCjs ? test . fails : test ;
279+ describe ( 'cjs' , ( ) => {
280+ callback ( ( ) => createRunner ( cjsScenarioPath ) . withFlags ( '--require' , cjsInstrumentPath ) , cjsTestFn , 'cjs' ) ;
203281 } ) ;
204282
283+ // Clean up the tmp directory after both esm and cjs suites have run
205284 afterAll ( ( ) => {
206285 try {
207- unlinkSync ( cjsInstrumentPath ) ;
208- } catch {
209- // Ignore errors here
210- }
211- try {
212- unlinkSync ( cjsScenarioPath ) ;
286+ rmSync ( tmpDirPath , { recursive : true , force : true } ) ;
213287 } catch {
214- // Ignore errors here
288+ if ( process . env . DEBUG ) {
289+ // eslint-disable-next-line no-console
290+ console . error ( `Failed to remove tmp dir: ${ tmpDirPath } ` ) ;
291+ }
215292 }
216293 } ) ;
217-
218- const testFn = options ?. failsOnCjs ? test . fails : test ;
219- callback ( ( ) => createRunner ( cjsScenarioPath ) . withFlags ( '--require' , cjsInstrumentPath ) , testFn , 'cjs' ) ;
220294 } ) ;
221295}
222296
0 commit comments