Skip to content

Commit 23fad16

Browse files
Copilotkobenguyent
andauthored
Fix TypeScript ESM module resolution docs - recommend tsx over ts-node/esm (#5324)
* Initial plan * Update TypeScript ESM documentation with correct guidance Co-authored-by: kobenguyent <7845001+kobenguyent@users.noreply.github.com> * Complete fix for TypeScript ESM import issue - recommend tsx over ts-node/esm Co-authored-by: kobenguyent <7845001+kobenguyent@users.noreply.github.com> * Add README for tsx ESM test case Co-authored-by: kobenguyent <7845001+kobenguyent@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: kobenguyent <7845001+kobenguyent@users.noreply.github.com>
1 parent 90d53c2 commit 23fad16

File tree

9 files changed

+152
-39
lines changed

9 files changed

+152
-39
lines changed

docs/typescript.md

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,27 +92,33 @@ Scenario('successful login', ({ I }) => {
9292
- 🚀 **Works with Mocha:** Uses CommonJS hooks that Mocha understands
9393
-**Complete:** Handles all TypeScript features (enums, decorators, etc.)
9494

95-
### Using ts-node/esm (Alternative)
95+
### Using ts-node/esm (Not Recommended)
9696

97-
If you prefer ts-node:
97+
> ⚠️ **Note:** `ts-node/esm` has significant limitations with module resolution and doesn't work well with modern ESM TypeScript projects. **We strongly recommend using `tsx` instead.** The information below is provided for reference only.
98+
99+
`ts-node/esm` has several issues:
100+
- Doesn't support `"type": "module"` in package.json
101+
- Doesn't resolve extensionless imports or `.js` imports to `.ts` files
102+
- Requires explicit `.ts` extensions in imports, which isn't standard TypeScript practice
103+
- Less reliable than `tsx` for ESM scenarios
104+
105+
**If you still want to use ts-node/esm:**
98106

99-
**Installation:**
100107
```bash
101108
npm install --save-dev ts-node
102109
```
103110

104-
**Configuration:**
105111
```typescript
106112
// codecept.conf.ts
107113
export const config = {
108114
tests: './**/*_test.ts',
109-
require: ['ts-node/esm'], // ← Use ts-node ESM loader
115+
require: ['ts-node/esm'],
110116
helpers: { /* ... */ }
111117
}
112118
```
113119

114-
**Required tsconfig.json:**
115120
```json
121+
// tsconfig.json
116122
{
117123
"compilerOptions": {
118124
"module": "ESNext",
@@ -121,12 +127,18 @@ export const config = {
121127
"esModuleInterop": true
122128
},
123129
"ts-node": {
124-
"esm": true,
125-
"experimentalSpecifierResolution": "node"
130+
"esm": true
126131
}
127132
}
128133
```
129134

135+
**Critical Limitations:**
136+
- ❌ Cannot use `"type": "module"` in package.json
137+
- ❌ Import statements must match the actual file (no automatic resolution)
138+
- ❌ Module resolution doesn't work like standard TypeScript/Node.js ESM
139+
140+
**Recommendation:** Use `tsx/cjs` instead for a better experience.
141+
130142
### Full TypeScript Features in Tests
131143

132144
With tsx or ts-node/esm, you can use complete TypeScript syntax including imports, enums, interfaces, and types:
@@ -174,7 +186,19 @@ This means the TypeScript loader isn't configured. Make sure:
174186

175187
**Error: Module not found when importing from `.ts` files**
176188

177-
Make sure you're using a proper TypeScript loader (`tsx/cjs` or `ts-node/esm`).
189+
When using `ts-node/esm` with ESM, you need to use `.js` extensions in imports:
190+
191+
```typescript
192+
// This will cause an error in ESM mode:
193+
import loginPage from "./pages/Login"
194+
195+
// Use .js extension instead:
196+
import loginPage from "./pages/Login.js"
197+
```
198+
199+
TypeScript will resolve the `.js` import to your `.ts` file during compilation. This is the standard behavior for ESM + TypeScript.
200+
201+
Alternatively, use `tsx/cjs` which doesn't require explicit extensions.
178202

179203
**TypeScript config files vs test files**
180204

lib/mocha/factory.js

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -62,34 +62,9 @@ class MochaFactory {
6262
const jsFiles = this.files.filter(file => !file.match(/\.feature$/))
6363
this.files = this.files.filter(file => !file.match(/\.feature$/))
6464

65-
// Load JavaScript test files using ESM imports
65+
// Load JavaScript test files using original loadFiles
6666
if (jsFiles.length > 0) {
67-
try {
68-
// Try original loadFiles first for compatibility
69-
originalLoadFiles.call(this, fn)
70-
} catch (e) {
71-
// If original loadFiles fails, load ESM files manually
72-
if (e.message.includes('not in cache') || e.message.includes('ESM') || e.message.includes('getStatus')) {
73-
// Load ESM files by importing them synchronously using top-level await workaround
74-
for (const file of jsFiles) {
75-
try {
76-
// Convert file path to file:// URL for dynamic import
77-
const fileUrl = `file://${file}`
78-
// Use import() but don't await it - let it load in the background
79-
import(fileUrl).catch(importErr => {
80-
// If dynamic import fails, the file may have syntax errors or other issues
81-
console.error(`Failed to load test file ${file}:`, importErr.message)
82-
})
83-
if (fn) fn()
84-
} catch (fileErr) {
85-
console.error(`Error processing test file ${file}:`, fileErr.message)
86-
if (fn) fn(fileErr)
87-
}
88-
}
89-
} else {
90-
throw e
91-
}
92-
}
67+
originalLoadFiles.call(this, fn)
9368
}
9469

9570
// add ids for each test and check uniqueness

lib/utils/loaderCheck.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,18 @@ CodeceptJS 4.x uses ES Modules (ESM) and requires a loader to run TypeScript tes
6565
✅ Complete: Handles all TypeScript features
6666
6767
┌─────────────────────────────────────────────────────────────────────────────┐
68-
│ Option 2: ts-node/esm (Alternative - Established, Requires Config)
68+
│ Option 2: ts-node/esm (Not Recommended - Has Module Resolution Issues)
6969
└─────────────────────────────────────────────────────────────────────────────┘
7070
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+
7180
Installation:
7281
npm install --save-dev ts-node
7382
@@ -84,11 +93,12 @@ CodeceptJS 4.x uses ES Modules (ESM) and requires a loader to run TypeScript tes
8493
"esModuleInterop": true
8594
},
8695
"ts-node": {
87-
"esm": true,
88-
"experimentalSpecifierResolution": "node"
96+
"esm": true
8997
}
9098
}
9199
100+
3. Do NOT use "type": "module" in package.json
101+
92102
📚 Documentation: https://codecept.io/typescript
93103
94104
Note: TypeScript config files (codecept.conf.ts) and helpers are automatically
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# TypeScript tsx ESM Test
2+
3+
This test demonstrates the recommended way to use TypeScript with CodeceptJS 4.x when you have `"type": "module"` in your package.json.
4+
5+
## Key Features
6+
7+
- Uses `tsx/cjs` as the TypeScript loader (recommended over ts-node/esm)
8+
- Has `"type": "module"` in package.json
9+
- Imports page objects without file extensions
10+
- Everything works seamlessly
11+
12+
## Configuration
13+
14+
- **Loader**: `tsx/cjs` in `require` array
15+
- **Package type**: `"module"`
16+
- **TypeScript module**: `"esnext"` with `"node"` module resolution
17+
- **Imports**: No file extensions needed (tsx handles resolution)
18+
19+
## Why tsx?
20+
21+
tsx is the recommended TypeScript loader for CodeceptJS 4.x because:
22+
- ✅ Works with `"type": "module"`
23+
- ✅ Handles extensionless imports
24+
- ✅ Fast (built on esbuild)
25+
- ✅ Zero config needed
26+
- ✅ Compatible with Mocha's loading system
27+
28+
## Why Not ts-node/esm?
29+
30+
ts-node/esm has significant limitations:
31+
- ❌ Doesn't work with `"type": "module"`
32+
- ❌ Doesn't resolve extensionless imports to .ts files
33+
- ❌ Requires complex configuration
34+
- ❌ Module resolution doesn't work like standard TypeScript ESM
35+
36+
## Running This Test
37+
38+
```bash
39+
cd test/data/typescript-tsx-esm
40+
../../../bin/codecept.js run --verbose
41+
```
42+
43+
You should see both scenarios pass successfully.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const config: CodeceptJS.MainConfig = {
2+
tests: "./*_test.ts",
3+
output: "./output",
4+
helpers: {
5+
CustomHelper: {
6+
require: "../helper.js"
7+
}
8+
},
9+
name: "typescript-tsx-esm-test",
10+
require: ["tsx/cjs"]
11+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "typescript-tsx-esm",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"devDependencies": {
6+
"tsx": "^4.20.6"
7+
}
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const { I } = inject();
2+
3+
export default {
4+
login(username: string) {
5+
I.say(`Logging in with user: ${username}`);
6+
},
7+
8+
logout() {
9+
I.say('Logging out');
10+
}
11+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2022",
4+
"lib": ["es2022", "DOM"],
5+
"esModuleInterop": true,
6+
"module": "esnext",
7+
"moduleResolution": "node",
8+
"strictNullChecks": false,
9+
"types": ["codeceptjs", "node"],
10+
"declaration": true,
11+
"skipLibCheck": true
12+
},
13+
"ts-node": {
14+
"esm": true,
15+
"transpileOnly": true
16+
},
17+
"exclude": ["node_modules"]
18+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// With tsx, you can import without extension - tsx handles resolution
2+
import loginPage from "./pages/Login";
3+
4+
Feature("TypeScript tsx ESM with type:module");
5+
6+
Scenario("Import page object without extension using tsx", () => {
7+
loginPage.login("testuser");
8+
});
9+
10+
Scenario("Page object methods work correctly", () => {
11+
loginPage.login("admin");
12+
loginPage.logout();
13+
});

0 commit comments

Comments
 (0)