Using Package Exports to Define Multiple Entrypoints
At work I was recently faced with this error and I was a bit clueless at first what it could be:
ModuleParseError: Module parse failed: Unexpected character ''
A colleague mentioned that they also have seen it while doing some work on ES Modules (ESM). So what was the cause of this error and how did I solve it? TL;DR: Use exports
in package.json
.
#Setup
So in short, the setup was as following:
- Package
frontend
imported a helper function from packageutils
.frontend
is used in client-side code, e.g. React - Package
utils
contains a couple of helper functions, both with and without 3rd-party dependencies. It is written in TypeScript and the output is compiled into adist
folder at the root of the package. webpack
is used forfrontend
to bundle all code
The file inside frontend
imports it like this:
import { convertPath } from "utils"
export const linkHelper = (input) => { return convertPath(input)}
The utils
package contains multiple files, for example two files:
export const convertPath = (path: string) => path.toUpperCase()
import * as path from "path"import * as chokidar from "chokidar"
export const getDirname = (path: string) => path.dirname(path)export const watchDir = (path: string, glob: string) => { return new Promise((resolve) => { chokidar.watch(glob, { cwd: path }).on("ready", () => resolve()) })}
Those get exported in an index.ts
file:
export { convertPath } from "./path"export { getDirname, watchDir } from "./watch"
And referenced in the main
field of package.json
with "main": "dist/index.js"
.
#Solution
The problem with this setup is that webpack
(or probably any other bundler) has problems tree-shaking the Node.js and chokidar
bits from utils
when you only import convertPath
in the frontend
package. This is why one sees the error mentioned at the beginning.
But ESM and/or exports
in package.json
can come to the rescue here! Node.js itself has extensive documentation about Package entry points so for the sake of this small post here’s just the solution on how to fix the problem for the given example.
First, add the exports
to utils:
"exports": { ".": "./dist/index.js", "./*": "./dist/*.js"}
This enables the change in this second step:
import { convertPath } from "utils/convert-path"
export const linkHelper = (input) => { return convertPath(input)}
Notice the different import for convertPath
? This is a new entrypoint you can use the ignore all the stuff that you don’t need. Awesome!