javascriptnode.jsnpmes6-modulesnpm-link

Why importing from a linked local ES modules package using the package name works with "main" property but fails with "module"


Question

Why does importing from a linked local NPM pacakage (built as an ES module) using the pacakge name works when the pacakge.json has the "main" property, but fails when it has the "module" property?

Setup

More spicifically, if we have the following setup:

  1. exporter: A Node.js package which is an ES module that exports something (e.g., using export default).
  2. importer: A Node.js module that tries to import what exporter exports, using import something from 'exporter'.
  3. Using npm link to locally link exporter to importer.

Then:

Why is that? I guess I'm doing something wrong?

Code

exporter

|- webpack.config.js
|- package.json
|- /src
  |- index.js
|- /dist
  |- bundle.js

webpack.config.js:

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

const __dirname = path.dirname(fileURLToPath(import.meta.url));

export default {
  mode: "development",     
  entry: "./src/index.js",
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
    library: {
      type: "module",
    },
  },
  experiments: {
    outputModule: true,
  },
};

package.json:

{
  "name": "exporter",
  "version": "1.0.0",
  "main": "dist/bundle.js", <-- *** NOTE THIS LINE ***
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^5.51.1",
    "webpack-cli": "^4.8.0"
  },
  "type": "module"
}

index.js:

function util() {
  return "I'm a util!";
}
export default util;

importer

|- package.json
|- /src
  |- index.js

package.json

{
  "name": "importer",
  "version": "1.0.0",
  "type": "module"
}

index.js

import util from 'exporter';

console.log(util());

Then:

  1. Linking:
⚡  cd exporter
⚡  npm link
⚡  cd importer
⚡  npm link exporter
  1. Executing:
⚡  node importer.js 
I'm a util!

However, if exporter's package.json is changed to:

{
  "name": "exporter",
  "version": "1.0.0",
  "module": "dist/bundle.js", <-- *** NOTE THIS LINE ***
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^5.51.1",
    "webpack-cli": "^4.8.0"
  },
  "type": "module"
}

Then:

  1. Executing:
⚡  node importer.js 

Fails:

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'importer\node_modules\exporter\' imported from importer\src\index.js

But Why?


Solution

  • When resolving a node_modules package, Node.js first checks if there's a "exports" field in the package.json, if there's none it looks for a "main" field, and if it's. also not there, it checks if there's a file named index.js – although this behavior is deprecated and may be removed in a later version of Node.js, I would advise against relying on it and always specify "exports" and/or "main" fields. You can check out Package entry points section of Node.js docs to get more info on that.

    "module" is simply not a field Node.js uses, it's used by some other tools so it's certainly okay to have it defined in your package.json, but you should also have "main" and/or "exports" fields. Node.js will use the file extension to determine if the file is an ES module (dist/bundle.js is using .js as extension and you have "type": "module" in your package.json, so you're all set).