Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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
like image 653
Dan Levy Avatar asked Nov 23 '20 01:11

Dan Levy


People also ask

What is import meta?

import. meta is an object available inside of an ES module that contains useful information about the environment in which the module runs. import. meta object is extensible, and the host can write any useful information into it.

Do I need to import Jest?

In your test files, Jest puts each of these methods and objects into the global environment. You don't have to require or import anything to use them.

Does Jest need TS-node?

When you run jest with a jest. config. ts file it will use ts-node to compile that file, then it will pass it to ts-jest which will compile your tests, then it will pass those . js tests to jest to run it.

How does Jest work with TypeScript?

Jest is a simple, lightweight testing framework that provides a variety of testing capabilities to JavaScript and TypeScript projects. It provides functionality like assertions, mocking, spies, running tests in parallel, prioritizing failed tests, and coverage analysis.


6 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(...);
like image 191
TygerTy Avatar answered Oct 03 '22 21:10

TygerTy


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.

like image 44
Christopher Lenz Avatar answered Oct 03 '22 21:10

Christopher Lenz


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

like image 43
pykiss Avatar answered Oct 03 '22 20:10

pykiss


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;
like image 37
ssylviageo Avatar answered Oct 03 '22 19:10

ssylviageo


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.

like image 28
Marcones Oliveira Avatar answered Oct 03 '22 20:10

Marcones Oliveira


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
      }
    ]
  ]
}
like image 37
G100 Avatar answered Oct 03 '22 19:10

G100