@@ -14,10 +14,10 @@ import type {
14
14
import { normalize } from '@sentry/core' ;
15
15
import { createBasicSentryServer } from '@sentry-internal/test-utils' ;
16
16
import { 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' ;
19
19
import { inspect } from 'util' ;
20
- import { afterAll , beforeAll , describe , test } from 'vitest' ;
20
+ import { afterAll , describe , test } from 'vitest' ;
21
21
import {
22
22
assertEnvelopeHeader ,
23
23
assertSentryCheckIn ,
@@ -174,7 +174,10 @@ export function createEsmAndCjsTests(
174
174
testFn : typeof test | typeof test . fails ,
175
175
mode : 'esm' | 'cjs' ,
176
176
) => 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 > } ,
178
181
) : void {
179
182
const mjsScenarioPath = join ( cwd , scenarioPath ) ;
180
183
const mjsInstrumentPath = join ( cwd , instrumentPath ) ;
@@ -187,36 +190,107 @@ export function createEsmAndCjsTests(
187
190
throw new Error ( `Instrument file not found: ${ mjsInstrumentPath } ` ) ;
188
191
}
189
192
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
+ } ) ;
192
232
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
+ }
197
249
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' ) ;
203
281
} ) ;
204
282
283
+ // Clean up the tmp directory after both esm and cjs suites have run
205
284
afterAll ( ( ) => {
206
285
try {
207
- unlinkSync ( cjsInstrumentPath ) ;
208
- } catch {
209
- // Ignore errors here
210
- }
211
- try {
212
- unlinkSync ( cjsScenarioPath ) ;
286
+ rmSync ( tmpDirPath , { recursive : true , force : true } ) ;
213
287
} 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
+ }
215
292
}
216
293
} ) ;
217
-
218
- const testFn = options ?. failsOnCjs ? test . fails : test ;
219
- callback ( ( ) => createRunner ( cjsScenarioPath ) . withFlags ( '--require' , cjsInstrumentPath ) , testFn , 'cjs' ) ;
220
294
} ) ;
221
295
}
222
296
0 commit comments