@@ -27,6 +27,8 @@ import {
2727} from "./config-env-vars" ;
2828
2929const CONFIG_WRAPPER_MODULE = "@unflakable/cypress-plugin/config-wrapper" ;
30+ const CONFIG_WRAPPER_SYNC_MODULE =
31+ "@unflakable/cypress-plugin/config-wrapper-sync" ;
3032
3133const debug = _debug ( "unflakable:main" ) ;
3234
@@ -266,6 +268,8 @@ const main = async (): Promise<void> => {
266268 const unflakableConfig = await loadConfig ( projectRoot , args [ "test-suite-id" ] ) ;
267269 debug ( `Unflakable plugin is ${ unflakableConfig . enabled ? "en" : "dis" } abled` ) ;
268270
271+ let configFile : string | undefined = undefined ;
272+
269273 if ( unflakableConfig . enabled ) {
270274 if ( args . branch !== undefined ) {
271275 branchOverride . value = args . branch ;
@@ -293,43 +297,77 @@ const main = async (): Promise<void> => {
293297 JSON . stringify ( unflakableConfig ) ;
294298
295299 if ( args [ "auto-config" ] ) {
300+ let userConfigPath : string | undefined = undefined ;
296301 if ( runOptions . config !== undefined ) {
297302 ENV_VAR_USER_CONFIG_JSON . value = JSON . stringify ( runOptions . config ) ;
298303 } else {
299- const userConfigPath = await resolveUserConfigPath (
300- projectRoot ,
301- runOptions
302- ) ;
304+ userConfigPath = await resolveUserConfigPath ( projectRoot , runOptions ) ;
303305 ENV_VAR_USER_CONFIG_PATH . value = userConfigPath ;
306+ }
304307
305- // By default, Cypress invokes ts-node on CommonJS TypeScript projects by setting `dir`
306- // (deprecated alias for `cwd`) to the directory containing the Cypress config file:
307- // https://github.com/cypress-io/cypress/blob/62f58e00ec0e1f95bc0db3c644638e4882b91992/packages/server/lib/plugins/child/ts_node.js#L63
308-
309- // For both ESM and CommonJS TypeScript projects, Cypress invokes ts-node with the CWD set
310- // to that directory:
311- // https://github.com/cypress-io/cypress/blob/62f58e00ec0e1f95bc0db3c644638e4882b91992/packages/data-context/src/data/ProjectConfigIpc.ts#L260
312-
313- // Since we're passing our `config-wrapper.js` as the Cypress config, the CWD becomes our
314- // dist/ directory. However, we need ts-node to load the user's tsconfig.json, not our own,
315- // or the user's cypress.config.ts file may not load properly when we require()/import()
316- // it.
317- // To accomplish this, we try to discover the user's tsconfig.json by traversing the
318- // ancestor directories containing the user's Cypress config file. This is the same
319- // approach TypeScript uses:
320- // https://github.com/microsoft/TypeScript/blob/2beeb8b93143f75cdf788d05bb3678ce3ff0e2b3/src/compiler/program.ts#L340-L345
321-
322- // If we find a tsconfig.json, we set the TS_NODE_PROJECT environment variable to the
323- // directory containing it, which ts-node then uses instead of searching the `dir` passed by
324- // Cypress.
325- const userTsConfig = await findUserTsConfig (
326- path . dirname ( userConfigPath )
327- ) ;
328- if ( userTsConfig !== null ) {
329- const tsNodeProject = path . dirname ( userTsConfig ) ;
330- debug ( `Setting TS_NODE_PROJECT to ${ tsNodeProject } ` ) ;
331- process . env . TS_NODE_PROJECT = tsNodeProject ;
332- }
308+ // By default, Cypress invokes ts-node on CommonJS TypeScript projects by setting `dir`
309+ // (deprecated alias for `cwd`) to the directory containing the Cypress config file:
310+ // https://github.com/cypress-io/cypress/blob/62f58e00ec0e1f95bc0db3c644638e4882b91992/packages/server/lib/plugins/child/ts_node.js#L63
311+
312+ // For both ESM and CommonJS TypeScript projects, Cypress invokes ts-node with the CWD set
313+ // to that directory:
314+ // https://github.com/cypress-io/cypress/blob/62f58e00ec0e1f95bc0db3c644638e4882b91992/packages/data-context/src/data/ProjectConfigIpc.ts#L260
315+
316+ // Since we're passing our `config-wrapper.js` as the Cypress config, the CWD becomes our
317+ // dist/ directory. However, we need ts-node to load the user's tsconfig.json, not our own,
318+ // or the user's cypress.config.ts file may not load properly when we require()/import()
319+ // it.
320+ // To accomplish this, we try to discover the user's tsconfig.json by traversing the
321+ // ancestor directories containing the user's Cypress config file. This is the same
322+ // approach TypeScript uses:
323+ // https://github.com/microsoft/TypeScript/blob/2beeb8b93143f75cdf788d05bb3678ce3ff0e2b3/src/compiler/program.ts#L340-L345
324+
325+ // If we find a tsconfig.json, we set the TS_NODE_PROJECT environment variable to the
326+ // directory containing it, which ts-node then uses instead of searching the `dir` passed by
327+ // Cypress.
328+ const userTsConfig = await findUserTsConfig (
329+ path . dirname ( userConfigPath ?? projectRoot )
330+ ) ;
331+ if ( userTsConfig !== null ) {
332+ const tsNodeProject = path . dirname ( userTsConfig ) ;
333+ debug ( `Setting TS_NODE_PROJECT to ${ tsNodeProject } ` ) ;
334+ process . env . TS_NODE_PROJECT = tsNodeProject ;
335+ }
336+
337+ // ESM configuration files can only be imported via dynamic import(), which is async.
338+ // However,
339+ // CommonJS files can't have top-level await, which means we can't have a CommonJS wrapper
340+ // (config-wrapper-sync) import an ESM configuration file. For that, we use the ESM config
341+ // wrapper. However, the ESM wrapper doesn't work for CommonJS TypeScript projects unless
342+ // they explicitly set `moduleResolution: node16` (or `nodenext`), which we don't want to
343+ // require from users. This is why we detect whether the project and/or config file are ESM
344+ // when deciding which config wrapper to use. We assume that if the Cypress config file is
345+ // .mjs/.mts, the TypeScript project (if any) is already using `node16`/`nodenext` (since
346+ // otherwise, the project's own Cypress config file wouldn't work).
347+ const userConfigIsEsm =
348+ userConfigPath !== undefined
349+ ? [ ".mjs" , ".mts" ] . includes ( path . extname ( userConfigPath ) )
350+ : false ;
351+
352+ // Try to determine whether the project is using ESM, as Cypress does. See
353+ // https://github.com/cypress-io/cypress/blob/62f58e00ec0e1f95bc0db3c644638e4882b91992/packages/data-context/src/data/ProjectConfigIpc.ts#L276-L285
354+ try {
355+ const pkgJson = JSON . parse (
356+ ( await fs . readFile ( path . join ( projectRoot , "package.json" ) ) ) . toString (
357+ "utf8"
358+ )
359+ ) as { type ?: string } ;
360+
361+ configFile =
362+ pkgJson . type === "module" || userConfigIsEsm
363+ ? require . resolve ( CONFIG_WRAPPER_MODULE )
364+ : require . resolve ( CONFIG_WRAPPER_SYNC_MODULE ) ;
365+ } catch ( e ) {
366+ // Project does not have `package.json` or it was not found.
367+ // Reasonable to assume not using es modules unless config is explicitly ESM.
368+ configFile = userConfigIsEsm
369+ ? require . resolve ( CONFIG_WRAPPER_MODULE )
370+ : require . resolve ( CONFIG_WRAPPER_SYNC_MODULE ) ;
333371 }
334372 }
335373
@@ -344,7 +382,7 @@ const main = async (): Promise<void> => {
344382 ...runOptions ,
345383 ...( args [ "auto-config" ]
346384 ? {
347- configFile : require . resolve ( CONFIG_WRAPPER_MODULE ) ,
385+ configFile,
348386 }
349387 : { } ) ,
350388 quiet : true ,
0 commit comments