How to Use `import.meta` When Testing With Jest

Viewed 16552

I am writing Node.js code (in TypeScript) using ESModules and I need access to __dirname. In order to access the ESM equivalent of __dirname in CommonJS, I call dirname(fileURLToPath(import.meta.url)). I am also writing tests in Jest with TypeScript. Using this guide, I set up Babel. When I run the jest command, I get

const DIRNAME = (0, _path.dirname)((0, _url.fileURLToPath)(import.meta.url));
                                                                      ^^^^
SyntaxError: Cannot use 'import.meta' outside a module

Here are the files I wrote

someCode.ts:

import { dirname } from "path";
import { fileURLToPath } from "url";

const DIRNAME = dirname(fileURLToPath(import.meta.url));

export const x = 5;

someCode.test.ts:

import { x } from "./someCode";

it("Returns 5 as the result", () => {
    expect(x).toEqual(5);
});

.babelrc.json:

{
    "presets": [
        ["@babel/preset-env", { "targets": { "node": "current" } }],
        "@babel/preset-typescript"
    ]
}

tsconfig.json:

{
    "compilerOptions": {
        "target": "ES2020",
        "module": "ESNext",
        "moduleResolution": "node"
    },
    "include": ["./src/**/*.ts", "./src/**/*.js"]
}

package.json:

{
    "name": "test",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "type": "module",
    "scripts": {
        "test": "jest"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "devDependencies": {
        "@babel/core": "^7.12.7",
        "@babel/preset-env": "^7.12.7",
        "@babel/preset-typescript": "^7.12.7",
        "jest": "^26.6.3",
        "typescript": "^4.1.2"
    }
}

Environment:

  • Node: 14.1.0
  • See package.json for module versions
9 Answers

The solution: Modularize the ESM code and mock it.

I ran into this issue recently, as well, and I ended up having to:

  1. Export the ESM specific issue, e.g. import.meta functionality, into it's own, "utils" file/function.
  2. Then create a mock function that doesn't require ESM specific functionality in a mocks directory.
  3. in the files that use that functionality, declare jest.mock("/path/to/that/file.ts")

The process will look something like this:

The Original File Structure

src/
--/someCode.ts
--/__tests__/
/--/--/someCode.test.ts
// someCode.ts
...
const getApiUrl = () => { 
  ...
  const url = import.meta.env.SOME_ENV_VAR_HERE;
  ...
  return url;
};
...

The New File Structure

Heading

src/
--/someCode.ts
--/utils.js
--/__mocks__/
--/--/utils.ts
--/__tests__/
--/--/someCome.test.ts

and then in the files themselves:

// someCode.ts
...
import { getApiUrl } from "./utils.ts";
...

// utils.js
export const getApiUrl = () => {...};

and for the tests:

// __mocks__/utils.ts
export const getpiUrl = jest.fn(() => 'some.url');

// __tests__/utils.test.ts
...
jest.mock("src/util.ts");
...
describe(...);

We are using import.meta.url to make use of web workers, for example using the native worker support in Webpack 5. That works great but fails when running the Jest tests.

babel-vite-preset does not handle this as far as I can tell. The top answer about extracting and mocking the code that uses import.meta does work but is unwieldy.

I found the currently best solution is to use babel-plugin-transform-import-meta. That simple plugin replaces the import.meta.url syntax with Node.js code to retrieve the URL to the current file, so it works nicely in tests. Note that you will only want to have this plugin active when running the tests.

I had the same problem and I fix it with a babel plugin: search-and-replace

In my code I changed all import.meta.url with import_meta_url

And I add the plugin to babel config to change it by import.meta.url in development and production and by resolve(__dirname, 'workers')

I was able to do so because it matched all my cases, if your import_meta_url needs to be replaced with different stings in different scenarios you would need to use import_meta_url_1 import_meta_url_2 etc. for instance

finally my babel.config.js

const { resolve } = require('path');

module.exports = (api) => {
  api.cache.using(() => process.env.NODE_ENV);

  if (process.env.NODE_ENV === 'test') {
    return {
      presets: ['next/babel'],
      plugins: [
        [
          'search-and-replace',
          {
            rules: [
              {
                searchTemplateStrings: true,
                search: 'import_meta_url',
                replace: resolve(__dirname, 'workers'),
              },
            ],
          },
        ],
      ],
    };
  }

  return {
    presets: ['next/babel'],
    plugins: [
      [
        'search-and-replace',
        {
          rules: [
            {
              searchTemplateStrings: true,
              search: 'import_meta_url',
              replace: 'import.meta.url',
            },
          ],
        },
      ],
    ],
  };
};

GL

I also ran into this issue. I was able to resolve the issue by dynamically inserting a correct value during babel processing. Instead of the search-and-replace babel plugin that was suggested, I used the codegen macro for babel.

// someCode.ts
import { dirname } from "path";
import { fileURLToPath } from "url";
import codegen from 'codegen.macro';

const DIRNAME = dirname(fileURLToPath(codegen`module.exports = process.env.NODE_ENV === "test" ? "{ADD VALID VALUE FOR TESTS}" : "import.meta.url"`));

export const x = 5;

babel-vite-preset

I think this one is great.

non-official.

you can choose to use it only babel-plugin-transform-vite-meta-env plugin or use whole preset and set config like below

{
  "presets": [
    [
      "babel-preset-vite",
      {
        "env": true, // defaults to true
        "glob": false // defaults to true
      }
    ]
  ]
}

I found a solution that is so ridiculous i don't even believed when it worked:

Create a commonjs file, like "currentPath.cjs", Inside it put something like:

module.exports = __dirname;

In your module use:

import * as currentPath from "./currentPath.cjs";
console.log(currentPath.default);

And watch the magic happen. PS: Use the path.normalize() before using it on anything.

Inside your tsconfig.json change to

    {
    "compilerOptions": {
        "target": "ES2020",
        "module": "ES2020",
        "moduleResolution": "node"
    },
    "include": ["./src/**/*.ts", "./src/**/*.js"]
}

I struggled with this for a Node.js, Express, TypeScript, Jest, ts-jext project. The issue, for me, was getting the jest.config.json configured correctly.

I'm posting my files in case this helps someone.

PACKAGE.JSON

{
  "name": "express-react",
  "version": "0.0.0",
  "private": true,
  "main": "dist/index.js",
  "type": "module",
  "engines": {
    "node": ">=14.0.0"
  },
  "scripts": {
    "start:dev": "npm run build && npm run dev",
    "build": "npm run format && npm run lint && npm run tsc",
    "start:prod": "npm run build && node --experimental-specifier-resolution=node dist/index.js",
    "start": "npm run start:prod",
    "dev": "cross-env-shell DEBUG=express:* node --experimental-specifier-resolution=node --loader ts-node/esm ./src/index.ts",
    "format": "prettier --write \"**/*.ts\"",
    "lint": "eslint . --ext 'ts,json'",
    "tsc": "tsc",

    "test": "cross-env-shell TS_JEST_LOG=ts-jest.jog NODE_OPTIONS=--experimental-vm-modules jest --detectOpenHandles --verbose --testTimeout 20000 --coverage=true --reporters=default --json --debug --outputFile=./test/log.json",
    "test:list": "jest --detectOpenHandles --coverage=true --listTests --debug"

  },
  "dependencies": {

    "cookie-parser": "~1.4.4",
    "cors": "^2.8.5",
    "debug": "~2.6.9",
    "dotenv": "^16.0.0",
    "express": "~4.16.1",
    "express-fileupload": "^1.3.1",
    "http-errors": "^1.6.3",
    "into-stream": "^7.0.0",
    "jest": "^28.1.0",
    "morgan": "~1.9.1",
    "multer": "^1.4.4",
    "save-dev": "0.0.1-security",
    "sharp": "^0.30.4",
    "supertest": "^6.2.3",
    "ts-jest": "^28.0.2",
    "ts-node": "^10.7.0",
    "uuid": "^8.3.2"
  },
  "devDependencies": {
    "@types/jest": "^27.5.1",
    "@types/supertest": "^2.0.12",
    "@azure/storage-blob": "^12.9.0",
    "@types/cookie-parser": "^1.4.3",
    "@types/cors": "^2.8.12",
    "@types/debug": "^4.1.7",
    "@types/express": "^4.16.1",
    "@types/express-fileupload": "^1.2.2",
    "@types/http-errors": "^1.8.2",
    "@types/into-stream": "^3.1.1",
    "@types/morgan": "^1.9.3",
    "@types/multer": "^1.4.7",
    "@types/node": "^17.0.31",
    "@types/sharp": "^0.30.2",
    "@types/uuid": "^8.3.4",
    "@typescript-eslint/eslint-plugin": "^5.22.0",
    "@typescript-eslint/parser": "^5.22.0",
    "cross-env": "^7.0.3",
    "eslint": "^8.15.0",
    "eslint-config-prettier": "^8.5.0",
    "eslint-plugin-import": "^2.26.0",
    "eslint-plugin-jsx-a11y": "^6.5.1",
    "eslint-plugin-node": "^11.1.0",
    "nodemon": "^2.0.16",
    "prettier": "^2.6.2",
    "tslib": "^2.4.0",
    "typescript": "^4.6.4"
  }
}

TSCONFIG.JSON

{
  "name": "express-react",
  "version": "0.0.0",
  "private": true,
  "main": "dist/index.js",
  "type": "module",
  "engines": {
    "node": ">=14.0.0"
  },
  "scripts": {
    "start:dev": "npm run build && npm run dev",
    "build": "npm run format && npm run lint && npm run tsc",
    "start:prod": "npm run build && node --experimental-specifier-resolution=node dist/index.js",
    "start": "npm run start:prod",
    "dev": "cross-env-shell DEBUG=express:* node --experimental-specifier-resolution=node --loader ts-node/esm ./src/index.ts",
    "format": "prettier --write \"**/*.ts\"",
    "lint": "eslint . --ext 'ts,json'",
    "tsc": "tsc",

    "test": "cross-env-shell TS_JEST_LOG=ts-jest.jog NODE_OPTIONS=--experimental-vm-modules jest --detectOpenHandles --verbose --testTimeout 20000 --coverage=true --reporters=default --json --debug --outputFile=./test/log.json",
    "test:list": "jest --detectOpenHandles --coverage=true --listTests --debug"

  },
  "dependencies": {

    "cookie-parser": "~1.4.4",
    "cors": "^2.8.5",
    "debug": "~2.6.9",
    "dotenv": "^16.0.0",
    "express": "~4.16.1",
    "express-fileupload": "^1.3.1",
    "http-errors": "^1.6.3",
    "into-stream": "^7.0.0",
    "jest": "^28.1.0",
    "morgan": "~1.9.1",
    "multer": "^1.4.4",
    "save-dev": "0.0.1-security",
    "sharp": "^0.30.4",
    "supertest": "^6.2.3",
    "ts-jest": "^28.0.2",
    "ts-node": "^10.7.0",
    "uuid": "^8.3.2"
  },
  "devDependencies": {
    "@types/jest": "^27.5.1",
    "@types/supertest": "^2.0.12",
    "@azure/storage-blob": "^12.9.0",
    "@types/cookie-parser": "^1.4.3",
    "@types/cors": "^2.8.12",
    "@types/debug": "^4.1.7",
    "@types/express": "^4.16.1",
    "@types/express-fileupload": "^1.2.2",
    "@types/http-errors": "^1.8.2",
    "@types/into-stream": "^3.1.1",
    "@types/morgan": "^1.9.3",
    "@types/multer": "^1.4.7",
    "@types/node": "^17.0.31",
    "@types/sharp": "^0.30.2",
    "@types/uuid": "^8.3.4",
    "@typescript-eslint/eslint-plugin": "^5.22.0",
    "@typescript-eslint/parser": "^5.22.0",
    "cross-env": "^7.0.3",
    "eslint": "^8.15.0",
    "eslint-config-prettier": "^8.5.0",
    "eslint-plugin-import": "^2.26.0",
    "eslint-plugin-jsx-a11y": "^6.5.1",
    "eslint-plugin-node": "^11.1.0",
    "nodemon": "^2.0.16",
    "prettier": "^2.6.2",
    "tslib": "^2.4.0",
    "typescript": "^4.6.4"
  }
}

JEST.CONFIG.JS

/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
  clearMocks: true,
  moduleFileExtensions: ["js", "ts", "json", "node"],
  roots: ["./"],
  testMatch: ["./**/*.test.ts"],
  preset: "ts-jest",
  testEnvironment: "node",
  transform: {
    "^.+\\.ts?$": "ts-jest",
  },
  transformIgnorePatterns: [
    "./node_modules/",
    "./dist/",
    "./files/",
    "./public/",
  ],
  extensionsToTreatAsEsm: ['.ts'],
  globals: {
    "ts-jest": {
      tsconfig: './tsconfig.json',
      useESM: true
    }
  }
};

1.change your tsconfig with below

compilerOptions": {
       "module": "esnext",
       //... remaining options 
     }

2.add below in jest.config

  globals: {
     "ts-jest": {
      tsconfig: false,
       useESM: true,
      babelConfig: true,
       plugins: ["babel-plugin-transform-vite-meta-env"],
     },
    },
   transform: {
      "^.+\\.(js|jsx|ts)$": "babel-jest",
    }
`

3.Now it will start giving error Cannot use import.meta To resolve this problem add below in babel config: `

module.exports = function (api) {
 
 api.cache(true);
  const presets = [
   ["@babel/preset-env", { targets: { node: "current" } }],
  "@babel/preset-typescript",
  "@babel/preset-react",
];

return {
  presets,
  plugins: [
    "@babel/plugin-transform-runtime",
    "babel-plugin-transform-import-meta",
    "babel-plugin-transform-vite-meta-env",
   ],
  };
 };` 

@babel/plugin-transform-runtime, babel-plugin-transform-import-meta these plugins will help you to resolve this problem

Related