Skip to content
This repository was archived by the owner on Sep 1, 2024. It is now read-only.

Commit 5f8dd7c

Browse files
committed
[cypress] Add synchronous config wrapper for CommonJS
1 parent 300640e commit 5f8dd7c

File tree

22 files changed

+295
-147
lines changed

22 files changed

+295
-147
lines changed

README.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,6 @@ This plugin maintains compatibility with the Cypress and Node.js versions listed
3131
[![11.2.0+ | 12.0.0+](https://img.shields.io/badge/Cypress-11.2.0%2B%20%7C%2012.0.0%2B-17202C?logo=cypress&labelColor=white&logoColor=17202C&style=flat)](https://cypress.io)
3232
[![16 | 18 | 20](https://img.shields.io/badge/Node.js-16%20%7C%2018%20%7C%2020-339933?logo=node.js&labelColor=white&logoColor=339933&style=flat)](https://nodejs.org)
3333

34-
For TypeScript projects, `typescript` version 4.7 or later is required, and `tsconfig.json` must set
35-
[`moduleResolution`](https://www.typescriptlang.org/tsconfig#moduleResolution) to `node16` or
36-
`nodenext`. This setting is required so that
37-
[ES modules](https://nodejs.org/api/esm.html#modules-ecmascript-modules) resolve correctly.
38-
3934
## Jest Plugin
4035

4136
The Jest plugin enables users of the [Jest](https://jestjs.io) JavaScript test framework

packages/cypress-plugin/README.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,6 @@ This plugin maintains compatibility with the Cypress and Node.js versions listed
2222
[![11.2.0+ | 12.0.0+](https://img.shields.io/badge/Cypress-11.2.0%2B%20%7C%2012.0.0%2B-17202C?logo=cypress&labelColor=white&logoColor=17202C&style=flat)](https://cypress.io)
2323
[![16 | 18 | 20](https://img.shields.io/badge/Node.js-16%20%7C%2018%20%7C%2020-339933?logo=node.js&labelColor=white&logoColor=339933&style=flat)](https://nodejs.org)
2424

25-
For TypeScript projects, `typescript` version 4.7 or later is required, and `tsconfig.json` must set
26-
[`moduleResolution`](https://www.typescriptlang.org/tsconfig#moduleResolution) to `node16` or
27-
`nodenext`. This setting is required so that
28-
[ES modules](https://nodejs.org/api/esm.html#modules-ecmascript-modules) resolve correctly.
29-
3025
## Contributing
3126

3227
To report a bug or request a new feature, please

packages/cypress-plugin/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
"types": "./dist/config-wrapper.d.ts",
1919
"default": "./dist/config-wrapper.mjs"
2020
},
21+
"./config-wrapper-sync": {
22+
"types": "./dist/config-wrapper-sync.d.ts",
23+
"default": "./dist/config-wrapper-sync.js"
24+
},
2125
"./reporter": {
2226
"types": "./dist/reporter.d.ts",
2327
"default": "./dist/reporter.js"
@@ -33,6 +37,7 @@
3337
"dist/**/*.mjs",
3438
"dist/index.d.ts",
3539
"dist/config-wrapper.d.ts",
40+
"dist/config-wrapper-sync.d.ts",
3641
"dist/reporter.d.ts",
3742
"dist/skip-tests.d.ts"
3843
],
@@ -86,7 +91,7 @@
8691
},
8792
"scripts": {
8893
"build": "yarn clean && tsc --noEmit && tsc --noEmit -p src && yarn build:cjs && yarn build:esm",
89-
"build:cjs": "rollup --config --dir dist",
94+
"build:cjs": "rollup --config --dir dist && rollup --config --input src/config-wrapper-sync.ts --file dist/config-wrapper-sync.js --chunkFileNames \"[name]-[hash]-sync.js\"",
9095
"build:esm": "rollup --config --input src/config-wrapper.ts --file dist/config-wrapper.mjs --format es --chunkFileNames \"[name]-[hash].mjs\"",
9196
"clean": "rm -rf dist/",
9297
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --useStderr --verbose"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) 2023 Developer Innovations, LLC
2+
3+
import { wrapCypressConfig } from "./index";
4+
import _debug from "debug";
5+
import { loadUserConfigSync } from "./load-user-config";
6+
7+
const debug = _debug("unflakable:config-wrapper-sync");
8+
9+
const userConfig = loadUserConfigSync();
10+
debug("Loaded user config %o", userConfig);
11+
12+
export default wrapCypressConfig(userConfig);

packages/cypress-plugin/src/config-wrapper.ts

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,10 @@
22

33
import { wrapCypressConfig } from "./index";
44
import _debug from "debug";
5-
import { require } from "./utils";
6-
import {
7-
ENV_VAR_USER_CONFIG_JSON,
8-
ENV_VAR_USER_CONFIG_PATH,
9-
} from "./config-env-vars";
10-
import path from "path";
5+
import { loadUserConfig } from "./load-user-config";
116

127
const debug = _debug("unflakable:config-wrapper");
138

14-
type LoadedConfig =
15-
| {
16-
default: Cypress.ConfigOptions<unknown>;
17-
}
18-
| (Cypress.ConfigOptions<unknown> & { default: undefined });
19-
20-
const loadUserConfig = async (): Promise<Cypress.ConfigOptions<unknown>> => {
21-
if (ENV_VAR_USER_CONFIG_JSON.value !== undefined) {
22-
debug(`Parsing inline user config ${ENV_VAR_USER_CONFIG_JSON.value}`);
23-
24-
return JSON.parse(
25-
ENV_VAR_USER_CONFIG_JSON.value
26-
) as Cypress.ConfigOptions<unknown>;
27-
} else if (ENV_VAR_USER_CONFIG_PATH.value === undefined) {
28-
throw new Error("No user config to load");
29-
}
30-
31-
debug(`Loading user config from ${ENV_VAR_USER_CONFIG_PATH.value}`);
32-
33-
// Relative paths from the user's config need to resolve relative to the location of their
34-
// cypress.config.js, not ours. This affects things like webpack for component testing.
35-
const configPathDir = path.dirname(ENV_VAR_USER_CONFIG_PATH.value);
36-
debug(`Changing working directory to ${configPathDir}`);
37-
process.chdir(configPathDir);
38-
39-
// For CommonJS projects, we need to use require(), at least for TypeScript config files.
40-
// Dynamic import() doesn't support TypeScript imports in CommonJS projects, at least the way
41-
// Cypress sets up the environment before loading the config.
42-
try {
43-
const config = require(ENV_VAR_USER_CONFIG_PATH.value) as LoadedConfig;
44-
return config.default ?? config;
45-
} catch (e) {
46-
// require() can't import ES modules, so now we try a dynamic import(). This is what gets used
47-
// for ESM projects.
48-
debug(`require() failed; attempting dynamic import(): ${e as string}`);
49-
const config = (await import(
50-
ENV_VAR_USER_CONFIG_PATH.value
51-
)) as LoadedConfig;
52-
return config.default ?? config;
53-
}
54-
};
55-
569
const userConfig = await loadUserConfig();
5710
debug("Loaded user config %o", userConfig);
5811

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) 2023 Developer Innovations, LLC
2+
3+
import _debug from "debug";
4+
import { require } from "./utils";
5+
import {
6+
ENV_VAR_USER_CONFIG_JSON,
7+
ENV_VAR_USER_CONFIG_PATH,
8+
} from "./config-env-vars";
9+
import path from "path";
10+
11+
const debug = _debug("unflakable:load-user-config");
12+
13+
export type LoadedConfig =
14+
| {
15+
default: Cypress.ConfigOptions<unknown>;
16+
}
17+
| (Cypress.ConfigOptions<unknown> & { default: undefined });
18+
19+
export const loadUserConfigSync = (): Cypress.ConfigOptions<unknown> => {
20+
if (ENV_VAR_USER_CONFIG_JSON.value !== undefined) {
21+
debug(`Parsing inline user config ${ENV_VAR_USER_CONFIG_JSON.value}`);
22+
23+
return JSON.parse(
24+
ENV_VAR_USER_CONFIG_JSON.value
25+
) as Cypress.ConfigOptions<unknown>;
26+
} else if (ENV_VAR_USER_CONFIG_PATH.value === undefined) {
27+
throw new Error("No user config to load");
28+
}
29+
30+
debug(`Loading user config from ${ENV_VAR_USER_CONFIG_PATH.value}`);
31+
32+
// Relative paths from the user's config need to resolve relative to the location of their
33+
// cypress.config.js, not ours. This affects things like webpack for component testing.
34+
const configPathDir = path.dirname(ENV_VAR_USER_CONFIG_PATH.value);
35+
debug(`Changing working directory to ${configPathDir}`);
36+
process.chdir(configPathDir);
37+
38+
// For CommonJS projects, we need to use require(), at least for TypeScript config files.
39+
// Dynamic import() doesn't support TypeScript imports in CommonJS projects, at least the way
40+
// Cypress sets up the environment before loading the config.
41+
const config = require(ENV_VAR_USER_CONFIG_PATH.value) as LoadedConfig;
42+
return config.default ?? config;
43+
};
44+
45+
export const loadUserConfig = async (): Promise<
46+
Cypress.ConfigOptions<unknown>
47+
> => {
48+
// For CommonJS projects, we need to use require(), at least for TypeScript config files.
49+
// Dynamic import() doesn't support TypeScript imports in CommonJS projects, at least the way
50+
// Cypress sets up the environment before loading the config.
51+
try {
52+
return loadUserConfigSync();
53+
} catch (e) {
54+
// require() can't import ES modules, so now we try a dynamic import(). This is what gets used
55+
// for ESM projects.
56+
debug(`require() failed; attempting dynamic import(): ${e as string}`);
57+
const config = (await import(
58+
ENV_VAR_USER_CONFIG_PATH.value as string
59+
)) as LoadedConfig;
60+
return config.default ?? config;
61+
}
62+
};

packages/cypress-plugin/src/main.ts

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import {
2727
} from "./config-env-vars";
2828

2929
const CONFIG_WRAPPER_MODULE = "@unflakable/cypress-plugin/config-wrapper";
30+
const CONFIG_WRAPPER_SYNC_MODULE =
31+
"@unflakable/cypress-plugin/config-wrapper-sync";
3032

3133
const 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,

packages/cypress-plugin/test/integration-common/package.json

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,6 @@
11
{
22
"name": "cypress-integration-common",
33
"private": true,
4-
"exports": {
5-
"./config": {
6-
"types": "./dist/config.d.ts",
7-
"default": "./dist/config.js"
8-
},
9-
"./git": {
10-
"types": "./dist/git.d.ts",
11-
"default": "./dist/git.js"
12-
},
13-
"./mock-cosmiconfig": {
14-
"default": "./dist/mock-cosmiconfig.js"
15-
}
16-
},
174
"dependencies": {
185
"debug": "^4.3.3",
196
"expect": "^29.5.0",
@@ -25,6 +12,7 @@
2512
"@rollup/plugin-typescript": "^11.1.1",
2613
"@unflakable/plugins-common": "workspace:^",
2714
"rollup": "^3.21.1",
15+
"rollup-plugin-dts": "^5.3.0",
2816
"typescript": "^4.9.5"
2917
},
3018
"scripts": {

0 commit comments

Comments
 (0)