I'm trying to move a TypeScript project from CJS to ESM and, along with that, our tests. They're using ts-jest
to run, and used to use __dirname
and __filename
, but in line with the change to ESM, I've changed them to use import.meta.dirname
and import.meta.filename
.
The problem seems to be that, when I run this with ts-jest
, it's always undefined
. I've checked and import.meta.url
is defined, but this would mean that I'd have to use fileURLToPath(new URL(".", import.meta.url))
everywhere to get the __dirname
.
I've run the same files with ts-node
and they are defined, which confuses me further. I've also checked this in the examples
folder of the official repository of ts-jest
, and it's also not defined or available when running with a "module": "nodenext"
setting.
Is this because of a setting I've missed, or is this a bug with ts-jest
that I should report?
tsconfig.json
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "esnext",
"isolatedModules": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
},
}
jest.config.ts
import type { Config } from 'jest';
export default {
extensionsToTreatAsEsm: ['.ts', '.tsx', '.mts'],
testRegex: '.*\.spec\.ts',
transform: {
'^.+\\.m?tsx?$': [
'ts-jest',
{
tsconfig: './tsconfig.json',
useESM: true,
},
],
},
testEnvironment: 'node',
rootDir: '.',
} satisfies Config;
package.json
{
"name": "project",
"private": true,
"type": "module",
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/jest": "29.5.14",
"@types/node": "22.10.10",
"typescript": "5.7.3"
},
"dependencies": {
"jest": "^29.0.0",
"ts-jest": "^29.3.3",
"ts-node": "^10.9.2"
}
}
location.ts
export const getPath = () => {
return import.meta.dirname;
};
ts-node
worksYou're right that ts-node
works correctly. It executes the file in a true ESM context, so you get import.meta.url
, and you can derive __dirname
from it like:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
ts-jest
does notHowever, ts-jest
compiles your TypeScript to JS and then passes it into Jest. But Jest currently:
Loads modules via CJS internally, not truly as native ESM.
Does not emulate import.meta.*
features correctly in its current architecture.
Even though you've configured "module": "nodenext"
, ts-jest
strips away ESM behavior during the compile phase and feeds Jest transpiled CommonJS.
Use import.meta.url
conditionally or abstract it behind a utility function that works in both environments:
// src/utils/paths.ts
import { fileURLToPath } from 'url';
import { dirname } from 'path';
export const getDirname = (metaUrl: string): string => dirname(fileURLToPath(metaUrl));
export const getFilename = (metaUrl: string): string => fileURLToPath(metaUrl);
Then use it like this:
// my-file.ts
import { getDirname } from './utils/paths.js';
const __dirname = getDirname(import.meta.url);
And in tests, you can mock this utility if needed or make sure the import.meta.url
is passed in from a file that runs in an actual ESM context.
If you're heavily ESM-based:
Use ts-node
+ vitest
instead of Jest.
vitest
supports ESM and import.meta.url
natively.
It’s gaining popularity for projects moving to modern TypeScript/ESM.
Use dynamic fileURLToPath(import.meta.url)
inline, though verbose.
Stick with Jest but refactor out the need for __dirname
in your test context — try to pass values as arguments or mock them.