Solution:
Root Cause: Mismatch Between Source and Runtime Output Extensions
When writing ESM in TypeScript or JSX, developers often write
import { foo } from './foo.ts' or leave off extensions. Because TypeScript does not rewrite specifiers during compilation (
tsc), importing
./foo.ts attempts to load a literal
.ts file at runtime, which fails under Node.js native ESM execution. Conversely, Node.js ESM requires the specifier to match the compiled output extension (
.js or
.mjs) even inside TypeScript source files.
# Diagnostic Verification:
Check the compiled JavaScript file in the output directory (
dist/ or
build/). If the source
.ts file contains
import './foo.ts' or
import './foo', the compiled output contains identical text, causing Node.js to fail:
text
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/project/dist/foo.ts' imported from /project/dist/app.js
# Step-by-Step Fix:
1.
Use .js Extensions in TypeScript Source Files:
Write imports in TypeScript targeting the eventual JavaScript output extension: typescript
// Correct TypeScript ESM specifier targeting compiled file
import { helper } from './helper.js';
2.
Configure TSConfig Module Settings:
Ensure tsconfig.json uses modern module resolution settings: json
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022"
}
}
3.
Execute Compiler:
Run npx tsc and verify output in target destination.# Prevention & Long-Term Monitoring:
Set "moduleResolution": "NodeNext" in tsconfig.json so the TypeScript compiler enforces mandatory output extensions during editing.