codeceptjs 4.0.0-beta.20 → 4.0.0-beta.21
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 +2 -0
- package/lib/container.js +125 -1
- package/lib/parser.js +8 -1
- package/package.json +1 -1
package/lib/actor.js
CHANGED
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: {},
|
|
@@ -349,6 +394,9 @@ async function createHelpers(config) {
|
|
|
349
394
|
}
|
|
350
395
|
}
|
|
351
396
|
|
|
397
|
+
// Wait for all async helpers to be resolved before calling _init
|
|
398
|
+
await asyncHelperPromise
|
|
399
|
+
|
|
352
400
|
for (const name in helpers) {
|
|
353
401
|
if (helpers[name]._init) await helpers[name]._init()
|
|
354
402
|
}
|
|
@@ -392,7 +440,83 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
|
|
|
392
440
|
}
|
|
393
441
|
HelperClass = mod.default || mod
|
|
394
442
|
} catch (err) {
|
|
395
|
-
if (err.code === '
|
|
443
|
+
if (err.code === 'ERR_UNKNOWN_FILE_EXTENSION' || (err.message && err.message.includes('Unknown file extension'))) {
|
|
444
|
+
// This is likely a TypeScript helper file. Transpile it to a temporary JS file
|
|
445
|
+
// and import that as a reliable fallback (no NODE_OPTIONS required).
|
|
446
|
+
try {
|
|
447
|
+
const pathModule = await import('path')
|
|
448
|
+
const absolutePath = pathModule.default.resolve(moduleName)
|
|
449
|
+
|
|
450
|
+
// Attempt to load local 'typescript' to transpile the helper
|
|
451
|
+
let typescript
|
|
452
|
+
try {
|
|
453
|
+
typescript = await import('typescript')
|
|
454
|
+
} catch (tsImportErr) {
|
|
455
|
+
throw new Error(`TypeScript helper detected (${moduleName}). Please install 'typescript' in your project: npm install --save-dev typescript`)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const { tempFile, allTempFiles } = await transpileTypeScript(absolutePath, typescript)
|
|
459
|
+
debug(`Transpiled TypeScript helper: ${moduleName} -> ${tempFile}`)
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
try {
|
|
463
|
+
const mod = await import(tempFile)
|
|
464
|
+
HelperClass = mod.default || mod
|
|
465
|
+
debug(`helper ${helperName} loaded from transpiled JS: ${tempFile}`)
|
|
466
|
+
} catch (importTempErr) {
|
|
467
|
+
// If import fails due to CommonJS named export incompatibility,
|
|
468
|
+
// try a quick transform: convert named imports from CommonJS packages
|
|
469
|
+
// into default import + destructuring. This resolves cases like
|
|
470
|
+
// "Named export 'Helper' not found. The requested module '@codeceptjs/helper' is a CommonJS module"
|
|
471
|
+
const msg = importTempErr && importTempErr.message || ''
|
|
472
|
+
const commonJsMatch = msg.match(/The requested module '(.+?)' is a CommonJS module/)
|
|
473
|
+
if (commonJsMatch) {
|
|
474
|
+
// Read the transpiled file, perform heuristic replacement, and import again
|
|
475
|
+
const fs = await import('fs')
|
|
476
|
+
let content = fs.readFileSync(tempFile, 'utf8')
|
|
477
|
+
// Heuristic: replace "import { X, Y } from 'mod'" with default import + destructure
|
|
478
|
+
content = content.replace(/import\s+\{([^}]+)\}\s+from\s+(['"])([^'"\)]+)\2/gm, (m, names, q, modName) => {
|
|
479
|
+
// Only adjust imports for the module reported in the error or for local modules
|
|
480
|
+
if (modName === commonJsMatch[1] || modName.startsWith('.') || !modName.includes('/')) {
|
|
481
|
+
const cleanedNames = names.trim()
|
|
482
|
+
return `import pkg__interop from ${q}${modName}${q};\nconst { ${cleanedNames} } = pkg__interop`
|
|
483
|
+
}
|
|
484
|
+
return m
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
// Write to a secondary temp file
|
|
488
|
+
const os = await import('os')
|
|
489
|
+
const path = await import('path')
|
|
490
|
+
const fallbackTemp = path.default.join(os.default.tmpdir(), `helper-fallback-${Date.now()}.mjs`)
|
|
491
|
+
fs.writeFileSync(fallbackTemp, content, 'utf8')
|
|
492
|
+
try {
|
|
493
|
+
const mod = await import(fallbackTemp)
|
|
494
|
+
HelperClass = mod.default || mod
|
|
495
|
+
debug(`helper ${helperName} loaded from transpiled JS after CommonJS interop fix: ${fallbackTemp}`)
|
|
496
|
+
} finally {
|
|
497
|
+
try { fs.unlinkSync(fallbackTemp) } catch (e) {}
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
throw importTempErr
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
} finally {
|
|
504
|
+
// Cleanup transpiled temporary files
|
|
505
|
+
const filesToClean = Array.isArray(allTempFiles) ? allTempFiles : [allTempFiles]
|
|
506
|
+
cleanupTempFiles(filesToClean)
|
|
507
|
+
}
|
|
508
|
+
} catch (importErr) {
|
|
509
|
+
throw new Error(
|
|
510
|
+
`Helper '${helperName}' is a TypeScript file but could not be loaded.\n` +
|
|
511
|
+
`Path: ${moduleName}\n` +
|
|
512
|
+
`Error: ${importErr.message}\n\n` +
|
|
513
|
+
`To load TypeScript helpers, install 'typescript' in your project or use an ESM loader (e.g. tsx):\n` +
|
|
514
|
+
` npm install --save-dev typescript\n` +
|
|
515
|
+
` OR run CodeceptJS with an ESM loader: NODE_OPTIONS='--import tsx' npx codeceptjs run\n\n` +
|
|
516
|
+
`CodeceptJS will transpile TypeScript helpers automatically at runtime if 'typescript' is available.`
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
} else if (err.code === 'ERR_REQUIRE_ESM' || (err.message && err.message.includes('ES module'))) {
|
|
396
520
|
// This is an ESM module, use dynamic import
|
|
397
521
|
try {
|
|
398
522
|
const pathModule = await import('path')
|
package/lib/parser.js
CHANGED
|
@@ -17,7 +17,14 @@ export const getParamsToString = function (fn) {
|
|
|
17
17
|
function getParams(fn) {
|
|
18
18
|
if (fn.isSinonProxy) return []
|
|
19
19
|
try {
|
|
20
|
-
|
|
20
|
+
// Convert arrow functions to regular functions for parsing
|
|
21
|
+
let fnString = fn.toString()
|
|
22
|
+
// Handle async arrow functions: async (...) => { becomes async function(...) {
|
|
23
|
+
fnString = fnString.replace(/^async\s*(\([^)]*\))\s*=>/, 'async function$1')
|
|
24
|
+
// Handle regular arrow functions: (...) => { becomes function(...) {
|
|
25
|
+
fnString = fnString.replace(/^(\([^)]*\))\s*=>/, 'function$1')
|
|
26
|
+
|
|
27
|
+
const reflected = parser.parse(fnString)
|
|
21
28
|
if (reflected.args.length > 1 || reflected.args[0] === 'I') {
|
|
22
29
|
output.error('Error: old CodeceptJS v2 format detected. Upgrade your project to the new format -> https://bit.ly/codecept3Up')
|
|
23
30
|
}
|