javascriptnode.jstypescriptts-jestts-node

`import.meta.dirname` and `import.meta.filename` are `undefined` when using `ts-jest`


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;
};

Solution

  • Detailed Explanation

    ts-node works

    You'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 not

    However, ts-jest compiles your TypeScript to JS and then passes it into Jest. But Jest currently:

    Even though you've configured "module": "nodenext", ts-jest strips away ESM behavior during the compile phase and feeds Jest transpiled CommonJS.


    Recommended Workaround

    Use import.meta.url conditionally or abstract it behind a utility function that works in both environments:

    Utility wrapper:

    // 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.

    Alternative Solutions

    If you're heavily ESM-based:

    1. 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.

    2. Use dynamic fileURLToPath(import.meta.url) inline, though verbose.

    3. Stick with Jest but refactor out the need for __dirname in your test context — try to pass values as arguments or mock them.