codeceptjs 4.0.0-beta.20 → 4.0.0-beta.22

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.
package/lib/actor.js CHANGED
@@ -7,6 +7,8 @@ import event from './event.js'
7
7
  import store from './store.js'
8
8
  import output from './output.js'
9
9
  import Container from './container.js'
10
+ import debugModule from 'debug'
11
+ const debug = debugModule('codeceptjs:actor')
10
12
 
11
13
  /**
12
14
  * @interface
package/lib/container.js CHANGED
@@ -17,6 +17,51 @@ import ai from './ai.js'
17
17
  import actorFactory from './actor.js'
18
18
 
19
19
  let asyncHelperPromise
20
+ let tsxLoaderRegistered = false
21
+
22
+ /**
23
+ * Automatically register tsx ESM loader for TypeScript imports
24
+ * This allows loading .ts files without NODE_OPTIONS
25
+ */
26
+ async function ensureTsxLoader() {
27
+ if (tsxLoaderRegistered) return true
28
+
29
+ try {
30
+ // Check if tsx is available
31
+ const { createRequire } = await import('module')
32
+ const { pathToFileURL } = await import('url')
33
+ const userRequire = createRequire(pathToFileURL(path.join(global.codecept_dir || process.cwd(), 'package.json')).href)
34
+
35
+ // Try to resolve tsx from user's project
36
+ try {
37
+ userRequire.resolve('tsx')
38
+ } catch {
39
+ debug('tsx not found in user project')
40
+ return false
41
+ }
42
+
43
+ // Register tsx/esm loader dynamically
44
+ // Use Node.js register() API from node:module (Node 18.19+, 20.6+)
45
+ try {
46
+ const { register } = await import('node:module')
47
+ if (typeof register === 'function') {
48
+ debug('Registering tsx ESM loader via node:module register()')
49
+ const tsxPath = userRequire.resolve('tsx/esm')
50
+ register(tsxPath, import.meta.url)
51
+ tsxLoaderRegistered = true
52
+ return true
53
+ }
54
+ } catch (registerErr) {
55
+ debug('node:module register() not available:', registerErr.message)
56
+ }
57
+
58
+ debug('module.register not available, tsx loader must be set via NODE_OPTIONS')
59
+ return false
60
+ } catch (err) {
61
+ debug('Failed to register tsx loader:', err.message)
62
+ return false
63
+ }
64
+ }
20
65
 
21
66
  let container = {
22
67
  helpers: {},
@@ -32,7 +77,7 @@ let container = {
32
77
  translation: {},
33
78
  /** @type {Result | null} */
34
79
  result: null,
35
- sharedKeys: new Set() // Track keys shared via share() function
80
+ sharedKeys: new Set(), // Track keys shared via share() function
36
81
  }
37
82
 
38
83
  /**
@@ -250,10 +295,10 @@ class Container {
250
295
  // Instead of using append which replaces the entire container,
251
296
  // directly update the support object to maintain proxy references
252
297
  Object.assign(container.support, data)
253
-
298
+
254
299
  // Track which keys were explicitly shared
255
300
  Object.keys(data).forEach(key => container.sharedKeys.add(key))
256
-
301
+
257
302
  if (!options.local) {
258
303
  WorkerStorage.share(data)
259
304
  }
@@ -288,39 +333,30 @@ async function createHelpers(config) {
288
333
  helperName = HelperClass.constructor.name
289
334
  }
290
335
 
291
- // classical require - may be async for ESM modules
292
- if (!HelperClass) {
293
- const helperResult = requireHelperFromModule(helperName, config)
294
- if (helperResult instanceof Promise) {
295
- // Handle async ESM loading
296
- helpers[helperName] = {}
297
- asyncHelperPromise = asyncHelperPromise
298
- .then(() => helperResult)
299
- .then(async ResolvedHelperClass => {
300
- debug(`helper ${helperName} resolved type: ${typeof ResolvedHelperClass}`, ResolvedHelperClass)
301
-
302
- // Extract default export from ESM module wrapper if needed
303
- if (ResolvedHelperClass && ResolvedHelperClass.__esModule && ResolvedHelperClass.default) {
304
- ResolvedHelperClass = ResolvedHelperClass.default
305
- debug(`extracted default export for ${helperName}, new type: ${typeof ResolvedHelperClass}`)
306
- }
307
-
308
- if (typeof ResolvedHelperClass !== 'function') {
309
- throw new Error(`Helper '${helperName}' is not a class. Got: ${typeof ResolvedHelperClass}`)
310
- }
311
-
312
- checkHelperRequirements(ResolvedHelperClass)
313
- helpers[helperName] = new ResolvedHelperClass(config[helperName])
314
- if (helpers[helperName]._init) await helpers[helperName]._init()
315
- debug(`helper ${helperName} async initialized`)
316
- })
336
+ // Check for inline helper object (plain object with callable methods, no require field)
337
+ // Inline helpers have methods defined directly on them, unlike config objects
338
+ if (!HelperClass && !config[helperName].require && typeof config[helperName] === 'object') {
339
+ // Check if this object has any callable methods (indicates it's an inline helper)
340
+ const hasMethods = Object.values(config[helperName]).some(val => typeof val === 'function')
341
+ if (hasMethods) {
342
+ // This is an inline helper object, use it directly
343
+ helpers[helperName] = config[helperName]
344
+ debug(`helper ${helperName} loaded as inline object`)
317
345
  continue
318
- } else {
319
- HelperClass = helperResult
320
346
  }
321
347
  }
322
348
 
323
- // handle async CJS modules that use dynamic import
349
+ // classical require - may be async for ESM modules
350
+ if (!HelperClass) {
351
+ // requireHelperFromModule is async, so we need to await it
352
+ HelperClass = await requireHelperFromModule(helperName, config)
353
+
354
+ // Extract default export from ESM module wrapper if needed
355
+ if (HelperClass && HelperClass.__esModule && HelperClass.default) {
356
+ HelperClass = HelperClass.default
357
+ debug(`extracted default export for ${helperName}`)
358
+ }
359
+ } // handle async CJS modules that use dynamic import
324
360
  if (isAsyncFunction(HelperClass)) {
325
361
  helpers[helperName] = {}
326
362
 
@@ -349,6 +385,9 @@ async function createHelpers(config) {
349
385
  }
350
386
  }
351
387
 
388
+ // Wait for all async helpers to be resolved before calling _init
389
+ await asyncHelperPromise
390
+
352
391
  for (const name in helpers) {
353
392
  if (helpers[name]._init) await helpers[name]._init()
354
393
  }
@@ -392,7 +431,85 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
392
431
  }
393
432
  HelperClass = mod.default || mod
394
433
  } catch (err) {
395
- if (err.code === 'ERR_REQUIRE_ESM' || (err.message && err.message.includes('ES module'))) {
434
+ if (err.code === 'ERR_UNKNOWN_FILE_EXTENSION' || (err.message && err.message.includes('Unknown file extension'))) {
435
+ // This is likely a TypeScript helper file. Transpile it to a temporary JS file
436
+ // and import that as a reliable fallback (no NODE_OPTIONS required).
437
+ try {
438
+ const pathModule = await import('path')
439
+ const absolutePath = pathModule.default.resolve(moduleName)
440
+
441
+ // Attempt to load local 'typescript' to transpile the helper
442
+ let typescript
443
+ try {
444
+ typescript = await import('typescript')
445
+ } catch (tsImportErr) {
446
+ throw new Error(`TypeScript helper detected (${moduleName}). Please install 'typescript' in your project: npm install --save-dev typescript`)
447
+ }
448
+
449
+ const { tempFile, allTempFiles } = await transpileTypeScript(absolutePath, typescript)
450
+ debug(`Transpiled TypeScript helper: ${moduleName} -> ${tempFile}`)
451
+
452
+ try {
453
+ try {
454
+ const mod = await import(tempFile)
455
+ HelperClass = mod.default || mod
456
+ debug(`helper ${helperName} loaded from transpiled JS: ${tempFile}`)
457
+ } catch (importTempErr) {
458
+ // If import fails due to CommonJS named export incompatibility,
459
+ // try a quick transform: convert named imports from CommonJS packages
460
+ // into default import + destructuring. This resolves cases like
461
+ // "Named export 'Helper' not found. The requested module '@codeceptjs/helper' is a CommonJS module"
462
+ const msg = (importTempErr && importTempErr.message) || ''
463
+ const commonJsMatch = msg.match(/The requested module '(.+?)' is a CommonJS module/)
464
+ if (commonJsMatch) {
465
+ // Read the transpiled file, perform heuristic replacement, and import again
466
+ const fs = await import('fs')
467
+ let content = fs.readFileSync(tempFile, 'utf8')
468
+ // Heuristic: replace "import { X, Y } from 'mod'" with default import + destructure
469
+ content = content.replace(/import\s+\{([^}]+)\}\s+from\s+(['"])([^'"\)]+)\2/gm, (m, names, q, modName) => {
470
+ // Only adjust imports for the module reported in the error or for local modules
471
+ if (modName === commonJsMatch[1] || modName.startsWith('.') || !modName.includes('/')) {
472
+ const cleanedNames = names.trim()
473
+ return `import pkg__interop from ${q}${modName}${q};\nconst { ${cleanedNames} } = pkg__interop`
474
+ }
475
+ return m
476
+ })
477
+
478
+ // Write to a secondary temp file
479
+ const os = await import('os')
480
+ const path = await import('path')
481
+ const fallbackTemp = path.default.join(os.default.tmpdir(), `helper-fallback-${Date.now()}.mjs`)
482
+ fs.writeFileSync(fallbackTemp, content, 'utf8')
483
+ try {
484
+ const mod = await import(fallbackTemp)
485
+ HelperClass = mod.default || mod
486
+ debug(`helper ${helperName} loaded from transpiled JS after CommonJS interop fix: ${fallbackTemp}`)
487
+ } finally {
488
+ try {
489
+ fs.unlinkSync(fallbackTemp)
490
+ } catch (e) {}
491
+ }
492
+ } else {
493
+ throw importTempErr
494
+ }
495
+ }
496
+ } finally {
497
+ // Cleanup transpiled temporary files
498
+ const filesToClean = Array.isArray(allTempFiles) ? allTempFiles : [allTempFiles]
499
+ cleanupTempFiles(filesToClean)
500
+ }
501
+ } catch (importErr) {
502
+ throw new Error(
503
+ `Helper '${helperName}' is a TypeScript file but could not be loaded.\n` +
504
+ `Path: ${moduleName}\n` +
505
+ `Error: ${importErr.message}\n\n` +
506
+ `To load TypeScript helpers, install 'typescript' in your project or use an ESM loader (e.g. tsx):\n` +
507
+ ` npm install --save-dev typescript\n` +
508
+ ` OR run CodeceptJS with an ESM loader: NODE_OPTIONS='--import tsx' npx codeceptjs run\n\n` +
509
+ `CodeceptJS will transpile TypeScript helpers automatically at runtime if 'typescript' is available.`,
510
+ )
511
+ }
512
+ } else if (err.code === 'ERR_REQUIRE_ESM' || (err.message && err.message.includes('ES module'))) {
396
513
  // This is an ESM module, use dynamic import
397
514
  try {
398
515
  const pathModule = await import('path')
@@ -677,24 +794,23 @@ async function loadSupportObject(modulePath, supportObjectName) {
677
794
  // Use dynamic import for both ESM and CJS modules
678
795
  let importPath = modulePath
679
796
  let tempJsFile = null
680
-
797
+
681
798
  if (typeof importPath === 'string') {
682
799
  const ext = path.extname(importPath)
683
-
800
+
684
801
  // Handle TypeScript files
685
802
  if (ext === '.ts') {
686
803
  try {
687
804
  // Use the TypeScript transpilation utility
688
805
  const typescript = await import('typescript')
689
806
  const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
690
-
807
+
691
808
  debug(`Transpiled TypeScript file: ${importPath} -> ${tempFile}`)
692
-
809
+
693
810
  // Attach cleanup handler
694
811
  importPath = tempFile
695
812
  // Store temp files list in a way that cleanup can access them
696
813
  tempJsFile = allTempFiles
697
-
698
814
  } catch (tsError) {
699
815
  throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
700
816
  }
@@ -703,7 +819,7 @@ async function loadSupportObject(modulePath, supportObjectName) {
703
819
  importPath = `${importPath}.js`
704
820
  }
705
821
  }
706
-
822
+
707
823
  let obj
708
824
  try {
709
825
  obj = await import(importPath)
package/lib/parser.js CHANGED
@@ -17,7 +17,18 @@ export const getParamsToString = function (fn) {
17
17
  function getParams(fn) {
18
18
  if (fn.isSinonProxy) return []
19
19
  try {
20
- const reflected = parser.parse(fn)
20
+ // Convert arrow functions to regular functions for parsing
21
+ let fnString = fn.toString()
22
+ // Handle async arrow functions: async (...) => expr becomes async function(...) { return expr }
23
+ // Handle async arrow functions: async (...) => { ... } becomes async function(...) { ... }
24
+ fnString = fnString.replace(/^async\s*(\([^)]*\))\s*=>\s*\{/, 'async function$1 {')
25
+ fnString = fnString.replace(/^async\s*(\([^)]*\))\s*=>\s*(.+)$/, 'async function$1 { return $2 }')
26
+ // Handle regular arrow functions: (...) => expr becomes function(...) { return expr }
27
+ // Handle regular arrow functions: (...) => { ... } becomes function(...) { ... }
28
+ fnString = fnString.replace(/^(\([^)]*\))\s*=>\s*\{/, 'function$1 {')
29
+ fnString = fnString.replace(/^(\([^)]*\))\s*=>\s*(.+)$/, 'function$1 { return $2 }')
30
+
31
+ const reflected = parser.parse(fnString)
21
32
  if (reflected.args.length > 1 || reflected.args[0] === 'I') {
22
33
  output.error('Error: old CodeceptJS v2 format detected. Upgrade your project to the new format -> https://bit.ly/codecept3Up')
23
34
  }
@@ -4,7 +4,7 @@ import path from 'path'
4
4
  /**
5
5
  * Transpile TypeScript files to ES modules with CommonJS shim support
6
6
  * Handles recursive transpilation of imported TypeScript files
7
- *
7
+ *
8
8
  * @param {string} mainFilePath - Path to the main TypeScript file to transpile
9
9
  * @param {object} typescript - TypeScript compiler instance
10
10
  * @returns {Promise<{tempFile: string, allTempFiles: string[]}>} - Main temp file and all temp files created
@@ -16,9 +16,9 @@ export async function transpileTypeScript(mainFilePath, typescript) {
16
16
  * Transpile a single TypeScript file to JavaScript
17
17
  * Injects CommonJS shims (require, module, exports, __dirname, __filename) as needed
18
18
  */
19
- const transpileTS = (filePath) => {
19
+ const transpileTS = filePath => {
20
20
  const tsContent = fs.readFileSync(filePath, 'utf8')
21
-
21
+
22
22
  // Transpile TypeScript to JavaScript with ES module output
23
23
  let jsContent = transpile(tsContent, {
24
24
  module: 99, // ModuleKind.ESNext
@@ -29,16 +29,16 @@ export async function transpileTypeScript(mainFilePath, typescript) {
29
29
  suppressOutputPathCheck: true,
30
30
  skipLibCheck: true,
31
31
  })
32
-
32
+
33
33
  // Check if the code uses CommonJS globals
34
34
  const usesCommonJSGlobals = /__dirname|__filename/.test(jsContent)
35
35
  const usesRequire = /\brequire\s*\(/.test(jsContent)
36
36
  const usesModuleExports = /\b(module\.exports|exports\.)/.test(jsContent)
37
-
37
+
38
38
  if (usesCommonJSGlobals || usesRequire || usesModuleExports) {
39
39
  // Inject ESM equivalents at the top of the file
40
40
  let esmGlobals = ''
41
-
41
+
42
42
  if (usesRequire || usesModuleExports) {
43
43
  // IMPORTANT: Use the original .ts file path as the base for require()
44
44
  // This ensures dynamic require() calls work with relative paths from the original file location
@@ -81,7 +81,7 @@ const exports = module.exports;
81
81
 
82
82
  `
83
83
  }
84
-
84
+
85
85
  if (usesCommonJSGlobals) {
86
86
  // For __dirname and __filename, also use the original file path
87
87
  const originalFileUrl = `file://${filePath.replace(/\\/g, '/')}`
@@ -92,48 +92,49 @@ const __dirname = __dirname_fn(__filename);
92
92
 
93
93
  `
94
94
  }
95
-
95
+
96
96
  jsContent = esmGlobals + jsContent
97
-
97
+
98
98
  // If module.exports is used, we need to export it as default
99
99
  if (usesModuleExports) {
100
100
  jsContent += `\nexport default module.exports;\n`
101
101
  }
102
102
  }
103
-
103
+
104
104
  return jsContent
105
105
  }
106
-
106
+
107
107
  // Create a map to track transpiled files
108
108
  const transpiledFiles = new Map()
109
109
  const baseDir = path.dirname(mainFilePath)
110
-
110
+
111
111
  // Recursive function to transpile a file and all its TypeScript dependencies
112
- const transpileFileAndDeps = (filePath) => {
112
+ const transpileFileAndDeps = filePath => {
113
113
  // Already transpiled, skip
114
114
  if (transpiledFiles.has(filePath)) {
115
115
  return
116
116
  }
117
-
117
+
118
118
  // Transpile this file
119
119
  let jsContent = transpileTS(filePath)
120
-
120
+
121
121
  // Find all relative TypeScript imports in this file
122
- const importRegex = /from\s+['"](\..+?)(?:\.ts)?['"]/g
122
+ // Match imports that start with ./ or ../
123
+ const importRegex = /from\s+['"](\.\.?\/[^'"]+?)(?:\.ts)?['"]/g
123
124
  let match
124
125
  const imports = []
125
-
126
+
126
127
  while ((match = importRegex.exec(jsContent)) !== null) {
127
128
  imports.push(match[1])
128
129
  }
129
-
130
+
130
131
  // Get the base directory for this file
131
132
  const fileBaseDir = path.dirname(filePath)
132
-
133
+
133
134
  // Recursively transpile each imported TypeScript file
134
135
  for (const relativeImport of imports) {
135
136
  let importedPath = path.resolve(fileBaseDir, relativeImport)
136
-
137
+
137
138
  // Handle .js extensions that might actually be .ts files
138
139
  if (importedPath.endsWith('.js')) {
139
140
  const tsVersion = importedPath.replace(/\.js$/, '.ts')
@@ -141,7 +142,7 @@ const __dirname = __dirname_fn(__filename);
141
142
  importedPath = tsVersion
142
143
  }
143
144
  }
144
-
145
+
145
146
  // Try adding .ts extension if file doesn't exist and no extension provided
146
147
  if (!path.extname(importedPath)) {
147
148
  const tsPath = importedPath + '.ts'
@@ -155,68 +156,76 @@ const __dirname = __dirname_fn(__filename);
155
156
  continue
156
157
  }
157
158
  }
159
+ } else if (importedPath.match(/\.[^./\\]+$/)) {
160
+ // Has an extension that's not .ts - check if .ts version exists by appending .ts
161
+ const tsPath = importedPath + '.ts'
162
+ if (fs.existsSync(tsPath)) {
163
+ importedPath = tsPath
164
+ }
158
165
  }
159
-
166
+
160
167
  // If it's a TypeScript file, recursively transpile it and its dependencies
161
168
  if (importedPath.endsWith('.ts') && fs.existsSync(importedPath)) {
162
169
  transpileFileAndDeps(importedPath)
163
170
  }
164
171
  }
165
-
172
+
166
173
  // After all dependencies are transpiled, rewrite imports in this file
167
- jsContent = jsContent.replace(
168
- /from\s+['"](\..+?)(?:\.ts)?['"]/g,
169
- (match, importPath) => {
170
- let resolvedPath = path.resolve(fileBaseDir, importPath)
171
-
172
- // Handle .js extension that might be .ts
173
- if (resolvedPath.endsWith('.js')) {
174
- const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
175
- if (transpiledFiles.has(tsVersion)) {
176
- const tempFile = transpiledFiles.get(tsVersion)
177
- const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
178
- // Ensure the path starts with ./
179
- if (!relPath.startsWith('.')) {
180
- return `from './${relPath}'`
181
- }
182
- return `from '${relPath}'`
183
- }
184
- }
185
-
186
- // Try with .ts extension
187
- const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'
188
-
189
- // If we transpiled this file, use the temp file
190
- if (transpiledFiles.has(tsPath)) {
191
- const tempFile = transpiledFiles.get(tsPath)
192
- const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
174
+ // IMPORTANT: We need to calculate temp file location first so we can compute correct relative paths
175
+ const tempFile = filePath.replace(/\.ts$/, '.temp.mjs')
176
+ const tempFileDir = path.dirname(tempFile)
177
+
178
+ jsContent = jsContent.replace(/from\s+['"](\.\.?\/[^'"]+?)(?:\.ts)?['"]/g, (match, importPath) => {
179
+ let resolvedPath = path.resolve(fileBaseDir, importPath)
180
+
181
+ // Handle .js extension that might be .ts
182
+ if (resolvedPath.endsWith('.js')) {
183
+ const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
184
+ if (transpiledFiles.has(tsVersion)) {
185
+ const importedTempFile = transpiledFiles.get(tsVersion)
186
+ // Calculate relative path from THIS temp file to the imported temp file
187
+ const relPath = path.relative(tempFileDir, importedTempFile).replace(/\\/g, '/')
193
188
  // Ensure the path starts with ./
194
189
  if (!relPath.startsWith('.')) {
195
190
  return `from './${relPath}'`
196
191
  }
197
192
  return `from '${relPath}'`
198
193
  }
199
-
200
- // Otherwise, keep the import as-is
201
- return match
202
194
  }
203
- )
204
-
195
+
196
+ // Try with .ts extension
197
+ const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'
198
+
199
+ // If we transpiled this file, use the temp file
200
+ if (transpiledFiles.has(tsPath)) {
201
+ const importedTempFile = transpiledFiles.get(tsPath)
202
+ // Calculate relative path from THIS temp file to the imported temp file
203
+ const relPath = path.relative(tempFileDir, importedTempFile).replace(/\\/g, '/')
204
+ // Ensure the path starts with ./
205
+ if (!relPath.startsWith('.')) {
206
+ return `from './${relPath}'`
207
+ }
208
+ return `from '${relPath}'`
209
+ }
210
+
211
+ // Otherwise, keep the import as-is (for npm packages)
212
+ return match
213
+ })
214
+
205
215
  // Write the transpiled file with updated imports
206
- const tempFile = filePath.replace(/\.ts$/, '.temp.mjs')
207
216
  fs.writeFileSync(tempFile, jsContent)
208
217
  transpiledFiles.set(filePath, tempFile)
209
218
  }
210
-
219
+
211
220
  // Start recursive transpilation from the main file
212
221
  transpileFileAndDeps(mainFilePath)
213
-
222
+
214
223
  // Get the main transpiled file
215
224
  const tempJsFile = transpiledFiles.get(mainFilePath)
216
-
225
+
217
226
  // Store all temp files for cleanup
218
227
  const allTempFiles = Array.from(transpiledFiles.values())
219
-
228
+
220
229
  return { tempFile: tempJsFile, allTempFiles }
221
230
  }
222
231
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "4.0.0-beta.20",
3
+ "version": "4.0.0-beta.22",
4
4
  "type": "module",
5
5
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
6
6
  "keywords": [