codeceptjs 4.0.0-beta.9.esm-aria → 4.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/bin/codecept.js +2 -2
  2. package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
  3. package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
  4. package/lib/actor.js +12 -8
  5. package/lib/codecept.js +31 -1
  6. package/lib/command/definitions.js +14 -7
  7. package/lib/command/init.js +2 -1
  8. package/lib/command/run-workers.js +12 -2
  9. package/lib/command/workers/runTests.js +121 -9
  10. package/lib/config.js +24 -33
  11. package/lib/container.js +158 -24
  12. package/lib/helper/GraphQL.js +6 -4
  13. package/lib/helper/JSONResponse.js +3 -4
  14. package/lib/helper/Playwright.js +195 -423
  15. package/lib/helper/Puppeteer.js +155 -38
  16. package/lib/helper/REST.js +15 -9
  17. package/lib/helper/WebDriver.js +158 -23
  18. package/lib/helper/errors/ElementNotFound.js +5 -2
  19. package/lib/helper/errors/MultipleElementsFound.js +135 -0
  20. package/lib/listener/config.js +11 -3
  21. package/lib/listener/helpers.js +2 -14
  22. package/lib/locator.js +32 -0
  23. package/lib/mocha/cli.js +6 -0
  24. package/lib/mocha/factory.js +7 -27
  25. package/lib/mocha/gherkin.js +4 -4
  26. package/lib/mocha/test.js +4 -2
  27. package/lib/output.js +2 -2
  28. package/lib/plugin/auth.js +2 -1
  29. package/lib/plugin/htmlReporter.js +4 -4
  30. package/lib/step/base.js +14 -1
  31. package/lib/step/meta.js +18 -1
  32. package/lib/step/record.js +8 -0
  33. package/lib/utils/loaderCheck.js +162 -0
  34. package/lib/utils/typescript.js +449 -0
  35. package/lib/workers.js +114 -47
  36. package/package.json +39 -30
  37. package/typings/index.d.ts +101 -4
  38. package/typings/promiseBasedTypes.d.ts +3974 -5516
  39. package/typings/types.d.ts +4146 -5817
  40. package/lib/helper/extras/PlaywrightLocator.js +0 -110
package/bin/codecept.js CHANGED
@@ -174,7 +174,7 @@ program
174
174
  .option('-R, --reporter <name>', 'specify the reporter to use')
175
175
  .option('-S, --sort', 'sort test files')
176
176
  .option('-b, --bail', 'bail after first test failure')
177
- .option('-d, --debug', "enable node's debugger, synonym for node --debug")
177
+ .option('--inspec', "enable node's debugger, synonym for node --debug")
178
178
  .option('-g, --grep <pattern>', 'only run tests matching <pattern>')
179
179
  .option('-f, --fgrep <string>', 'only run tests containing <string>')
180
180
  .option('-i, --invert', 'inverts --grep and --fgrep matches')
@@ -276,7 +276,7 @@ program
276
276
  .option('-R, --reporter <name>', 'specify the reporter to use')
277
277
  .option('-S, --sort', 'sort test files')
278
278
  .option('-b, --bail', 'bail after first test failure')
279
- .option('-d, --debug', "enable node's debugger, synonym for node --debug")
279
+ .option('--inspect', "enable node's debugger, synonym for node --debug")
280
280
  .option('-g, --grep <pattern>', 'only run tests matching <pattern>')
281
281
  .option('-f, --fgrep <string>', 'only run tests containing <string>')
282
282
  .option('-i, --invert', 'inverts --grep and --fgrep matches')
@@ -0,0 +1,10 @@
1
+ Checks that current URL path does NOT match the expected path.
2
+ Query strings and URL fragments are ignored.
3
+
4
+ ```js
5
+ I.dontSeeCurrentPathEquals('/form'); // fails for '/form', '/form?user=1', '/form#section'
6
+ I.dontSeeCurrentPathEquals('/'); // fails for '/', '/?user=ok', '/#top'
7
+ ```
8
+
9
+ @param {string} path value to check.
10
+ @returns {void} automatically synchronized promise through #recorder
@@ -0,0 +1,10 @@
1
+ Checks that current URL path matches the expected path.
2
+ Query strings and URL fragments are ignored.
3
+
4
+ ```js
5
+ I.seeCurrentPathEquals('/info'); // passes for '/info', '/info?user=1', '/info#section'
6
+ I.seeCurrentPathEquals('/'); // passes for '/', '/?user=ok', '/#top'
7
+ ```
8
+
9
+ @param {string} path value to check.
10
+ @returns {void} automatically synchronized promise through #recorder
package/lib/actor.js CHANGED
@@ -75,7 +75,8 @@ export default function (obj = {}, container) {
75
75
  if (!container) {
76
76
  container = Container
77
77
  }
78
-
78
+
79
+ // Get existing actor or create a new one
79
80
  const actor = container.actor() || new Actor()
80
81
 
81
82
  // load all helpers once container initialized
@@ -111,14 +112,17 @@ export default function (obj = {}, container) {
111
112
  }
112
113
  })
113
114
 
114
- container.append({
115
- support: {
116
- I: actor,
117
- },
118
- })
115
+ // Update container.support.I to ensure it has the latest actor reference
116
+ if (!container.actor() || container.actor() !== actor) {
117
+ container.append({
118
+ support: {
119
+ I: actor,
120
+ },
121
+ })
122
+ }
119
123
  })
120
- // store.actor = actor;
121
- // add custom steps from actor
124
+
125
+ // add custom steps from actor immediately
122
126
  Object.keys(obj).forEach(key => {
123
127
  const ms = new MetaStep('I', key)
124
128
  ms.setContext(actor)
package/lib/codecept.js CHANGED
@@ -3,8 +3,9 @@ import { globSync } from 'glob'
3
3
  import shuffle from 'lodash.shuffle'
4
4
  import fsPath from 'path'
5
5
  import { resolve } from 'path'
6
- import { fileURLToPath } from 'url'
6
+ import { fileURLToPath, pathToFileURL } from 'url'
7
7
  import { dirname } from 'path'
8
+ import { createRequire } from 'module'
8
9
 
9
10
  const __filename = fileURLToPath(import.meta.url)
10
11
  const __dirname = dirname(__filename)
@@ -18,6 +19,7 @@ import ActorFactory from './actor.js'
18
19
  import output from './output.js'
19
20
  import { emptyFolder } from './utils.js'
20
21
  import { initCodeceptGlobals } from './globals.js'
22
+ import { validateTypeScriptSetup, getTSNodeESMWarning } from './utils/loaderCheck.js'
21
23
  import recorder from './recorder.js'
22
24
 
23
25
  import storeListener from './listener/store.js'
@@ -66,6 +68,21 @@ class Codecept {
66
68
  modulePath = `${modulePath}.js`
67
69
  }
68
70
  }
71
+ } else {
72
+ // For npm packages, resolve from the user's directory
73
+ // This ensures packages like tsx are found in user's node_modules
74
+ const userDir = global.codecept_dir || process.cwd()
75
+
76
+ try {
77
+ // Use createRequire to resolve from user's directory
78
+ const userRequire = createRequire(pathToFileURL(resolve(userDir, 'package.json')).href)
79
+ const resolvedPath = userRequire.resolve(requiredModule)
80
+ modulePath = pathToFileURL(resolvedPath).href
81
+ } catch (resolveError) {
82
+ // If resolution fails, try direct import (will check from CodeceptJS node_modules)
83
+ // This is the fallback for globally installed packages
84
+ modulePath = requiredModule
85
+ }
69
86
  }
70
87
  // Use dynamic import for ESM
71
88
  await import(modulePath)
@@ -246,6 +263,19 @@ class Codecept {
246
263
  async run(test) {
247
264
  await container.started()
248
265
 
266
+ // Check TypeScript loader configuration before running tests
267
+ const tsValidation = validateTypeScriptSetup(this.testFiles, this.requiringModules || [])
268
+ if (tsValidation.hasError) {
269
+ output.error(tsValidation.message)
270
+ process.exit(1)
271
+ }
272
+
273
+ // Show warning if ts-node/esm is being used
274
+ const tsWarning = getTSNodeESMWarning(this.requiringModules || [])
275
+ if (tsWarning) {
276
+ output.print(output.colors.yellow(tsWarning))
277
+ }
278
+
249
279
  // Ensure translations are loaded for Gherkin features
250
280
  try {
251
281
  const { loadTranslations } = await import('./mocha/gherkin.js')
@@ -41,7 +41,7 @@ const getDefinitionsFileContent = ({ hasCustomHelper, hasCustomStepsFile, helper
41
41
 
42
42
  const importPathsFragment = importPaths.join('\n')
43
43
  const supportObjectsTypeFragment = convertMapToType(supportObject)
44
- const methodsTypeFragment = helperNames.length > 0 ? `interface Methods extends ${helperNames.join(', ')} {}` : ''
44
+ const methodsTypeFragment = helperNames.length > 0 ? `interface Methods extends ${helperNames.join(', ')} {}` : 'interface Methods {}'
45
45
  const translatedActionsFragment = JSON.stringify(translations.vocabulary.actions, null, 2)
46
46
 
47
47
  return generateDefinitionsContent({
@@ -229,18 +229,25 @@ function getImportString(testsPath, targetFolderPath, pathsToType, pathsToValue)
229
229
  const importStrings = []
230
230
 
231
231
  for (const name in pathsToType) {
232
- const relativePath = getPath(pathsToType[name], targetFolderPath, testsPath)
233
- // For ESM modules with default exports, we need to access the default export type
234
- if (relativePath.endsWith('.js')) {
235
- importStrings.push(`type ${name} = typeof import('${relativePath}')['default'];`)
232
+ const originalPath = pathsToType[name]
233
+ const relativePath = getPath(originalPath, targetFolderPath, testsPath)
234
+ // For .js files with plain object exports, access .default to allow TypeScript to extract properties
235
+ // For .ts files, the default export is handled differently by TypeScript
236
+ if (originalPath.endsWith('.js')) {
237
+ importStrings.push(`type ${name} = typeof import('${relativePath}').default;`)
236
238
  } else {
237
239
  importStrings.push(`type ${name} = typeof import('${relativePath}');`)
238
240
  }
239
241
  }
240
242
 
241
243
  for (const name in pathsToValue) {
242
- const relativePath = getPath(pathsToValue[name], targetFolderPath, testsPath)
243
- importStrings.push(`type ${name} = import('${relativePath}');`)
244
+ const originalPath = pathsToValue[name]
245
+ const relativePath = getPath(originalPath, targetFolderPath, testsPath)
246
+ if (originalPath.endsWith('.js') || originalPath.endsWith('.ts')) {
247
+ importStrings.push(`type ${name} = InstanceType<typeof import('${relativePath}').default>;`)
248
+ } else {
249
+ importStrings.push(`type ${name} = import('${relativePath}');`)
250
+ }
244
251
  }
245
252
 
246
253
  return importStrings
@@ -161,7 +161,7 @@ export default async function (initPath) {
161
161
  isTypeScript = true
162
162
  extension = isTypeScript === true ? 'ts' : 'js'
163
163
  packages.push('typescript')
164
- packages.push('ts-node')
164
+ packages.push('tsx') // Add tsx for TypeScript support
165
165
  packages.push('@types/node')
166
166
  }
167
167
 
@@ -172,6 +172,7 @@ export default async function (initPath) {
172
172
  config.tests = result.tests
173
173
  if (isTypeScript) {
174
174
  config.tests = `${config.tests.replace(/\.js$/, `.${extension}`)}`
175
+ config.require = ['tsx/cjs'] // Add tsx/cjs loader for TypeScript tests
175
176
  }
176
177
 
177
178
  // create a directory tests if it is included in tests path
@@ -40,11 +40,22 @@ export default async function (workerCount, selectedRuns, options) {
40
40
 
41
41
  output.print(`CodeceptJS v${Codecept.version()} ${output.standWithUkraine()}`)
42
42
  output.print(`Running tests in ${output.styles.bold(numberOfWorkers)} workers...`)
43
- output.print()
44
43
  store.hasWorkers = true
45
44
 
46
45
  const workers = new Workers(numberOfWorkers, config)
47
46
  workers.overrideConfig(overrideConfigs)
47
+
48
+ // Show test distribution after workers are initialized
49
+ await workers.bootstrapAll()
50
+
51
+ const workerObjects = workers.getWorkers()
52
+ output.print()
53
+ output.print('Test distribution:')
54
+ workerObjects.forEach((worker, index) => {
55
+ const testCount = worker.tests.length
56
+ output.print(` Worker ${index + 1}: ${testCount} test${testCount !== 1 ? 's' : ''}`)
57
+ })
58
+ output.print()
48
59
 
49
60
  workers.on(event.test.failed, test => {
50
61
  output.test.failed(test)
@@ -68,7 +79,6 @@ export default async function (workerCount, selectedRuns, options) {
68
79
  if (options.verbose) {
69
80
  await getMachineInfo()
70
81
  }
71
- await workers.bootstrapAll()
72
82
  await workers.run()
73
83
  } catch (err) {
74
84
  output.error(err)
@@ -11,7 +11,7 @@ import { parentPort, workerData } from 'worker_threads'
11
11
 
12
12
  // Delay imports to avoid ES Module loader race conditions in Node 22.x worker threads
13
13
  // These will be imported dynamically when needed
14
- let event, container, Codecept, getConfig, tryOrDefault, deepMerge
14
+ let event, container, Codecept, getConfig, tryOrDefault, deepMerge, fixErrorStack
15
15
 
16
16
  let stdout = ''
17
17
 
@@ -19,6 +19,48 @@ const stderr = ''
19
19
 
20
20
  const { options, tests, testRoot, workerIndex, poolMode } = workerData
21
21
 
22
+ // Global error handlers to catch critical errors but not test failures
23
+ process.on('uncaughtException', (err) => {
24
+ if (global.container?.tsFileMapping && fixErrorStack) {
25
+ const fileMapping = global.container.tsFileMapping()
26
+ if (fileMapping) {
27
+ fixErrorStack(err, fileMapping)
28
+ }
29
+ }
30
+
31
+ // Log to stderr to bypass stdout suppression
32
+ process.stderr.write(`[Worker ${workerIndex}] UNCAUGHT EXCEPTION: ${err.message}\n`)
33
+ process.stderr.write(`${err.stack}\n`)
34
+
35
+ // Don't exit on test assertion errors - those are handled by mocha
36
+ if (err.name === 'AssertionError' || err.message?.includes('expected')) {
37
+ return
38
+ }
39
+ process.exit(1)
40
+ })
41
+
42
+ process.on('unhandledRejection', (reason, promise) => {
43
+ if (reason && typeof reason === 'object' && reason.stack && global.container?.tsFileMapping && fixErrorStack) {
44
+ const fileMapping = global.container.tsFileMapping()
45
+ if (fileMapping) {
46
+ fixErrorStack(reason, fileMapping)
47
+ }
48
+ }
49
+
50
+ // Log to stderr to bypass stdout suppression
51
+ const msg = reason?.message || String(reason)
52
+ process.stderr.write(`[Worker ${workerIndex}] UNHANDLED REJECTION: ${msg}\n`)
53
+ if (reason?.stack) {
54
+ process.stderr.write(`${reason.stack}\n`)
55
+ }
56
+
57
+ // Don't exit on test-related rejections
58
+ if (msg.includes('expected') || msg.includes('AssertionError')) {
59
+ return
60
+ }
61
+ process.exit(1)
62
+ })
63
+
22
64
  // hide worker output
23
65
  // In pool mode, only suppress output if debug is NOT enabled
24
66
  // In regular mode, hide result output but allow step output in verbose/debug
@@ -26,6 +68,10 @@ if (poolMode && !options.debug) {
26
68
  // In pool mode without debug, allow test names and important output but suppress verbose details
27
69
  const originalWrite = process.stdout.write
28
70
  process.stdout.write = string => {
71
+ // Always allow Worker logs
72
+ if (string.includes('[Worker')) {
73
+ return originalWrite.call(process.stdout, string)
74
+ }
29
75
  // Allow test names (✔ or ✖), Scenario Steps, failures, and important markers
30
76
  if (
31
77
  string.includes('✔') ||
@@ -45,7 +91,12 @@ if (poolMode && !options.debug) {
45
91
  return originalWrite.call(process.stdout, string)
46
92
  }
47
93
  } else if (!poolMode && !options.debug && !options.verbose) {
94
+ const originalWrite = process.stdout.write
48
95
  process.stdout.write = string => {
96
+ // Always allow Worker logs
97
+ if (string.includes('[Worker')) {
98
+ return originalWrite.call(process.stdout, string)
99
+ }
49
100
  stdout += string
50
101
  return true
51
102
  }
@@ -82,30 +133,69 @@ let config
82
133
  // Load test and run
83
134
  initPromise = (async function () {
84
135
  try {
136
+ // Add staggered delay at the very start to prevent resource conflicts
137
+ // Longer delay for browser initialization conflicts
138
+ const delay = (workerIndex - 1) * 2000 // 0ms, 2s, 4s, etc.
139
+ if (delay > 0) {
140
+ await new Promise(resolve => setTimeout(resolve, delay))
141
+ }
142
+
85
143
  // Import modules dynamically to avoid ES Module loader race conditions in Node 22.x
86
144
  const eventModule = await import('../../event.js')
87
145
  const containerModule = await import('../../container.js')
88
146
  const utilsModule = await import('../utils.js')
89
147
  const coreUtilsModule = await import('../../utils.js')
90
148
  const CodeceptModule = await import('../../codecept.js')
91
-
149
+ const typescriptModule = await import('../../utils/typescript.js')
150
+
92
151
  event = eventModule.default
93
152
  container = containerModule.default
94
153
  getConfig = utilsModule.getConfig
95
154
  tryOrDefault = coreUtilsModule.tryOrDefault
96
155
  deepMerge = coreUtilsModule.deepMerge
97
156
  Codecept = CodeceptModule.default
157
+ fixErrorStack = typescriptModule.fixErrorStack
98
158
 
99
159
  const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
100
160
 
101
- // IMPORTANT: await is required here since getConfig is async
102
- const baseConfig = await getConfig(options.config || testRoot)
161
+ let baseConfig
162
+ try {
163
+ // IMPORTANT: await is required here since getConfig is async
164
+ baseConfig = await getConfig(options.config || testRoot)
165
+ } catch (configErr) {
166
+ if (global.container?.tsFileMapping && fixErrorStack) {
167
+ const fileMapping = global.container.tsFileMapping()
168
+ if (fileMapping) {
169
+ fixErrorStack(configErr, fileMapping)
170
+ }
171
+ }
172
+ process.stderr.write(`[Worker ${workerIndex}] FAILED loading config: ${configErr.message}\n`)
173
+ process.stderr.write(`${configErr.stack}\n`)
174
+ await new Promise(resolve => setTimeout(resolve, 100))
175
+ process.exit(1)
176
+ }
103
177
 
104
178
  // important deep merge so dynamic things e.g. functions on config are not overridden
105
179
  config = deepMerge(baseConfig, overrideConfigs)
106
180
 
107
- codecept = new Codecept(config, options)
108
- await codecept.init(testRoot)
181
+ // Pass workerIndex as child option for output.process() to display worker prefix
182
+ const optsWithChild = { ...options, child: workerIndex }
183
+ codecept = new Codecept(config, optsWithChild)
184
+
185
+ try {
186
+ await codecept.init(testRoot)
187
+ } catch (initErr) {
188
+ if (global.container?.tsFileMapping && fixErrorStack) {
189
+ const fileMapping = global.container.tsFileMapping()
190
+ if (fileMapping) {
191
+ fixErrorStack(initErr, fileMapping)
192
+ }
193
+ }
194
+ process.stderr.write(`[Worker ${workerIndex}] FAILED during codecept.init(): ${initErr.message}\n`)
195
+ process.stderr.write(`${initErr.stack}\n`)
196
+ process.exit(1)
197
+ }
198
+
109
199
  codecept.loadTests()
110
200
  mocha = container.mocha()
111
201
 
@@ -124,10 +214,18 @@ initPromise = (async function () {
124
214
  await runTests()
125
215
  } else {
126
216
  // No tests to run, close the worker
217
+ console.error(`[Worker ${workerIndex}] ERROR: No tests found after filtering! Assigned ${tests.length} UIDs but none matched.`)
127
218
  parentPort?.close()
128
219
  }
129
220
  } catch (err) {
130
- console.error('Error in worker initialization:', err)
221
+ if (global.container?.tsFileMapping && fixErrorStack) {
222
+ const fileMapping = global.container.tsFileMapping()
223
+ if (fileMapping) {
224
+ fixErrorStack(err, fileMapping)
225
+ }
226
+ }
227
+ process.stderr.write(`[Worker ${workerIndex}] FATAL ERROR: ${err.message}\n`)
228
+ process.stderr.write(`${err.stack}\n`)
131
229
  process.exit(1)
132
230
  }
133
231
  })()
@@ -145,8 +243,14 @@ async function runTests() {
145
243
  disablePause()
146
244
  try {
147
245
  await codecept.run()
246
+ } catch (err) {
247
+ throw err
148
248
  } finally {
149
- await codecept.teardown()
249
+ try {
250
+ await codecept.teardown()
251
+ } catch (err) {
252
+ // Ignore teardown errors
253
+ }
150
254
  }
151
255
  }
152
256
 
@@ -334,8 +438,16 @@ function filterTests() {
334
438
  mocha.files = files
335
439
  mocha.loadFiles()
336
440
 
337
- for (const suite of mocha.suite.suites) {
441
+ // Recursively filter tests in all suites (including nested ones)
442
+ const filterSuiteTests = (suite) => {
338
443
  suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0)
444
+ for (const childSuite of suite.suites) {
445
+ filterSuiteTests(childSuite)
446
+ }
447
+ }
448
+
449
+ for (const suite of mocha.suite.suites) {
450
+ filterSuiteTests(suite)
339
451
  }
340
452
  }
341
453
 
package/lib/config.js CHANGED
@@ -2,6 +2,7 @@ import fs from 'fs'
2
2
  import path from 'path'
3
3
  import { createRequire } from 'module'
4
4
  import { fileExists, isFile, deepMerge, deepClone } from './utils.js'
5
+ import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'
5
6
 
6
7
  const defaultConfig = {
7
8
  output: './_output',
@@ -155,42 +156,32 @@ async function loadConfigFile(configFile) {
155
156
  try {
156
157
  // For .ts files, try to compile and load as JavaScript
157
158
  if (extensionName === '.ts') {
159
+ let transpileError = null
160
+ let tempFile = null
161
+ let allTempFiles = null
162
+ let fileMapping = null
163
+
158
164
  try {
159
- // Try to load ts-node and compile the file
160
- const { transpile } = require('typescript')
161
- const tsContent = fs.readFileSync(configFile, 'utf8')
162
-
163
- // Transpile TypeScript to JavaScript with ES module output
164
- const jsContent = transpile(tsContent, {
165
- module: 99, // ModuleKind.ESNext
166
- target: 99, // ScriptTarget.ESNext
167
- esModuleInterop: true,
168
- allowSyntheticDefaultImports: true,
169
- })
170
-
171
- // Create a temporary JS file with .mjs extension to force ES module treatment
172
- const tempJsFile = configFile.replace('.ts', '.temp.mjs')
173
- fs.writeFileSync(tempJsFile, jsContent)
174
-
175
- try {
176
- configModule = await import(tempJsFile)
177
- // Clean up temp file
178
- fs.unlinkSync(tempJsFile)
179
- } catch (err) {
180
- // Clean up temp file even on error
181
- if (fs.existsSync(tempJsFile)) {
182
- fs.unlinkSync(tempJsFile)
183
- }
184
- throw err
165
+ // Use the TypeScript transpilation utility
166
+ const typescript = require('typescript')
167
+ const result = await transpileTypeScript(configFile, typescript)
168
+ tempFile = result.tempFile
169
+ allTempFiles = result.allTempFiles
170
+ fileMapping = result.fileMapping
171
+
172
+ configModule = await import(tempFile)
173
+ cleanupTempFiles(allTempFiles)
174
+ } catch (err) {
175
+ transpileError = err
176
+ if (fileMapping) {
177
+ fixErrorStack(err, fileMapping)
185
178
  }
186
- } catch (tsError) {
187
- // If TypeScript compilation fails, fallback to ts-node
188
- try {
189
- require('ts-node/register')
190
- configModule = require(configFile)
191
- } catch (tsNodeError) {
192
- throw new Error(`Failed to load TypeScript config: ${tsError.message}`)
179
+ if (allTempFiles) {
180
+ cleanupTempFiles(allTempFiles)
193
181
  }
182
+ // Throw immediately with the actual error - don't fall back to ts-node
183
+ // as it will mask the real error with "Unexpected token 'export'"
184
+ throw err
194
185
  }
195
186
  } else {
196
187
  // Try ESM import first for JS files