developit / microbundle

📦 Zero-configuration bundler for tiny modules.
https://npm.im/microbundle
MIT License
8.04k stars 362 forks source link

Issues with documentation regarding node resolution algorithm #902

Closed hanayashiki closed 2 years ago

hanayashiki commented 2 years ago

Environment:

node --version
v14.17.5

First, following the README here (https://github.com/developit/microbundle/blob/76272fdf039d4db6a5c90ba76cb11cf561062e27/README.md)

We directly use the first example as package.json:

{
  "name": "foo",                     // your package name
  "type": "module",
  "source": "src/foo.js",            // your source code
  "exports": "./dist/foo.modern.js", // where to generate the modern bundle (see below)
  "main": "./dist/foo.cjs",          // where to generate the CommonJS bundle
  "module": "./dist/foo.module.js",  // where to generate the ESM bundle
  "unpkg": "./dist/foo.umd.js",      // where to generate the UMD bundle (also aliased as "umd:main")
  "scripts": {
    "build": "microbundle",          // compiles "source" to "main"/"module"/"unpkg"
    "dev": "microbundle watch"       // re-build when source files change
  }
}

And we have the following architecture of directory after build:

image

This doesn't work for node if we import the module using commonjs.

const foo = require('foo');

console.log({ foo });
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /xxx.../foo.modern.js

This behavior is expected in nodejs 16 documentation: https://nodejs.org/api/modules.html#folders-as-modules

As you might already know, node doesn't respect "module" field. Node will find the 'main' field first. But if there's 'export' field, that field overrides 'main' since v12.16.0(https://nodejs.org/api/packages.html#packages_exports). So, node can only correctly resolves the "cjs" when using require if it is an older version of node.

This will affect a lot of node users (but not bundler users, since most bundlers use a different algorithm that implements the "module" field), since they can't correctly import the module using 'require', which is a default in nodejs and transpiled typescript code.

The correct documentation should use conditional exports so that both older version and newer version can work with that package.json:

  "exports": {
    "import": "./dist/foo.modern.js",
    "require": "./dist/foo.cjs"
  },

In this case, according to https://nodejs.org/api/packages.html#conditional-exports, newer node will look at exports field and correctly find './dist/foo.cjs' from exports.require if using commonjs syntax, while older node will find that cjs from main field.

developit commented 2 years ago

Good catch! This needs to be fixed in the readme.