codeceptjs 4.0.0-beta.9.esm-aria → 4.0.0-rc.10
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/README.md +39 -27
- package/bin/codecept.js +2 -2
- package/bin/mcp-server.js +610 -0
- package/docs/webapi/appendField.mustache +5 -0
- package/docs/webapi/attachFile.mustache +12 -0
- package/docs/webapi/checkOption.mustache +1 -1
- package/docs/webapi/clearField.mustache +5 -0
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/dontSeeElement.mustache +4 -0
- package/docs/webapi/dontSeeInField.mustache +5 -0
- package/docs/webapi/fillField.mustache +5 -0
- package/docs/webapi/moveCursorTo.mustache +5 -1
- package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/seeElement.mustache +4 -0
- package/docs/webapi/seeInField.mustache +5 -0
- package/docs/webapi/selectOption.mustache +5 -0
- package/docs/webapi/uncheckOption.mustache +1 -1
- package/lib/actor.js +12 -8
- package/lib/codecept.js +51 -18
- package/lib/command/definitions.js +14 -7
- package/lib/command/init.js +2 -4
- package/lib/command/run-workers.js +13 -2
- package/lib/command/workers/runTests.js +121 -9
- package/lib/config.js +24 -33
- package/lib/container.js +177 -28
- package/lib/element/WebElement.js +81 -2
- package/lib/els.js +12 -6
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/GraphQL.js +6 -4
- package/lib/helper/JSONResponse.js +3 -4
- package/lib/helper/Playwright.js +339 -505
- package/lib/helper/Puppeteer.js +324 -89
- package/lib/helper/REST.js +15 -9
- package/lib/helper/WebDriver.js +311 -81
- package/lib/helper/errors/ElementNotFound.js +5 -2
- package/lib/helper/errors/MultipleElementsFound.js +52 -0
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/html.js +14 -1
- package/lib/listener/config.js +11 -3
- package/lib/listener/globalRetry.js +32 -6
- package/lib/listener/helpers.js +2 -14
- package/lib/locator.js +32 -0
- package/lib/mocha/cli.js +16 -0
- package/lib/mocha/factory.js +7 -27
- package/lib/mocha/gherkin.js +4 -4
- package/lib/mocha/test.js +4 -2
- package/lib/output.js +2 -2
- package/lib/plugin/aiTrace.js +464 -0
- package/lib/plugin/auth.js +2 -1
- package/lib/plugin/retryFailedStep.js +28 -19
- package/lib/plugin/stepByStepReport.js +5 -1
- package/lib/step/base.js +14 -1
- package/lib/step/config.js +15 -2
- package/lib/step/meta.js +18 -1
- package/lib/step/record.js +9 -1
- package/lib/utils/loaderCheck.js +162 -0
- package/lib/utils/typescript.js +449 -0
- package/lib/utils.js +48 -0
- package/lib/workers.js +163 -54
- package/package.json +43 -32
- package/typings/index.d.ts +120 -4
- package/lib/helper/extras/PlaywrightLocator.js +0 -110
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/retryCoordinator.js +0 -207
- package/typings/promiseBasedTypes.d.ts +0 -11011
- package/typings/types.d.ts +0 -13073
package/lib/step/meta.js
CHANGED
|
@@ -58,17 +58,24 @@ class MetaStep extends Step {
|
|
|
58
58
|
this.status = 'queued'
|
|
59
59
|
this.setArguments(Array.from(arguments).slice(1))
|
|
60
60
|
let result
|
|
61
|
+
let hasChildSteps = false
|
|
61
62
|
|
|
62
63
|
const registerStep = step => {
|
|
63
64
|
this.setMetaStep(null)
|
|
64
65
|
step.setMetaStep(this)
|
|
66
|
+
hasChildSteps = true
|
|
65
67
|
}
|
|
66
68
|
event.dispatcher.prependListener(event.step.before, registerStep)
|
|
69
|
+
|
|
70
|
+
// Start timing
|
|
71
|
+
this.startTime = Date.now()
|
|
72
|
+
|
|
67
73
|
// Handle async and sync methods.
|
|
68
74
|
if (fn.constructor.name === 'AsyncFunction') {
|
|
69
75
|
result = fn
|
|
70
76
|
.apply(this.context, this.args)
|
|
71
77
|
.then(result => {
|
|
78
|
+
this.setStatus('success')
|
|
72
79
|
return result
|
|
73
80
|
})
|
|
74
81
|
.catch(error => {
|
|
@@ -78,17 +85,27 @@ class MetaStep extends Step {
|
|
|
78
85
|
.finally(() => {
|
|
79
86
|
this.endTime = Date.now()
|
|
80
87
|
event.dispatcher.removeListener(event.step.before, registerStep)
|
|
88
|
+
// Only emit events if no child steps were registered
|
|
89
|
+
if (!hasChildSteps) {
|
|
90
|
+
event.emit(event.step.started, this)
|
|
91
|
+
event.emit(event.step.finished, this)
|
|
92
|
+
}
|
|
81
93
|
})
|
|
82
94
|
} else {
|
|
83
95
|
try {
|
|
84
|
-
this.startTime = Date.now()
|
|
85
96
|
result = fn.apply(this.context, this.args)
|
|
97
|
+
this.setStatus('success')
|
|
86
98
|
} catch (error) {
|
|
87
99
|
this.setStatus('failed')
|
|
88
100
|
throw error
|
|
89
101
|
} finally {
|
|
90
102
|
this.endTime = Date.now()
|
|
91
103
|
event.dispatcher.removeListener(event.step.before, registerStep)
|
|
104
|
+
// Only emit events if no child steps were registered
|
|
105
|
+
if (!hasChildSteps) {
|
|
106
|
+
event.emit(event.step.started, this)
|
|
107
|
+
event.emit(event.step.finished, this)
|
|
108
|
+
}
|
|
92
109
|
}
|
|
93
110
|
}
|
|
94
111
|
|
package/lib/step/record.js
CHANGED
|
@@ -5,12 +5,13 @@ import output from '../output.js'
|
|
|
5
5
|
import store from '../store.js'
|
|
6
6
|
import { TIMEOUT_ORDER } from '../timeout.js'
|
|
7
7
|
import retryStep from './retry.js'
|
|
8
|
+
import { fixErrorStack } from '../utils/typescript.js'
|
|
8
9
|
function recordStep(step, args) {
|
|
9
10
|
step.status = 'queued'
|
|
10
11
|
|
|
11
12
|
// apply step configuration
|
|
12
13
|
const lastArg = args[args.length - 1]
|
|
13
|
-
if (lastArg
|
|
14
|
+
if (StepConfig.isStepConfig(lastArg)) {
|
|
14
15
|
const stepConfig = args.pop()
|
|
15
16
|
const { opts, timeout, retry } = stepConfig.getConfig()
|
|
16
17
|
|
|
@@ -60,6 +61,13 @@ function recordStep(step, args) {
|
|
|
60
61
|
recorder.catch(err => {
|
|
61
62
|
step.status = 'failed'
|
|
62
63
|
step.endTime = +Date.now()
|
|
64
|
+
|
|
65
|
+
// Fix error stack to point to original .ts files (lazy import to avoid circular dependency)
|
|
66
|
+
const fileMapping = global.container?.tsFileMapping?.()
|
|
67
|
+
if (fileMapping) {
|
|
68
|
+
fixErrorStack(err, fileMapping)
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
event.emit(event.step.failed, step, err)
|
|
64
72
|
event.emit(event.step.finished, step)
|
|
65
73
|
throw err
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for checking TypeScript loader availability
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if a TypeScript loader is available for test files
|
|
7
|
+
* Note: This checks if loaders are in the require array, not if packages are installed
|
|
8
|
+
* Package installation is checked when actually requiring modules
|
|
9
|
+
* @param {string[]} requiredModules - Array of required modules from config
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function checkTypeScriptLoader(requiredModules = []) {
|
|
13
|
+
// Check if a loader is configured in the require array
|
|
14
|
+
return (
|
|
15
|
+
requiredModules.includes('tsx/esm') ||
|
|
16
|
+
requiredModules.includes('tsx/cjs') ||
|
|
17
|
+
requiredModules.includes('tsx') ||
|
|
18
|
+
requiredModules.includes('ts-node/esm') ||
|
|
19
|
+
requiredModules.includes('ts-node/register') ||
|
|
20
|
+
requiredModules.includes('ts-node')
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate helpful error message if .ts tests found but no loader configured
|
|
26
|
+
* @param {string[]} testFiles - Array of test file paths
|
|
27
|
+
* @returns {string|null} Error message or null if no TypeScript files
|
|
28
|
+
*/
|
|
29
|
+
export function getTypeScriptLoaderError(testFiles) {
|
|
30
|
+
const tsFiles = testFiles.filter(f => f.endsWith('.ts'))
|
|
31
|
+
|
|
32
|
+
if (tsFiles.length === 0) return null
|
|
33
|
+
|
|
34
|
+
return `
|
|
35
|
+
╔═════════════════════════════════════════════════════════════════════════════╗
|
|
36
|
+
║ ║
|
|
37
|
+
║ ⚠️ TypeScript Test Files Detected but No Loader Configured ║
|
|
38
|
+
║ ║
|
|
39
|
+
╚═════════════════════════════════════════════════════════════════════════════╝
|
|
40
|
+
|
|
41
|
+
Found ${tsFiles.length} TypeScript test file(s) but no TypeScript loader is configured.
|
|
42
|
+
|
|
43
|
+
CodeceptJS 4.x uses ES Modules (ESM) and requires a loader to run TypeScript tests.
|
|
44
|
+
|
|
45
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
46
|
+
│ Option 1: tsx (Recommended - Fast, Zero Config) │
|
|
47
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
48
|
+
|
|
49
|
+
Installation:
|
|
50
|
+
npm install --save-dev tsx
|
|
51
|
+
|
|
52
|
+
Configuration:
|
|
53
|
+
Add to your codecept.conf.ts or codecept.conf.js:
|
|
54
|
+
|
|
55
|
+
export const config = {
|
|
56
|
+
tests: './**/*_test.ts',
|
|
57
|
+
require: ['tsx/cjs'], // ← Add this line
|
|
58
|
+
helpers: { /* ... */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Why tsx?
|
|
62
|
+
⚡ Fast: Built on esbuild
|
|
63
|
+
🎯 Zero config: No tsconfig.json required
|
|
64
|
+
✅ Works with Mocha: Uses CommonJS hooks
|
|
65
|
+
✅ Complete: Handles all TypeScript features
|
|
66
|
+
|
|
67
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
68
|
+
│ Option 2: ts-node/esm (Not Recommended - Has Module Resolution Issues) │
|
|
69
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
70
|
+
|
|
71
|
+
⚠️ ts-node/esm has significant limitations and is not recommended:
|
|
72
|
+
- Doesn't work with "type": "module" in package.json
|
|
73
|
+
- Module resolution doesn't work like standard TypeScript ESM
|
|
74
|
+
- Import statements must use explicit file paths
|
|
75
|
+
|
|
76
|
+
We strongly recommend using tsx/cjs instead.
|
|
77
|
+
|
|
78
|
+
If you still want to use ts-node/esm:
|
|
79
|
+
|
|
80
|
+
Installation:
|
|
81
|
+
npm install --save-dev ts-node
|
|
82
|
+
|
|
83
|
+
Configuration:
|
|
84
|
+
1. Add to your codecept.conf.ts:
|
|
85
|
+
require: ['ts-node/esm']
|
|
86
|
+
|
|
87
|
+
2. Create tsconfig.json:
|
|
88
|
+
{
|
|
89
|
+
"compilerOptions": {
|
|
90
|
+
"module": "ESNext",
|
|
91
|
+
"target": "ES2022",
|
|
92
|
+
"moduleResolution": "node",
|
|
93
|
+
"esModuleInterop": true
|
|
94
|
+
},
|
|
95
|
+
"ts-node": {
|
|
96
|
+
"esm": true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
3. Do NOT use "type": "module" in package.json
|
|
101
|
+
|
|
102
|
+
📚 Documentation: https://codecept.io/typescript
|
|
103
|
+
|
|
104
|
+
Note: TypeScript config files (codecept.conf.ts) and helpers are automatically
|
|
105
|
+
transpiled. Only test files require a loader to be configured.
|
|
106
|
+
`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get warning message if ts-node/esm is being used
|
|
111
|
+
* @param {string[]} requiredModules - Array of required modules from config
|
|
112
|
+
* @returns {string|null} Warning message or null
|
|
113
|
+
*/
|
|
114
|
+
export function getTSNodeESMWarning(requiredModules = []) {
|
|
115
|
+
if (!requiredModules.includes('ts-node/esm')) {
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return `
|
|
120
|
+
⚠️ Warning: ts-node/esm with "module": "esnext" requires explicit file extensions in all imports.
|
|
121
|
+
|
|
122
|
+
This is a known limitation. Use tsx/cjs instead to write imports without extensions.
|
|
123
|
+
|
|
124
|
+
Examples:
|
|
125
|
+
|
|
126
|
+
❌ Incorrect (will fail):
|
|
127
|
+
import loginPage from "./pages/Login";
|
|
128
|
+
|
|
129
|
+
✅ Correct (must include .ts extension):
|
|
130
|
+
import loginPage from "./pages/Login.ts";
|
|
131
|
+
|
|
132
|
+
📚 Documentation: https://codecept.io/typescript
|
|
133
|
+
|
|
134
|
+
`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if user is trying to run TypeScript tests without proper loader
|
|
139
|
+
* @param {string[]} testFiles - Array of test file paths
|
|
140
|
+
* @param {string[]} requiredModules - Array of required modules from config
|
|
141
|
+
* @returns {{hasError: boolean, message: string|null}}
|
|
142
|
+
*/
|
|
143
|
+
export function validateTypeScriptSetup(testFiles, requiredModules = []) {
|
|
144
|
+
const tsFiles = testFiles.filter(f => f.endsWith('.ts'))
|
|
145
|
+
|
|
146
|
+
if (tsFiles.length === 0) {
|
|
147
|
+
// No TypeScript test files, all good
|
|
148
|
+
return { hasError: false, message: null }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check if a loader is configured in the require array
|
|
152
|
+
const hasLoader = checkTypeScriptLoader(requiredModules)
|
|
153
|
+
|
|
154
|
+
if (hasLoader) {
|
|
155
|
+
// Loader configured, all good (package will be checked when requireModules runs)
|
|
156
|
+
return { hasError: false, message: null }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// No loader configured and TypeScript tests exist
|
|
160
|
+
const message = getTypeScriptLoaderError(testFiles)
|
|
161
|
+
return { hasError: true, message }
|
|
162
|
+
}
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { pathToFileURL } from 'url'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load tsconfig.json if it exists
|
|
7
|
+
* @param {string} tsConfigPath - Path to tsconfig.json
|
|
8
|
+
* @returns {object|null} - Parsed tsconfig or null
|
|
9
|
+
*/
|
|
10
|
+
function loadTsConfig(tsConfigPath) {
|
|
11
|
+
if (!fs.existsSync(tsConfigPath)) {
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const tsConfigContent = fs.readFileSync(tsConfigPath, 'utf8')
|
|
17
|
+
return JSON.parse(tsConfigContent)
|
|
18
|
+
} catch (err) {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve TypeScript path alias to actual file path
|
|
25
|
+
* @param {string} importPath - Import path with alias (e.g., '#config/urls')
|
|
26
|
+
* @param {object} tsConfig - Parsed tsconfig.json
|
|
27
|
+
* @param {string} configDir - Directory containing tsconfig.json
|
|
28
|
+
* @returns {string|null} - Resolved file path or null if not an alias
|
|
29
|
+
*/
|
|
30
|
+
function resolveTsPathAlias(importPath, tsConfig, configDir) {
|
|
31
|
+
if (!tsConfig || !tsConfig.compilerOptions || !tsConfig.compilerOptions.paths) {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const paths = tsConfig.compilerOptions.paths
|
|
36
|
+
|
|
37
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
38
|
+
if (!targets || targets.length === 0) {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const patternRegex = new RegExp(
|
|
43
|
+
'^' + pattern.replace(/\*/g, '(.*)') + '$'
|
|
44
|
+
)
|
|
45
|
+
const match = importPath.match(patternRegex)
|
|
46
|
+
|
|
47
|
+
if (match) {
|
|
48
|
+
const wildcard = match[1] || ''
|
|
49
|
+
const target = targets[0]
|
|
50
|
+
const resolvedTarget = target.replace(/\*/g, wildcard)
|
|
51
|
+
|
|
52
|
+
return path.resolve(configDir, resolvedTarget)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Transpile TypeScript files to ES modules with CommonJS shim support
|
|
61
|
+
* Handles recursive transpilation of imported TypeScript files
|
|
62
|
+
*
|
|
63
|
+
* @param {string} mainFilePath - Path to the main TypeScript file to transpile
|
|
64
|
+
* @param {object} typescript - TypeScript compiler instance
|
|
65
|
+
* @returns {Promise<{tempFile: string, allTempFiles: string[], fileMapping: any}>} - Main temp file and all temp files created
|
|
66
|
+
*/
|
|
67
|
+
export async function transpileTypeScript(mainFilePath, typescript) {
|
|
68
|
+
const { transpile } = typescript
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Transpile a single TypeScript file to JavaScript
|
|
72
|
+
* Injects CommonJS shims (require, module, exports, __dirname, __filename) as needed
|
|
73
|
+
*/
|
|
74
|
+
const transpileTS = (filePath) => {
|
|
75
|
+
const tsContent = fs.readFileSync(filePath, 'utf8')
|
|
76
|
+
|
|
77
|
+
// Transpile TypeScript to JavaScript with ES module output
|
|
78
|
+
let jsContent = transpile(tsContent, {
|
|
79
|
+
module: 99, // ModuleKind.ESNext
|
|
80
|
+
target: 99, // ScriptTarget.ESNext
|
|
81
|
+
esModuleInterop: true,
|
|
82
|
+
allowSyntheticDefaultImports: true,
|
|
83
|
+
lib: ['lib.esnext.d.ts'], // Enable latest features including top-level await
|
|
84
|
+
suppressOutputPathCheck: true,
|
|
85
|
+
skipLibCheck: true,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Check if the code uses CommonJS globals
|
|
89
|
+
const usesCommonJSGlobals = /__dirname|__filename/.test(jsContent)
|
|
90
|
+
const usesRequire = /\brequire\s*\(/.test(jsContent)
|
|
91
|
+
const usesModuleExports = /\b(module\.exports|exports\.)/.test(jsContent)
|
|
92
|
+
|
|
93
|
+
if (usesCommonJSGlobals || usesRequire || usesModuleExports) {
|
|
94
|
+
// Inject ESM equivalents at the top of the file
|
|
95
|
+
let esmGlobals = ''
|
|
96
|
+
|
|
97
|
+
if (usesRequire || usesModuleExports) {
|
|
98
|
+
// IMPORTANT: Use the original .ts file path as the base for require()
|
|
99
|
+
// This ensures dynamic require() calls work with relative paths from the original file location
|
|
100
|
+
const originalFileUrl = `file://${filePath.replace(/\\/g, '/')}`
|
|
101
|
+
esmGlobals += `import { createRequire } from 'module';
|
|
102
|
+
import { extname as __extname } from 'path';
|
|
103
|
+
const __baseRequire = createRequire('${originalFileUrl}');
|
|
104
|
+
|
|
105
|
+
// Wrap require to auto-resolve extensions (mimics CommonJS behavior)
|
|
106
|
+
const require = (id) => {
|
|
107
|
+
try {
|
|
108
|
+
return __baseRequire(id);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
// If module not found and it's a relative/absolute path without extension, try common extensions
|
|
111
|
+
if (err.code === 'MODULE_NOT_FOUND' && (id.startsWith('./') || id.startsWith('../') || id.startsWith('/'))) {
|
|
112
|
+
const ext = __extname(id);
|
|
113
|
+
// Only treat known file extensions as real extensions (so names like .TEST don't block probing)
|
|
114
|
+
const __knownExts = ['.js', '.cjs', '.mjs', '.json', '.node'];
|
|
115
|
+
const hasKnownExt = ext && __knownExts.includes(ext.toLowerCase());
|
|
116
|
+
if (!hasKnownExt) {
|
|
117
|
+
// Try common extensions in order: .js, .cjs, .json, .node
|
|
118
|
+
// Note: .ts files cannot be required - they need transpilation first
|
|
119
|
+
const extensions = ['.js', '.cjs', '.json', '.node'];
|
|
120
|
+
for (const testExt of extensions) {
|
|
121
|
+
try {
|
|
122
|
+
return __baseRequire(id + testExt);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// Continue to next extension
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Re-throw original error if all attempts failed
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const module = { exports: {} };
|
|
135
|
+
const exports = module.exports;
|
|
136
|
+
|
|
137
|
+
`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (usesCommonJSGlobals) {
|
|
141
|
+
// For __dirname and __filename, also use the original file path
|
|
142
|
+
const originalFileUrl = `file://${filePath.replace(/\\/g, '/')}`
|
|
143
|
+
esmGlobals += `import { fileURLToPath as __fileURLToPath } from 'url';
|
|
144
|
+
import { dirname as __dirname_fn } from 'path';
|
|
145
|
+
const __filename = '${filePath.replace(/\\/g, '/')}';
|
|
146
|
+
const __dirname = __dirname_fn(__filename);
|
|
147
|
+
|
|
148
|
+
`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
jsContent = esmGlobals + jsContent
|
|
152
|
+
|
|
153
|
+
// If module.exports is used, we need to export it as default
|
|
154
|
+
if (usesModuleExports) {
|
|
155
|
+
jsContent += `\nexport default module.exports;\n`
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return jsContent
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Create a map to track transpiled files
|
|
163
|
+
const transpiledFiles = new Map()
|
|
164
|
+
const baseDir = path.dirname(mainFilePath)
|
|
165
|
+
|
|
166
|
+
// Try to find tsconfig.json by walking up the directory tree
|
|
167
|
+
let tsConfigPath = path.join(baseDir, 'tsconfig.json')
|
|
168
|
+
let configDir = baseDir
|
|
169
|
+
let searchDir = baseDir
|
|
170
|
+
|
|
171
|
+
while (!fs.existsSync(tsConfigPath) && searchDir !== path.dirname(searchDir)) {
|
|
172
|
+
searchDir = path.dirname(searchDir)
|
|
173
|
+
tsConfigPath = path.join(searchDir, 'tsconfig.json')
|
|
174
|
+
if (fs.existsSync(tsConfigPath)) {
|
|
175
|
+
configDir = searchDir
|
|
176
|
+
break
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const tsConfig = loadTsConfig(tsConfigPath)
|
|
181
|
+
|
|
182
|
+
// Recursive function to transpile a file and all its TypeScript dependencies
|
|
183
|
+
const transpileFileAndDeps = (filePath) => {
|
|
184
|
+
// Already transpiled, skip
|
|
185
|
+
if (transpiledFiles.has(filePath)) {
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Transpile this file
|
|
190
|
+
let jsContent = transpileTS(filePath)
|
|
191
|
+
|
|
192
|
+
// Find all TypeScript imports in this file (both ESM imports and require() calls)
|
|
193
|
+
const importRegex = /from\s+['"]([^'"]+?)['"]/g
|
|
194
|
+
const requireRegex = /require\s*\(\s*['"]([^'"]+?)['"]\s*\)/g
|
|
195
|
+
let match
|
|
196
|
+
const imports = []
|
|
197
|
+
|
|
198
|
+
while ((match = importRegex.exec(jsContent)) !== null) {
|
|
199
|
+
imports.push({ path: match[1], type: 'import' })
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
while ((match = requireRegex.exec(jsContent)) !== null) {
|
|
203
|
+
imports.push({ path: match[1], type: 'require' })
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Get the base directory for this file
|
|
207
|
+
const fileBaseDir = path.dirname(filePath)
|
|
208
|
+
|
|
209
|
+
// Recursively transpile each imported TypeScript file
|
|
210
|
+
for (const { path: importPath } of imports) {
|
|
211
|
+
let importedPath = importPath
|
|
212
|
+
|
|
213
|
+
// Check if this is a path alias
|
|
214
|
+
const resolvedAlias = resolveTsPathAlias(importPath, tsConfig, configDir)
|
|
215
|
+
if (resolvedAlias) {
|
|
216
|
+
importedPath = resolvedAlias
|
|
217
|
+
} else if (importPath.startsWith('.')) {
|
|
218
|
+
importedPath = path.resolve(fileBaseDir, importPath)
|
|
219
|
+
} else {
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Handle .js extensions that might actually be .ts files
|
|
224
|
+
if (importedPath.endsWith('.js')) {
|
|
225
|
+
const tsVersion = importedPath.replace(/\.js$/, '.ts')
|
|
226
|
+
if (fs.existsSync(tsVersion)) {
|
|
227
|
+
importedPath = tsVersion
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check for standard module extensions to determine if we should try adding .ts
|
|
232
|
+
const ext = path.extname(importedPath)
|
|
233
|
+
const standardExtensions = ['.js', '.mjs', '.cjs', '.json', '.node']
|
|
234
|
+
const hasStandardExtension = standardExtensions.includes(ext.toLowerCase())
|
|
235
|
+
|
|
236
|
+
// If it doesn't end with .ts and doesn't have a standard extension, try adding .ts
|
|
237
|
+
if (!importedPath.endsWith('.ts') && !hasStandardExtension) {
|
|
238
|
+
const tsPath = importedPath + '.ts'
|
|
239
|
+
if (fs.existsSync(tsPath)) {
|
|
240
|
+
importedPath = tsPath
|
|
241
|
+
} else {
|
|
242
|
+
// Try index.ts for directory imports
|
|
243
|
+
const indexTsPath = path.join(importedPath, 'index.ts')
|
|
244
|
+
if (fs.existsSync(indexTsPath)) {
|
|
245
|
+
importedPath = indexTsPath
|
|
246
|
+
} else {
|
|
247
|
+
// Try .js extension as well
|
|
248
|
+
const jsPath = importedPath + '.js'
|
|
249
|
+
if (fs.existsSync(jsPath)) {
|
|
250
|
+
// Skip .js files, they don't need transpilation
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// If it's a TypeScript file, recursively transpile it and its dependencies
|
|
258
|
+
if (importedPath.endsWith('.ts') && fs.existsSync(importedPath)) {
|
|
259
|
+
transpileFileAndDeps(importedPath)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// After all dependencies are transpiled, rewrite imports in this file
|
|
264
|
+
jsContent = jsContent.replace(
|
|
265
|
+
/from\s+['"]([^'"]+?)['"]/g,
|
|
266
|
+
(match, importPath) => {
|
|
267
|
+
let resolvedPath = importPath
|
|
268
|
+
const originalExt = path.extname(importPath)
|
|
269
|
+
|
|
270
|
+
// Check if this is a path alias
|
|
271
|
+
const resolvedAlias = resolveTsPathAlias(importPath, tsConfig, configDir)
|
|
272
|
+
if (resolvedAlias) {
|
|
273
|
+
resolvedPath = resolvedAlias
|
|
274
|
+
} else if (importPath.startsWith('.')) {
|
|
275
|
+
resolvedPath = path.resolve(fileBaseDir, importPath)
|
|
276
|
+
} else {
|
|
277
|
+
return match
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// If resolved path is a directory, try index.ts
|
|
281
|
+
if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) {
|
|
282
|
+
const indexPath = path.join(resolvedPath, 'index.ts')
|
|
283
|
+
if (fs.existsSync(indexPath) && transpiledFiles.has(indexPath)) {
|
|
284
|
+
const tempFile = transpiledFiles.get(indexPath)
|
|
285
|
+
const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
|
|
286
|
+
if (!relPath.startsWith('.')) {
|
|
287
|
+
return `from './${relPath}'`
|
|
288
|
+
}
|
|
289
|
+
return `from '${relPath}'`
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Handle .js extension that might be .ts
|
|
294
|
+
if (resolvedPath.endsWith('.js')) {
|
|
295
|
+
const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
|
|
296
|
+
if (transpiledFiles.has(tsVersion)) {
|
|
297
|
+
const tempFile = transpiledFiles.get(tsVersion)
|
|
298
|
+
const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
|
|
299
|
+
if (!relPath.startsWith('.')) {
|
|
300
|
+
return `from './${relPath}'`
|
|
301
|
+
}
|
|
302
|
+
return `from '${relPath}'`
|
|
303
|
+
}
|
|
304
|
+
return match
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Try with .ts extension
|
|
308
|
+
const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'
|
|
309
|
+
|
|
310
|
+
// If we transpiled this file, use the temp file
|
|
311
|
+
if (transpiledFiles.has(tsPath)) {
|
|
312
|
+
const tempFile = transpiledFiles.get(tsPath)
|
|
313
|
+
const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
|
|
314
|
+
if (!relPath.startsWith('.')) {
|
|
315
|
+
return `from './${relPath}'`
|
|
316
|
+
}
|
|
317
|
+
return `from '${relPath}'`
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Try index.ts for directory imports
|
|
321
|
+
const indexTsPath = path.join(resolvedPath, 'index.ts')
|
|
322
|
+
if (transpiledFiles.has(indexTsPath)) {
|
|
323
|
+
const tempFile = transpiledFiles.get(indexTsPath)
|
|
324
|
+
const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
|
|
325
|
+
if (!relPath.startsWith('.')) {
|
|
326
|
+
return `from './${relPath}'`
|
|
327
|
+
}
|
|
328
|
+
return `from '${relPath}'`
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// If the import doesn't have a standard module extension, add .js for ESM compatibility
|
|
332
|
+
const standardExtensions = ['.js', '.mjs', '.cjs', '.json', '.node']
|
|
333
|
+
const hasStandardExtension = standardExtensions.includes(originalExt.toLowerCase())
|
|
334
|
+
|
|
335
|
+
if (!hasStandardExtension) {
|
|
336
|
+
return match.replace(importPath, importPath + '.js')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return match
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
// Also rewrite require() calls to point to transpiled TypeScript files
|
|
344
|
+
jsContent = jsContent.replace(
|
|
345
|
+
/require\s*\(\s*['"]([^'"]+?)['"]\s*\)/g,
|
|
346
|
+
(match, requirePath) => {
|
|
347
|
+
let resolvedPath = requirePath
|
|
348
|
+
|
|
349
|
+
// Check if this is a path alias
|
|
350
|
+
const resolvedAlias = resolveTsPathAlias(requirePath, tsConfig, configDir)
|
|
351
|
+
if (resolvedAlias) {
|
|
352
|
+
resolvedPath = resolvedAlias
|
|
353
|
+
} else if (requirePath.startsWith('.')) {
|
|
354
|
+
resolvedPath = path.resolve(fileBaseDir, requirePath)
|
|
355
|
+
} else {
|
|
356
|
+
return match
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Handle .js extension that might be .ts
|
|
360
|
+
if (resolvedPath.endsWith('.js')) {
|
|
361
|
+
const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
|
|
362
|
+
if (transpiledFiles.has(tsVersion)) {
|
|
363
|
+
const tempFile = transpiledFiles.get(tsVersion)
|
|
364
|
+
const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
|
|
365
|
+
const finalPath = relPath.startsWith('.') ? relPath : './' + relPath
|
|
366
|
+
return `require('${finalPath}')`
|
|
367
|
+
}
|
|
368
|
+
return match
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Try with .ts extension
|
|
372
|
+
const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'
|
|
373
|
+
|
|
374
|
+
// If we transpiled this file, use the temp file
|
|
375
|
+
if (transpiledFiles.has(tsPath)) {
|
|
376
|
+
const tempFile = transpiledFiles.get(tsPath)
|
|
377
|
+
const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
|
|
378
|
+
const finalPath = relPath.startsWith('.') ? relPath : './' + relPath
|
|
379
|
+
return `require('${finalPath}')`
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Otherwise, keep the require as-is
|
|
383
|
+
return match
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
// Write the transpiled file with updated imports
|
|
388
|
+
const tempFile = filePath.replace(/\.ts$/, '.temp.mjs')
|
|
389
|
+
fs.writeFileSync(tempFile, jsContent)
|
|
390
|
+
transpiledFiles.set(filePath, tempFile)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Start recursive transpilation from the main file
|
|
394
|
+
transpileFileAndDeps(mainFilePath)
|
|
395
|
+
|
|
396
|
+
// Get the main transpiled file
|
|
397
|
+
const tempJsFile = transpiledFiles.get(mainFilePath)
|
|
398
|
+
|
|
399
|
+
// Convert to file:// URL for dynamic import() (required on Windows)
|
|
400
|
+
const tempFileUrl = pathToFileURL(tempJsFile).href
|
|
401
|
+
|
|
402
|
+
// Store all temp files for cleanup (keep as paths, not URLs)
|
|
403
|
+
const allTempFiles = Array.from(transpiledFiles.values())
|
|
404
|
+
|
|
405
|
+
return { tempFile: tempFileUrl, allTempFiles, fileMapping: transpiledFiles }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Map error stack traces from temp .mjs files back to original .ts files
|
|
410
|
+
* @param {Error} error - The error object to fix
|
|
411
|
+
* @param {Map<string, string>} fileMapping - Map of original .ts files to temp .mjs files
|
|
412
|
+
* @returns {Error} - Error with fixed stack trace
|
|
413
|
+
*/
|
|
414
|
+
export function fixErrorStack(error, fileMapping) {
|
|
415
|
+
if (!error.stack || !fileMapping) return error
|
|
416
|
+
|
|
417
|
+
let stack = error.stack
|
|
418
|
+
|
|
419
|
+
// Create reverse mapping (temp.mjs -> original.ts)
|
|
420
|
+
const reverseMap = new Map()
|
|
421
|
+
for (const [tsFile, mjsFile] of fileMapping.entries()) {
|
|
422
|
+
reverseMap.set(mjsFile, tsFile)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Replace all temp.mjs references with original .ts files
|
|
426
|
+
for (const [mjsFile, tsFile] of reverseMap.entries()) {
|
|
427
|
+
const mjsPattern = mjsFile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
428
|
+
stack = stack.replace(new RegExp(mjsPattern, 'g'), tsFile)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
error.stack = stack
|
|
432
|
+
return error
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Clean up temporary transpiled files
|
|
437
|
+
* @param {string[]} tempFiles - Array of temp file paths to delete
|
|
438
|
+
*/
|
|
439
|
+
export function cleanupTempFiles(tempFiles) {
|
|
440
|
+
for (const file of tempFiles) {
|
|
441
|
+
if (fs.existsSync(file)) {
|
|
442
|
+
try {
|
|
443
|
+
fs.unlinkSync(file)
|
|
444
|
+
} catch (err) {
|
|
445
|
+
// Ignore cleanup errors
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|