Skip to content

Commit 87c0766

Browse files
Copilotkobenguyent
andcommitted
Update TypeScript ESM documentation with correct guidance
Co-authored-by: kobenguyent <7845001+kobenguyent@users.noreply.github.com>
1 parent 67800aa commit 87c0766

File tree

8 files changed

+118
-32
lines changed

8 files changed

+118
-32
lines changed

docs/typescript.md

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,39 @@ export const config = {
121121
"esModuleInterop": true
122122
},
123123
"ts-node": {
124-
"esm": true,
125-
"experimentalSpecifierResolution": "node"
124+
"esm": true
126125
}
127126
}
128127
```
129128

129+
**Required package.json:**
130+
```json
131+
{
132+
"name": "your-project",
133+
"version": "1.0.0"
134+
// ⚠️ DO NOT include "type": "module" when using ts-node/esm
135+
// ts-node/esm works with CommonJS-style loading
136+
}
137+
```
138+
139+
**⚠️ Important Notes:**
140+
141+
1. **Do not use `"type": "module"`** in your package.json when using `ts-node/esm`. The ts-node/esm loader uses CommonJS-style module loading internally and is incompatible with `"type": "module"`.
142+
143+
2. **Use `.js` extensions in imports**: When writing TypeScript code for ESM, you must use `.js` extensions in your imports, even when importing TypeScript files:
144+
145+
```typescript
146+
// ❌ Wrong - will cause "Cannot find module" error
147+
import loginPage from "./pages/Login"
148+
149+
// ✅ Correct - use .js extension
150+
import loginPage from "./pages/Login.js"
151+
```
152+
153+
TypeScript will automatically resolve `.js` imports to `.ts` files during type-checking. This is the standard approach for ESM + TypeScript projects as documented in the [TypeScript handbook](https://www.typescriptlang.org/docs/handbook/modules/theory.html#typescript-imitates-the-hosts-module-resolution-but-with-types).
154+
155+
3. **For `"type": "module"` projects, use `tsx` instead**: If you need `"type": "module"` in your package.json, use `tsx/cjs` which is designed to work in both CommonJS and ESM contexts.
156+
130157
### Full TypeScript Features in Tests
131158

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

175202
**Error: Module not found when importing from `.ts` files**
176203

177-
Make sure you're using a proper TypeScript loader (`tsx/cjs` or `ts-node/esm`).
204+
When using `ts-node/esm` with ESM, you need to use `.js` extensions in imports:
205+
206+
```typescript
207+
// This will cause an error in ESM mode:
208+
import loginPage from "./pages/Login"
209+
210+
// Use .js extension instead:
211+
import loginPage from "./pages/Login.js"
212+
```
213+
214+
TypeScript will resolve the `.js` import to your `.ts` file during compilation. This is the standard behavior for ESM + TypeScript.
215+
216+
Alternatively, use `tsx/cjs` which doesn't require explicit extensions.
178217

179218
**TypeScript config files vs test files**
180219

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: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,23 @@ CodeceptJS 4.x uses ES Modules (ESM) and requires a loader to run TypeScript tes
8484
"esModuleInterop": true
8585
},
8686
"ts-node": {
87-
"esm": true,
88-
"experimentalSpecifierResolution": "node"
87+
"esm": true
8988
}
9089
}
9190
91+
3. In package.json, DO NOT include "type": "module"
92+
ts-node/esm is incompatible with "type": "module"
93+
94+
If you need "type": "module", use tsx/cjs instead.
95+
96+
⚠️ Important: When using ESM with TypeScript, use .js extensions in imports:
97+
98+
❌ Wrong: import loginPage from "./pages/Login"
99+
✅ Correct: import loginPage from "./pages/Login.js"
100+
101+
TypeScript will resolve .js imports to .ts files during type-checking.
102+
This is the recommended approach for ESM + TypeScript projects.
103+
92104
📚 Documentation: https://codecept.io/typescript
93105
94106
Note: TypeScript config files (codecept.conf.ts) and helpers are automatically
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-esm-imports-test",
10+
require: ["ts-node/esm"]
11+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "typescript-esm-imports",
3+
"version": "1.0.0",
4+
"devDependencies": {
5+
"ts-node": "^10.9.2"
6+
}
7+
}
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": "nodenext",
7+
"moduleResolution": "nodenext",
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+
// Test using .js extension to import .ts file (ESM + TypeScript best practice)
2+
import loginPage from "./pages/Login.js";
3+
4+
Feature("TypeScript ESM Imports");
5+
6+
Scenario("Import page object with .js extension", () => {
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)