Skip to content

Commit 6064ea6

Browse files
committed
chore: use tsx loader to load all config files, regardless of cjs, mjs, or typescript configuration [run ci]
1 parent 10d2a04 commit 6064ea6

File tree

20 files changed

+306
-302
lines changed

20 files changed

+306
-302
lines changed

.circleci/cache-version.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Bump this version to force CI to re-create the cache from scratch.
22

3-
2-10-25
3+
2-20-25

.circleci/workflows.yml

+5-5
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ mainBuildFilters: &mainBuildFilters
3939
- chore/update_wdio_deps
4040
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
4141
- 'update-v8-snapshot-cache-on-develop'
42-
- 'ryanm/chore/add_internal_studio'
42+
- 'experiment_with_tsx_to_load_config'
4343

4444
# usually we don't build Mac app - it takes a long time
4545
# but sometimes we want to really confirm we are doing the right thing
@@ -50,7 +50,7 @@ macWorkflowFilters: &darwin-workflow-filters
5050
- equal: [ develop, << pipeline.git.branch >> ]
5151
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
5252
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
53-
- equal: [ 'ryanm/chore/add_internal_studio', << pipeline.git.branch >> ]
53+
- equal: [ 'experiment_with_tsx_to_load_config', << pipeline.git.branch >> ]
5454
- matches:
5555
pattern: /^release\/\d+\.\d+\.\d+$/
5656
value: << pipeline.git.branch >>
@@ -61,7 +61,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters
6161
- equal: [ develop, << pipeline.git.branch >> ]
6262
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
6363
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
64-
- equal: [ 'ryanm/chore/add_internal_studio', << pipeline.git.branch >> ]
64+
- equal: [ 'experiment_with_tsx_to_load_config', << pipeline.git.branch >> ]
6565
- matches:
6666
pattern: /^release\/\d+\.\d+\.\d+$/
6767
value: << pipeline.git.branch >>
@@ -84,7 +84,7 @@ windowsWorkflowFilters: &windows-workflow-filters
8484
- equal: [ develop, << pipeline.git.branch >> ]
8585
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
8686
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
87-
- equal: [ 'ryanm/chore/add_internal_studio', << pipeline.git.branch >> ]
87+
- equal: [ 'experiment_with_tsx_to_load_config', << pipeline.git.branch >> ]
8888
- matches:
8989
pattern: /^release\/\d+\.\d+\.\d+$/
9090
value: << pipeline.git.branch >>
@@ -160,7 +160,7 @@ commands:
160160
name: Set environment variable to determine whether or not to persist artifacts
161161
command: |
162162
echo "Setting SHOULD_PERSIST_ARTIFACTS variable"
163-
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "ryanm/chore/add_internal_studio" ]]; then
163+
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "experiment_with_tsx_to_load_config" ]]; then
164164
export SHOULD_PERSIST_ARTIFACTS=true
165165
fi' >> "$BASH_ENV"
166166
# You must run `setup_should_persist_artifacts` command and be using bash before running this command

npm/vite-dev-server/tsconfig.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@
5555
"importsNotUsedAsValues": "error",
5656
/* skips checking declaration types. we skip this because we have multiple versions of vite installed as dev dependencies */
5757
"skipLibCheck": true,
58+
/** NOTE: only needed to run data-context package in system tests */
59+
"experimentalDecorators": true
5860
},
59-
"include": ["src"],
61+
"include": ["src", "../../packages/data-context/src/**/*.ts"],
6062
"exclude": ["node_modules", "*.js"]
6163
}

npm/webpack-dev-server/tsconfig.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@
5454
* as importsNotUsedAsValues, we need to use "verbatimModuleSyntax", which will require this package to be an ES Module.
5555
*/
5656
"importsNotUsedAsValues": "error",
57+
/** NOTE: only needed to run data-context package in system tests */
58+
"experimentalDecorators": true
5759
},
58-
"include": ["src"],
60+
"include": ["src", "../../packages/data-context/src/**/*.ts"],
5961
"exclude": ["node_modules", "*.js"]
6062
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@
209209
"through2": "^4.0.2",
210210
"tree-kill": "1.2.2",
211211
"ts-node": "^10.9.2",
212+
"tsx": "4.19.3",
212213
"typescript": "5.3.3",
213214
"yaml": "2.7.0",
214215
"yarn-deduplicate": "3.1.0"

packages/app/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"*.d.ts",
1010
"../frontend-shared/src/**/*.vue",
1111
"../frontend-shared/src/**/*.tsx",
12-
"../frontend-shared/cypress/**/*.ts"
12+
"../frontend-shared/cypress/**/*.ts",
13+
"../data-context/src/**/*.ts",
1314
],
1415
"compilerOptions": {
1516
"noImplicitThis": true,

packages/data-context/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"execa": "1.0.0",
4040
"front-matter": "^4.0.2",
4141
"fs-extra": "8.1.0",
42+
"get-tsconfig": "4.10.0",
4243
"getenv": "1.0.0",
4344
"globby": "^11.0.1",
4445
"graphql": "^15.5.1",
@@ -60,6 +61,7 @@
6061
"server-destroy": "1.0.1",
6162
"simple-git": "3.25.0",
6263
"stringify-object": "^3.0.0",
64+
"tsx": "4.19.3",
6365
"underscore.string": "^3.3.6",
6466
"wonka": "^4.0.15"
6567
},

packages/data-context/src/actions/MigrationActions.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { hasTypeScriptInstalled, toPosix } from '../util'
3838

3939
const debug = debugLib('cypress:data-context:MigrationActions')
4040

41-
const tsNode = toPosix(require.resolve('@packages/server/lib/plugins/child/register_ts_node'))
41+
const tsxCjs = toPosix(require.resolve('tsx/cjs'))
4242

4343
export function getConfigWithDefaults (legacyConfig: any) {
4444
const newConfig = _.cloneDeep(legacyConfig)
@@ -99,20 +99,20 @@ export async function processConfigViaLegacyPlugins (projectRoot: string, legacy
9999
const configProcessArgs = ['--projectRoot', projectRoot, '--file', cwd]
100100
const CHILD_PROCESS_FILE_PATH = require.resolve('@packages/server/lib/plugins/child/require_async_child')
101101

102-
// use ts-node if they've got typescript installed
102+
// use tsx if they've got typescript installed
103103
// this matches the 9.x behavior, which is what we want for
104104
// processing legacy pluginsFile (we never supported `"type": "module") in 9.x.
105105
if (hasTypeScriptInstalled(projectRoot)) {
106-
const tsNodeLoader = `--require "${tsNode}"`
106+
const tsxLoader = `--require "${tsxCjs}"`
107107

108108
if (!childOptions.env) {
109109
childOptions.env = {}
110110
}
111111

112112
if (childOptions.env.NODE_OPTIONS) {
113-
childOptions.env.NODE_OPTIONS += ` ${tsNodeLoader}`
113+
childOptions.env.NODE_OPTIONS += ` ${tsxLoader}`
114114
} else {
115-
childOptions.env.NODE_OPTIONS = tsNodeLoader
115+
childOptions.env.NODE_OPTIONS = tsxLoader
116116
}
117117
}
118118

packages/data-context/src/data/ProjectConfigIpc.ts

+50-70
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,24 @@ import { CypressError, getError } from '@packages/errors'
33
import type { FullConfig, TestingType } from '@packages/types'
44
import { ChildProcess, fork, ForkOptions, spawn } from 'child_process'
55
import EventEmitter from 'events'
6-
import fs from 'fs-extra'
76
import path from 'path'
87
import inspector from 'inspector'
98
import debugLib from 'debug'
9+
import { getTsconfig } from 'get-tsconfig'
1010
import { autoBindDebug, hasTypeScriptInstalled, toPosix } from '../util'
1111
import _ from 'lodash'
12-
import { pathToFileURL } from 'url'
1312
import os from 'os'
1413
import semver from 'semver'
1514
import type { OTLPTraceExporterCloud } from '@packages/telemetry'
1615
import { telemetry, encodeTelemetryContext } from '@packages/telemetry'
1716

1817
const pkg = require('@packages/root')
1918
const debug = debugLib(`cypress:lifecycle:ProjectConfigIpc`)
19+
const debugVerbose = debugLib(`cypress-verbose:lifecycle:ProjectConfigIpc`)
2020

2121
const CHILD_PROCESS_FILE_PATH = require.resolve('@packages/server/lib/plugins/child/require_async_child')
2222

23-
const tsNodeEsm = pathToFileURL(require.resolve('ts-node/esm/transpile-only')).href
24-
const tsNode = toPosix(require.resolve('@packages/server/lib/plugins/child/register_ts_node'))
23+
const tsx = toPosix(require.resolve('tsx'))
2524

2625
export type IpcHandler = (ipc: ProjectConfigIpc) => void
2726

@@ -250,10 +249,11 @@ export class ProjectConfigIpc extends EventEmitter {
250249

251250
private forkConfigProcess () {
252251
const configProcessArgs = ['--projectRoot', this.projectRoot, '--file', this.configFilePath]
253-
// allow the use of ts-node in subprocesses tests by removing the env constant from it
252+
// allow the use of tsx in subprocesses tests by removing the env constant from it
254253
// without this line, packages/ts/register.js never registers the ts-node module for config and
255254
// run_plugins can't use the config module.
256-
const env = _.omit(process.env, 'CYPRESS_INTERNAL_E2E_TESTING_SELF')
255+
// we also do not want telemetry enabled within our cy-in-cy tests as it isn't configured to handled it
256+
const env = _.omit(process.env, 'CYPRESS_INTERNAL_E2E_TESTING_SELF', 'CYPRESS_INTERNAL_ENABLE_TELEMETRY')
257257

258258
env.NODE_OPTIONS = process.env.ORIGINAL_NODE_OPTIONS || ''
259259

@@ -273,80 +273,60 @@ export class ProjectConfigIpc extends EventEmitter {
273273

274274
debug('fork child process %o', { CHILD_PROCESS_FILE_PATH, configProcessArgs, childOptions: _.omit(childOptions, 'env') })
275275

276-
let isProjectUsingESModules = false
276+
if (!childOptions.env) {
277+
childOptions.env = {}
278+
}
277279

278-
try {
279-
// TODO: convert this to async FS methods
280-
// eslint-disable-next-line no-restricted-syntax
281-
const pkgJson = fs.readJsonSync(path.join(this.projectRoot, 'package.json'))
280+
// use for 20.6.0 and above
281+
let tsxLoader = `--import ${tsx}`
282282

283-
isProjectUsingESModules = pkgJson.type === 'module'
284-
} catch (e) {
285-
// project does not have `package.json` or it was not found
286-
// reasonable to assume not using es modules
283+
// @see https://tsx.is/dev-api/node-cli#node-js-cli
284+
if (this.nodeVersion && semver.lte(this.nodeVersion, '20.5.1')) {
285+
debug(`detected node version ${this.nodeVersion}. Using deprecated "--loader" flag.`)
286+
tsxLoader = `--loader ${tsx}`
287287
}
288288

289-
if (!childOptions.env) {
290-
childOptions.env = {}
289+
// in nodejs 22.7.0, the --experimental-detect-module option is now enabled by default.
290+
// We need to disable it with the --no-experimental-detect-module flag.
291+
// @see https://github.com/cypress-io/cypress/issues/30084
292+
if (this.nodeVersion && semver.gte(this.nodeVersion, '22.7.0')) {
293+
debug(`detected node version ${this.nodeVersion}, adding --no-experimental-detect-module option to child_process NODE_OPTIONS.`)
294+
tsxLoader = `${tsxLoader} --no-experimental-detect-module`
295+
}
296+
297+
// in nodejs 22.12.0, the --experimental-require-module option is now enabled by default.
298+
// We need to disable it with the --no-experimental-require-module flag.
299+
// @see https://github.com/cypress-io/cypress/issues/30715
300+
if (this.nodeVersion && semver.gte(this.nodeVersion, '22.12.0')) {
301+
debug(`detected node version ${this.nodeVersion}, adding --no-experimental-require-module option to child_process NODE_OPTIONS.`)
302+
tsxLoader = `${tsxLoader} --no-experimental-require-module`
291303
}
292304

293-
// If they've got TypeScript installed, we can use
294-
// ts-node for CommonJS
295-
// ts-node/esm for ESM
296-
if (hasTypeScriptInstalled(this.projectRoot)) {
305+
// If they've got TypeScript installed, we can use tsx for CommonJS and ESM.
306+
// @see https://tsx.is/dev-api/node-cli#node-js-cli
307+
const hasTs = hasTypeScriptInstalled(this.projectRoot)
308+
309+
if (hasTs) {
297310
debug('found typescript in %s', this.projectRoot)
298-
if (isProjectUsingESModules) {
299-
debug(`using --experimental-specifier-resolution=node with --loader ${tsNodeEsm}`)
300-
// Use the ts-node/esm loader so they can use TypeScript with `"type": "module".
301-
// The loader API is experimental and will change.
302-
// The same can be said for the other alternative, esbuild, so this is the
303-
// best option that leverages the existing modules we bundle in the binary.
304-
// @see ts-node esm loader https://typestrong.org/ts-node/docs/usage/#node-flags-and-other-tools
305-
// @see Node.js Loader API https://nodejs.org/api/esm.html#customizing-esm-specifier-resolution-algorithm
306-
let tsNodeEsmLoader = `--experimental-specifier-resolution=node --loader ${tsNodeEsm}`
307-
308-
// in nodejs 22.7.0, the --experimental-detect-module option is now enabled by default.
309-
// We need to disable it with the --no-experimental-detect-module flag.
310-
// @see https://github.com/cypress-io/cypress/issues/30084
311-
if (this.nodeVersion && semver.gte(this.nodeVersion, '22.7.0')) {
312-
debug(`detected node version ${this.nodeVersion}, adding --no-experimental-detect-module option to child_process NODE_OPTIONS.`)
313-
tsNodeEsmLoader = `${tsNodeEsmLoader} --no-experimental-detect-module`
314-
}
315-
316-
// in nodejs 22.12.0, the --experimental-require-module option is now enabled by default.
317-
// We need to disable it with the --no-experimental-require-module flag.
318-
// @see https://github.com/cypress-io/cypress/issues/30715
319-
if (this.nodeVersion && semver.gte(this.nodeVersion, '22.12.0')) {
320-
debug(`detected node version ${this.nodeVersion}, adding --no-experimental-require-module option to child_process NODE_OPTIONS.`)
321-
tsNodeEsmLoader = `${tsNodeEsmLoader} --no-experimental-require-module`
322-
}
323-
324-
if (childOptions.env.NODE_OPTIONS) {
325-
childOptions.env.NODE_OPTIONS += ` ${tsNodeEsmLoader}`
326-
} else {
327-
childOptions.env.NODE_OPTIONS = tsNodeEsmLoader
328-
}
311+
312+
const tsConfigIfExists = getTsconfig(this.projectRoot)
313+
314+
if (tsConfigIfExists) {
315+
debug(`tsconfig.json found at ${tsConfigIfExists.path}`)
316+
childOptions.env.TSX_TSCONFIG_PATH = tsConfigIfExists.path
317+
318+
debugVerbose(`tsconfig.json parsed as follows: %o`, tsConfigIfExists.config)
329319
} else {
330-
// Not using ES Modules (via "type": "module"),
331-
// so we just register the standard ts-node module
332-
// to handle TypeScript that is compiled to CommonJS.
333-
// We do NOT use the `--loader` flag because we have some additional
334-
// custom logic for ts-node when used with CommonJS that needs to be evaluated
335-
// so we need to load and evaluate the hook first using the `--require` module API.
336-
const tsNodeLoader = `--require "${tsNode}"`
337-
338-
debug(`using cjs with --require ${tsNode}`)
339-
340-
if (childOptions.env.NODE_OPTIONS) {
341-
childOptions.env.NODE_OPTIONS += ` ${tsNodeLoader}`
342-
} else {
343-
childOptions.env.NODE_OPTIONS = tsNodeLoader
344-
}
320+
debug(`No tsconfig.json found! Attempting to parse file without tsconfig.json.`)
345321
}
322+
}
323+
324+
debug(`using generic ${tsxLoader} for esm and cjs ${hasTs ? 'with typescript' : ''}.`)
325+
326+
if (childOptions.env.NODE_OPTIONS) {
327+
childOptions.env.NODE_OPTIONS += ` ${tsxLoader}`
346328
} else {
347-
// Just use Node's built-in ESM support.
348-
// TODO: Consider using userland `esbuild` with Node's --loader API to handle ESM.
349-
debug(`no typescript found, just use regular Node.js`)
329+
childOptions.env.NODE_OPTIONS = tsxLoader
350330
}
351331

352332
const telemetryCtx = encodeTelemetryContext({ context: telemetry.getActiveContextObject(), version: pkg.version })

0 commit comments

Comments
 (0)