Encountering an SyntaxError with Next.js and Jest: "Import statement cannot be used outside a module"

Currently, I am developing a project with Next.js using TypeScript. For testing purposes, I rely on Jest and React Testing Lib. However, I have encountered an issue where I receive a SyntaxError: Cannot use import statement outside a module for components that utilize rehype-raw.

My understanding is that Jest does not fully support ES6, so there may be a need to transform the node_modules. This transformation can be set up by configuring transformIgnorePatterns. For instance, adding

"transformIgnorePatterns": ["node_modules/(?!rehype-raw)/"]
should allow for the proper transformation of rehype-raw without affecting other modules, thus resolving the error.

Unfortunately, this solution has not worked for me. I am unsure as to why and how I can address this issue. None of the suggested solutions have been effective in solving the problem. Below, you will find my error output, jest.config.js, and babel.rc files for reference.

Error Output:

 FAIL  test/pages/companies/[id].test.tsx                  
  ● Test suite failed to run

    Jest encountered an unexpected token

    [...]

    Details:

    /path/frontend-job/node_modules/rehype-raw/index.js:7
    import {raw} from 'hast-util-raw'
    ^^^^^^

    SyntaxError: Cannot use import statement outside a module

      3 | import Image from 'next/image';
      4 | import { Remark } from 'react-remark';
    > 5 | import rehypeRaw from 'rehype-raw';
        | ^
      6 | import rehypeSanitize from 'rehype-sanitize';
      7 | import { logError } from '@utils/logger';
      8 |

      at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1728:14)
      at Object.<anonymous> (components/markdown/JobMarkdown.tsx:5:1)

jest.config.js:

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

module.exports = {
  roots: ['<rootDir>'],
  moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node'],
  setupFiles: ['<rootDir>/test/setup.js'],
  testPathIgnorePatterns: ['<rootDir>[/\\\\](node_modules|.next)[/\\\\]'],
  transform: {
    '^.+\\.(ts|tsx)$': 'babel-jest',
  },
  transformIgnorePatterns: [
    'node_modules/(?!rehype-raw)/',
  ],
  watchPlugins: [
    'jest-watch-typeahead/filename',
    'jest-watch-typeahead/testname',
  ],
  moduleNameMapper: {
    // Force mocks: https://github.com/facebook/jest/issues/4262
    '@api/axios': '<rootDir>/test/__mocks__/axios.js',
    // Normal module aliases
    '\\.(css|less|sass|scss)$': 'identity-obj-proxy',
    '\\.(gif|ttf|eot|svg|png)$': '<rootDir>/test/__mocks__/fileMock.js',
    '^@test/(.*)$': resolve(__dirname, './test/$1'),
    '^@test/faker/(.*)$': resolve(__dirname, './test/faker/$1'),
    '^@components/(.*)$': resolve(__dirname, './components/$1'),
    '^@pages/(.*)$': resolve(__dirname, './pages/$1'),
    '^@utils/(.*)$': resolve(__dirname, './utils/$1'),
    '^@api/(.*)$': resolve(__dirname, './api/$1'),
    '^@store/(.*)$': resolve(__dirname, './store/$1'),
  },
  testEnvironment: 'jsdom',
};

babel.rc:

{
  "presets": ["next/babel"]
}

Answer №1

Unfortunately, Jest does not offer support for ECMAScript Modules, which happens to be the dependency utilized by hast-util-raw. Additionally, the issue persisted with transformIgnorePatterns not functioning as expected. Here's the workaround I came up with while working on a Next.JS setup.

1. Removal of babel.rc

The removal of your babel.rc file is necessary. You can directly incorporate the following modifications instead.

2. Including moduleNameMapper

This step resolves another error related to the unavailability of parse5, which is demanded by hast-util-raw

/** @type {import('jest').Config} */
const customJestConfig = {
  //...
  moduleNameMapper: {
    'parse5/lib/parser/index.js':
      '<rootDir>/node_modules/hast-util-raw/node_modules/parse5/lib/parser/index.js',
  }
}

3. Integration of transformIgnorePatterns

Addition of transformIgnorePatterns, positioned right at the end, is required. Inserting it directly into the configuration did not yield the desired outcome for some reason. It was also essential to include each package relied upon by hast-util-raw. Surely, there must be a more efficient approach to tackle this challenge :)

module.exports = async () => ({
  ...(await createJestConfig(customJestConfig)()),
  transformIgnorePatterns: [
    'node_modules/(?!(rehype-raw|hast-util-raw|unist-util-position|unist-util-visit|unist-util-visit-parents|unist-util-is|hast-util-from-parse5|hastscript|property-information|hast-util-parse-selector|space-separated-tokens|comma-separated-tokens|vfile-location|web-namespaces|hast-util-to-parse5|zwitch|html-void-elements)/)'
  ]
})


In case you are curious about my complete configuration...

/* eslint-disable @typescript-eslint/no-var-requires */
// jest.config.js
const nextJest = require('next/jest')

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './'
})

// Incorporate any personalized configurations to be passed to Jest
/** @type {import('jest').Config} */
const customJestConfig = {
  // Include additional setup options before every test execution
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  // If using TypeScript with a baseUrl set to the root directory, then the below aliases are crucial to function
  moduleDirectories: ['<rootDir>/node_modules', '<rootDir>/src', '<rootDir>/src/pages'],
  testEnvironment: 'jest-environment-jsdom',
  testPathIgnorePatterns: [
    '<rootDir>/.next/',
    '<rootDir>/node_modules/',
    '<rootDir>/coverage',
    '<rootDir>/dist'
  ],
  moduleNameMapper: {
    'react-markdown': '<rootDir>/node_modules/react-markdown/react-markdown.min.js',
    'parse5/lib/parser/index.js':
      '<rootDir>/node_modules/hast-util-raw/node_modules/parse5/lib/parser/index.js'
  },
  resolver: '<rootDir>/.jest/resolver.js',
  clearMocks: true,
  testTimeout: 20000
}

// The export of createJestConfig is structured as such to allow next/jest to fetch the Next.js config asynchronously
// module.exports = createJestConfig(customJestConfig)
module.exports = async () => ({
  ...(await createJestConfig(customJestConfig)()),
  transformIgnorePatterns: [
    'node_modules/(?!(rehype-raw|hast-util-raw|unist-util-position|unist-util-visit|unist-util-visit-parents|unist-util-is|hast-util-from-parse5|hastscript|property-information|hast-util-parse-selector|space-separated-tokens|comma-separated-tokens|vfile-location|web-namespaces|hast-util-to-parse5|zwitch|html-void-elements)/)'
  ]
})

Answer №2

To better grasp the concept, it's important to note that Next.js instructs Jest not to transform packages within the /node_modules directory unless specified in the config file (using the transpilePackages property).

Although managing proper exclusions for transformations can be tricky, a helpful discussion on this link pointed me in the right direction.

If you prefer not to manually maintain the array, as it is dependent on transpilePackages in Next.js, you can adapt their approach outlined in this source code snippet. This method allows for the transformation of both packages listed in transpilePackages and those necessary for Jest functionality.

You can view the differences between my workaround and the original setup in this commit: here

I have opted against sharing direct code in this post to avoid confusion.

Answer №3

I found the solution that worked best for me was using @rottitime. One thing I observed is that moving transformIgnorePatterns to the config object outside of the async function at the bottom does not yield the desired outcome. Here's an example of what won't work:

import nextJest from 'next/jest.js'
 
const createJestConfig = nextJest({
  dir: './',
})
 
// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const customJestConfig = {
  // Add more setup options before each test is run
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: 'jest-environment-jsdom',
  preset: 'ts-jest',
  testPathIgnorePatterns: [
    '<rootDir>/.next/',
    '<rootDir>/node_modules/',
    '<rootDir>/coverage',
    '<rootDir>/dist'
  ],
  transformIgnorePatterns: [
    'node_modules/(?!(mui-tel-input)/)'
  ]
}

const exportConfig = async () => ({
  ...(await createJestConfig(customJestConfig)())
});


export default exportConfig;

Answer №4

Have you incorporated the type:"module" setting in your package.json file?

Answer №5

If you're using Next.js, you're in luck as it comes with built-in Jest support. I recommend checking out the instructions outlined here to set up Jest seamlessly.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Disappear the loading icon once all children have finished rendering

I'm facing a minor issue with rendering in React and struggling to find a solution. Here's what's happening: My component acts as a wrapper for multiple entries, like this: class MyWrapper extends Component { renderItems() { return ( ...

Troubleshooting the inclusion of nodemon in package.json

I am interested in implementing nodemon to automatically recompile the project when there are changes made to the code during development. package.json { "name": "insurance-web-site", "version": "0.1.0", " ...

Access the state of a Vuex module within a different module's action

I'm feeling a bit lost when it comes to working with Vuex store components. How can I access the state of another module? I've tried various methods to retrieve data from the store, but I always end up with an Observer object. What is the corre ...

Guide on uploading files and data simultaneously by leveraging ajax, jquery, and spring mvc

How can I use ajax, query, and Spring MVC to upload data along with a file? Below is my modal pop-up, could someone help me with this? <!-- Modal --> <div id="fileUpload" class="modal fade" role="dialog"> <d ...

Issue with Vue-Validator form validation not functioning properly on JS Fiddle

I'm having trouble with vue-validator on JSFiddle. Can someone please assist in troubleshooting the issue so I can proceed with my main question? Check out JSFiddle Html: <div id="app"> <validator name="instanceForm"> & ...

No pipe named '' was discovered

I have created a custom pipe in Angular, but when I try to use it, I keep receiving the error message: "No pipe found with name 'RefPipe'". I have searched for solutions online and they all suggest importing the pipe. However, I have tried import ...

Unraveling URLs in JSON with TypeScript

After retrieving a Json string from the BE API, I am parsing it into an array of Products[]. This collection is structured as follows: class Products { ProductId: number; ProductName: string; Price: number; ProductUrl: string; } The issue I' ...

Confirming user credentials for every page

I am currently working with an HTML page that includes front end PHP for server side scripting. One issue I have encountered is the configuration page, which can be accessed by disabling JavaScript in Mozilla settings. My current validation method relies ...

What is causing ESLint errors not to be displayed in the browser when they occur?

Recently, I noticed that ESLint errors are no longer appearing on the browser screen while I develop my Next.js (v14) app. Despite having several ESLint rules in place, they do not display on the screen like they did when using CRA. I prefer it when the a ...

Adjusting the width of a <div> element dynamically using two array variables within a Vue.js loop

As a beginner in vuejs, I am facing a challenge in understanding its functionality. Currently, I have implemented a 'vuejs for each-loop' within a div to fetch data from a JSON object. My goal is to calculate the current distance relative to a m ...

Discover the content seamlessly integrated into HTML through a Chrome extension with Selenium

I am facing an issue while using Selenium to capture text injected by a Chrome Extension. Even though the text is being injected correctly, I am struggling to make the Selenium test wait for the div to appear and then store the inner text in a variable. Ca ...

Vue in d3 - the property ownerDocument is not defined

I'm currently working on integrating d3 charts with Vue. I've noticed that using a ref seems to trigger an error, but is that the correct approach? <template> <div ref="chart"></div> </template> const char ...

Using NodeJS in conjunction with Nginx

Running both NodeJS and Nginx on the same server has posed a unique challenge for me. I have successfully configured Nginx to handle requests from "www.example.com" while also wanting NodeJS to take requests from "api.example.com". The setup is almost comp ...

Alternative options in Javascript for event listeners

Apologies if it seems like I'm asking for opinions, I could use some clarification. I'm curious as to why it's necessary to replace attributes such as onclick with anonymous functions. What benefits does this provide? For instance, if I ha ...

How to retrieve the current zoom level of a map in Angular 2

I am facing an issue with my custom map where the markers are not aligning properly when zoomed in. Currently, I am using an overlayview as a marker and setting the center point of the marker in the file google-maps-types.ts. What I need is a way to retrie ...

What sets apart initializing an Express app using "app = new Express()" compared to "app = express()"?

Throughout my experience, express apps have always been initialized like this: var express = require('express') var app = express() However, today I came across an example where a new operator was used: var Express = require('express&apos ...

Utilizing JQuery to extract information from a JSON response

I've been struggling with accessing a specific piece of content within a JSON object. This is the code I'm using to fetch the data: function retrieveData(keyword){ $.ajax({ url: "https://openlibrary.org/api/books?bibkeys=ISBN ...

You cannot assign the "unknown" type to the "boolean" type

if (queueEntry) { this.inProcess.push(queueEntry); try { await callback(queueEntry); this.inProcess = this.inProcess.filter(queueTmp => queueTmp.id !== queueEntry.id).concat([]); th ...

Guide on incorporating d3 axis labels within <a> elements?

Issue at Hand: I am working with a specific scale var y = d3.scalePoint().domain(['a','b','c']).range([0,100]); I have successfully created an axis for this scale var y_axis = svg.append("g").attr("class", "axis").call(d3. ...

Issues have arisen with the functionality of noneditable contents in tinymce

I am currently using the tinyMCE editor and I need to make certain content readonly (nonEditable). According to the documentation, if I apply the class "mceNonEditable" to specific elements, it should meet this requirement. However, when I select that ele ...