remix: Unable to import from libs using ESM

The following error is thrown when trying to import from libs using ESM (like unist-util-visit):

Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /my-app/node_modules/unist-util-visit/index.js
require() of ES modules is not supported.
require() of /my-app/node_modules/unist-util-visit/index.js from /my-app/build/index.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.
Instead rename /my-app/node_modules/unist-util-visit/index.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from /my-app/node_modules/unist-util-visit/package.json.

Code which caused the error to be thrown:

// mdx.server.ts
import { visit } from "unist-util-visit"

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 62
  • Comments: 46 (17 by maintainers)

Most upvoted comments

Looks like it’s merged: https://github.com/remix-run/remix/pull/239#pullrequestreview-726906749

My working setup

// esm-modules.js
module.exports = {
  importUnified: async () => import('unified'),
  importParseMarkdown: async () => import('remark-parse'),
  importGfm: async () => import('remark-gfm'),
  importEmoji: async () => import('remark-emoji'),
  importSlug: async () => import('remark-slug'),
  importGithub: async () => import('remark-github'),
  importRemarkToRehype: async () => import('remark-rehype'),
  importRehypeStringify: async () => import('rehype-stringify'),
}
import {
  importUnified,
  importParseMarkdown,
  importGfm,
  importEmoji,
  importSlug,
  importGithub,
  importRemarkToRehype,
  importRehypeStringify,
} from './esm-modules'

// in some async context
const { unified } = await importUnified()
const { default: parseMarkdown } = await importParseMarkdown()
const { default: gfm } = await importGfm()
const { default: emoji } = await importEmoji()
const { default: slug } = await importSlug()
const { default: github } = await importGithub()
const { default: remarkToRehype } = await importRemarkToRehype()
const { default: rehypeStringify } = await importRehypeStringify()

it’s not pretty, but it works

Just to add a +1. I’m having the same issue as @edmundhung with react-markdown

Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /Users/niall.barber/Git/projects/react/ndb-remix/node_modules/react-markdown/index.js
require() of ES modules is not supported.
require() of /Users/niall.barber/Git/projects/react/ndb-remix/node_modules/react-markdown/index.js from /Users/niall.barber/Git/projects/react/ndb-remix/api/build/index.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.
Instead rename /Users/niall.barber/Git/projects/react/ndb-remix/node_modules/react-markdown/index.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from /Users/niall.barber/Git/projects/react/ndb-remix/node_modules/react-markdown/package.json.

    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1102:13)
    at Module.load (internal/modules/cjs/loader.js:950:32)
    at Function.Module._load (internal/modules/cjs/loader.js:790:14)
    at Module.require (internal/modules/cjs/loader.js:974:19)
    at require (internal/modules/cjs/helpers.js:92:18)
    at Object.<anonymous> (/Users/niall.barber/Git/projects/react/ndb-remix/api/build/index.js:185:40)
    at Module._compile (internal/modules/cjs/loader.js:1085:14)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
    at Module.load (internal/modules/cjs/loader.js:950:32)
    at Function.Module._load (internal/modules/cjs/loader.js:790:14)
    at Module.require (internal/modules/cjs/loader.js:974:19)
    at require (internal/modules/cjs/helpers.js:92:18)
    at /Users/niall.barber/Git/projects/react/ndb-remix/node_modules/@remix-run/serve/index.js:39:17
    at Layer.handle [as handle_request] (/Users/niall.barber/Git/projects/react/ndb-remix/node_modules/express/lib/router/layer.js:95:5)
    at next (/Users/niall.barber/Git/projects/react/ndb-remix/node_modules/express/lib/router/route.js:137:13)
    at next (/Users/niall.barber/Git/projects/react/ndb-remix/node_modules/express/lib/router/route.js:131:14)

Is there anything we can do as a community to help Remix have support for ESM modules? From a framework like this that advocates for using standards, the expectation from the community is for it to support ESM modules without any workarounds. We should be able to just do:

import { somthing } from "an-esm-module";

And it should just work™. Maybe a first good step would be to make remix itself an ESM module ("type": "module") and if folks actually want to use CommonJS, the can simply change the extension of their files to .cjs.

@Ferdzzzzzzzz I have ReScript working with Remix here: https://github.com/tom-sherman/remix-rescript-example but the solution was not pretty…

It required a patch to Remix’s esbuild config to exclude rescript from a specific transpilation step: https://github.com/tom-sherman/remix-rescript-example/blob/7625c5562b0bbe7a5cd435693ac2bb69c58b2b66/patches/@remix-run+dev+1.1.1.patch

After applying this patch, you can set transpileModules: ["rescript"] (or, in fact, any ES module you have in node_modules) in the remix config and everything should work…

This is essentially a port of next-transpile-modules but for Remix. Judging by the number of downloads of that package (and my anecdotal experience) it’s a very common requirement.

@kentcdodds What are your thoughts on supporting something like a transpileModules option?

@kentcdodds That error cause by the dynamic import will be fixed soon, I have a PR out that will allow for dynamic imports to be processed correctly.

Also been giving this some thought in general and one thing we could do is add some sort of “allowList” to the config that would bundle those node modules with the server build, removing the concern of cjs vs esm the same as a browser build for those specific modules.

I created a workaround that uses @kentcdodds’s method https://github.com/abc3354/remix-esm-workaround

It works with a custom hook + a react context and does not need a lot of modification to the code ! It also runs react-flow 😄

I have the same problem with react-markdown. Version downgrade didn’t work for me.

But also other packages like @apollo/client are suggesting me to add it to serverDependenciesToBundle in remix.config.js. If i do so, more and more packages show up – additionally to the ones already added to serverDependenciesToBundle.

@Girish21 thanks a lot for that serverDependenciesToBundle option. I was looking for something like that and couldn’t find anything! … I understand Node’s limitations with CJS/ESM (one of my personal packages is still in CJS because ESLint, Prettier and similar packages haven’t migrated yet to ESM), but as I mentioned in my previous comment, is kinda weird to have to update a config in remix.config.js to make ESM work properly when the idea of Remix is to use standards. Don’t take this as a critique to Remix itself, but just as an observation of something that’s “unintuitive” about it. Hope the ecosystem keeps evolving so we don’t have to deal with this kind of things in the future.

@edmundhung & @nialldbarber it worked for me after I downgraded react-markdown to v6.0.3

Just hit the same issue with react-markdown which is also developed by the same team.

I was thinking about trying the solution from Kent like this:

react-markdown.js

module.exports = {
  ReactMarkdown: () => import('react-markdown'),
};

/app/routes/index.tsx

import { ReactMarkdown as ReactMarkdownComponent } from '../../react-markdown.js';

const ReactMarkdown = React.lazy(ReactMarkdownComponent);

export default function Index() {
  let data = useRouteData();

  return (
      <ReactMarkdown>{'# test'}</ReactMarkdown>
  );
}

And I end up seeing Error: ReactDOMServer does not yet support Suspense. which seems valid I think…🤔

I also had the issue with react-markdown and the workaround of @RazvanRauta worked for me (#109 (comment)). But I wonder why the newer versions of the package won’t, even when adding it to remix.config.js (serverDependenciesToBundle: [“react-markdown”])? See remix.run/docs/en/v1/pages/gotchas#importing-esm-packages.

Do you have the code that work with react-markdown? I can’t make it works

Don’t know about react-markdown but I got micromark (with GitHub Flavoured Markdown) to work by adding each of the packages there was an error for and ended up with this:

{
  serverDependenciesToBundle: [
    "character-entities",
    "decode-named-character-reference",
    "micromark",
    "micromark-core-commonmark",
    "micromark-extension-frontmatter",
    "micromark-extension-gfm",
    "micromark-extension-gfm-autolink-literal",
    "micromark-extension-gfm-footnote",
    "micromark-extension-gfm-strikethrough",
    "micromark-extension-gfm-table",
    "micromark-extension-gfm-tagfilter",
    "micromark-extension-gfm-task-list-item",
    "micromark-extension-mdx-expression",
    "micromark-extension-mdx-jsx",
    "micromark-extension-mdx-md",
    "micromark-extension-mdxjs",
    "micromark-extension-mdxjs-esm",
    "micromark-factory-destination",
    "micromark-factory-label",
    "micromark-factory-mdx-expression",
    "micromark-factory-space",
    "micromark-factory-title",
    "micromark-factory-whitespace",
    "micromark-util-character",
    "micromark-util-chunked",
    "micromark-util-classify-character",
    "micromark-util-combine-extensions",
    "micromark-util-decode-numeric-character-reference",
    "micromark-util-decode-string",
    "micromark-util-encode",
    "micromark-util-events-to-acorn",
    "micromark-util-html-tag-name",
    "micromark-util-normalize-identifier",
    "micromark-util-resolve-all",
    "micromark-util-sanitize-uri",
    "micromark-util-subtokenize",
    "micromark-util-symbol",
    "micromark-util-types",
  ],
}

And now it works like a charm both in the browser and on the server!

I also had the issue with react-markdown and the workaround of @RazvanRauta worked for me (https://github.com/remix-run/remix/issues/109#issuecomment-996203134). But I wonder why the newer versions of the package won’t, even when adding it to remix.config.js (serverDependenciesToBundle: ["react-markdown"])? See https://remix.run/docs/en/v1/pages/gotchas#importing-esm-packages.

Circling back around. I’m pretty sure this is a workaround that’ll work for most folks (kinda depends on how you deploy and stuff). Basically it’s important to know that:

  1. The only way for CJS modules to import ESM modules is via dynamic imports
  2. Currently remix compiles all dynamic imports to CJS requires and that will fail if the module being required is ESM.

So the solution is to create a CJS module that does the dynamic import and is not compiled by remix.

One approach to this would be to create a CJS module (so it can be required by the compiled remix code) at the root of your repo (so it’s not compiled by remix code) and put all the dynamic imports in there. Then your remix code can reference that module instead.

So, for example:

// esm-modules.js

module.exports = {
  getUnistUtilVisit: async () => import('unist-util-visit'),
  // other modules would be imported here.
}
// app/utils/compile-mdx.server.ts

import {getUnistUtilVisit} from '../../esm-modules'

async function compileMdx() {
  const {default: visit} = await getUnistUtilVisit()
  // ... etc.
}

I haven’t tested that exactly, but I’m 99% that it’ll work (perhaps with some small modifications). Note that mdx-bundler uses xdm which is a native ESM module package via dynamic imports (checkout line 66 here: https://unpkg.com/browse/mdx-bundler@5.2.1/dist/index.js). The reason this works with remix is because remix’s server build tells esbuild to skip compiling files in node_modules so the dynamic import remains in place (note, this only works with the server, on the client everything is compiled).


If remix’s compiler was configured to leave dynamic imports alone, then it would be much simpler. No middle-man esm-modules.js file would be needed and we could use the dynamic import directly:

// app/utils/compile-mdx.server.ts

async function compileMdx() {
  const {default: visit} = await import('unist-util-visit')
  // ... etc.
}

If remix’s compiler was configured to leave all imports alone (not sure what this would take), then we could set "type": "module" in a remix app’s package.json, then it would be even easier:

// app/utils/compile-mdx.server.ts
import {visit} from 'unist-util-visit'

async function compileMdx() {
  // ... etc.
}

For now, we’re stuck with the esm-modules.js file until Remix’s compiler supports either ignoring dynamic imports or supports ignoring all imports (perhaps it could do this automatically if we set "type": "module" in the root package.json?).

Hope that helps!

My work around for now is to downgrade jsdom to 19.0.0 and @types/jsdom to 16.2.15 in my package.json, before they upgraded to parse5 v7, and remove node_modules and package-lock.json before running npm i again, to ensure that parse5 v6 is at the root of my node_modules so that the “serverDependenciesToBundle”-ed rehype-parse module can find it.

This seemed to work. However, a bit later on I realized I needed to install remark-gfm. By the way, a cool trick I’ve found is to run npm run build after adding a dependency, and it’ll list packages to add to “serverDependenciesToBundle”. After you add those packages and you run npm run build again, it’ll detect additional packages (basically doing a breadth first search, one layer at a time). This is faster (O(log n) vs O(n)?) than potentially seeing each package fail individually.

But now I have a new problem, that “serverDependenciesToBundle” seems to ignore “mdast-util-to-markdown”. I put it in remix.config.js:

{
  serverDependenciesToBundle: [
    "bail",
    "ccount",
    "character-entities",
    "comma-separated-tokens",
    "decode-named-character-reference",
    "escape-string-regexp",
    "hast-util-embedded",
    "hast-util-from-parse5",
    "hast-util-has-property",
    "hast-util-is-body-ok-link",
    "hast-util-is-element",
    "hast-util-parse-selector",
    "hast-util-phrasing",
    "hast-util-to-mdast",
    "hast-util-to-text",
    "hast-util-whitespace",
    "hastscript",
    "is-plain-obj",
    "longest-streak",
    "markdown-table",
    "mdast-util-find-and-replace",
    "mdast-util-gfm-autolink-literal",
    "mdast-util-gfm-footnote",
    "mdast-util-gfm-strikethrough",
    "mdast-util-gfm-table",
    "mdast-util-gfm-task-list-item",
    "mdast-util-gfm",
    "mdast-util-phrasing",
    "mdast-util-to-markdown", // <=====
    "mdast-util-to-string",
    "micromark-core-commonmark",
    "micromark-extension-gfm-autolink-literal",
    "micromark-extension-gfm-footnote",
    "micromark-extension-gfm-strikethrough",
    "micromark-extension-gfm-table",
    "micromark-extension-gfm-tagfilter",
    "micromark-extension-gfm-task-list-item",
    "micromark-extension-gfm",
    "micromark-factory-destination",
    "micromark-factory-label",
    "micromark-factory-space",
    "micromark-factory-title",
    "micromark-factory-whitespace",
    "micromark-util-character",
    "micromark-util-chunked",
    "micromark-util-classify-character",
    "micromark-util-combine-extensions",
    "micromark-util-decode-numeric-character-reference",
    "micromark-util-decode-string",
    "micromark-util-encode",
    "micromark-util-html-tag-name",
    "micromark-util-normalize-identifier",
    "micromark-util-resolve-all",
    "micromark-util-sanitize-uri",
    "micromark-util-subtokenize",
    "parse5",
    "property-information",
    "rehype-minify-whitespace",
    "rehype-parse",
    "rehype-remark",
    "remark-gfm",
    "remark-stringify",
    "space-separated-tokens",
    "trim-trailing-lines",
    "trough",
    "unified",
    "unist-util-find-after",
    "unist-util-is",
    "unist-util-stringify-position",
    "unist-util-visit-parents",
    "unist-util-visit",
    "vfile-location",
    "vfile-message",
    "vfile",
    "web-namespaces",
    "zwitch",
  ],
}

But even after rm -rf build node_modules package-lock.json && npm i, I still get the following error on npm run build:

mdast-util-to-markdown is possibly an ESM only package and should be bundled with "serverDependenciesToBundle" in remix.config.js.

And the following on npm run dev:


.../build/index.js:8360
var import_association = require("mdast-util-to-markdown/lib/util/association.js"), import_container_flow = require("mdast-util-to-markdown/lib/util/container-flow.js"), import_indent_lines = require("mdast-util-to-markdown/lib/util/indent-lines.js"), import_safe = require("mdast-util-to-markdown/lib/util/safe.js"), import_track = require("mdast-util-to-markdown/lib/util/track.js");
                         ^
Error [ERR_REQUIRE_ESM]: require() of ES Module .../node_modules/mdast-util-to-markdown/lib/util/association.js from .../build/index.js not supported.
Instead change the require of association.js in .../build/index.js to a dynamic import() which is available in all CommonJS modules.
    at Object.<anonymous> (.../build/index.js:8360:26)
    at Server.<anonymous> (.../build/server.js:44855:3)
    at Object.onceWrapper (node:events:641:28)
    at Server.emit (node:events:527:28)
    at emitListeningNT (node:net:1448:10)
ERROR: "dev:server" exited with 1.

Update: Bundling all packages using a blacklist instead of a whitelist fixed my issue.

@Chensokheng All I did was to stick to an older version of react-markdown (6.0.3) as suggested by another commenter (https://github.com/remix-run/remix/issues/109#issuecomment-996203134)

@codejet

I also had the issue with react-markdown and the workaround of @RazvanRauta worked for me (https://github.com/remix-run/remix/issues/109#issuecomment-996203134). But I wonder why the newer versions of the package won’t, even when adding it to remix.config.js (serverDependenciesToBundle: [“react-markdown”])? See https://remix.run/docs/en/v1/pages/gotchas#importing-esm-packages.

Do you have the code that work with react-markdown? I can’t make it works

@lukeshiru Remix already exports ESM modules for the browser bundle. The problem is with the server bundle. Still, Node’s ESM vs CJS situation is a bit iffy, and some of the dependencies still don’t support native ESM yet. Remix has a workaround, though; you can list the ESM only packages to be compiled to CJS during the build for the server bundle using serverdependenciestobundle. You can also take a look at importing ESM packages

+1 I have the same issue with React-Flow. I’ve tried multiple workarounds but unsuccessful so far…

https://github.com/wbkd/react-flow/issues/1953

Finally got things working with Netlify deploy. See netlify/zip-it-and-ship-it#869 for the solution.

One thing to note is that I did not need to use the esm-modules.tsx trick (neither for Remix Server nor for Netlify), just used await import(...) inside of an async function for all the ESM modules I needed to import.

Hitting this same issue … our internal UI library is ESM only, so no option to use the CJS version. Is there any kind of option available at the moment to allow certain dependencies in node_modules to be transpiled to CJS?