body-parser icon indicating copy to clipboard operation
body-parser copied to clipboard

no named exports for ESM projects

Open dynst opened this issue 1 month ago • 2 comments

Environment information

Version:

2.2.0

Platform:

Node.js version:

20.19.2

Any other relevant information:

What steps will reproduce the bug?

import { json } from 'body-parser';
 SyntaxError: The requested module 'body-parser' does not provide an export named 'json'

Named exports are found by static analysis:

https://nodejs.org/en/learn/modules/publishing-a-package

Node.js detects the named exports in CJS via static analysis that look for certain patterns, which the example above evades. To make the named exports detectable, do this:

defineProperty is a supported syntax, but a get() method can't be a require() call or a call to any other function, it can only return a simple 'identifier or member expression.'

https://github.com/expressjs/body-parser/blob/d96b63da8d7445de317736471633bac83ec76cbb/index.js#L30-L34

https://github.com/nodejs/cjs-module-lexer/blob/main/README.md#getter-exports-parsing

To avoid matching getters that have side effects, any getter for an export name that does not support the forms above will opt-out of the getter matching:

dynst avatar Nov 24 '25 23:11 dynst

I was able to confirm the bug. My question is: is there anything we can do on our side, or is this already an issue for Node.js to handle?

bjohansebas avatar Nov 25 '25 02:11 bjohansebas

Not ideal, but as a workaround maybe changing the import statement might solve the issue:

import bodyParser from 'body-parser';

app.use(bodyParser.json());
import pkg from 'body-parser';
const { json } = pkg;

app.use(json());

I was able to confirm the bug. My question is: is there anything we can do on our side, or is this already an issue for Node.js to handle?

Probably we can report this to Node... @dynst do you want to open an issue in https://github.com/nodejs/node/issues/new/choose and mention our current discussion for context?

UlisesGascon avatar Nov 25 '25 10:11 UlisesGascon

@UlisesGascon This is not an issue on the Node.js side. As I understand it, this behavior is intentional and already documented. As @dynst mentioned, Node.js explicitly opts out of matching getters that may have side effects:

To avoid matching getters that have side effects, any getter for an export name that does not support the forms above will opt-out of the getter matching

Raising an issue on the Node.js side will not help as the cjs-module-lexer readme clearly states that

Detection patterns for this project are frozen. This is because adding any new export detection patterns would result in fragmented backwards-compatibility. Specifically, it would be very difficult to figure out why an ES module named export for CommonJS might work in newer Node.js versions but not older versions. This problem would only be discovered downstream of module authors, with the fix for module authors being to then have to understand which patterns in this project provide full backwards-compatibily. Rather, by fully freezing the detected patterns, if it works in any Node.js version it will work in any other. Build tools can also reliably treat the supported syntax for this project as a part of their output target for ensuring syntax support.

What can we do?

We can fix this by directly exporting the individual parsers, but we would lose the benefit of lazy-loading them. Here's what the change would look like (without jsdoc):

'use strict'

exports = module.exports = bodyParser

exports.json = require('./lib/types/json')
exports.raw = require('./lib/types/raw')
exports.text = require('./lib/types/text')
exports.urlencoded = require('./lib/types/urlencoded')

function bodyParser () {
  throw new Error('The bodyParser() generic has been split into individual middleware to use instead.')
}

Trade-offs

The current implementation uses Object.defineProperty with getters to support lazy-loading, which may help in reducing memory usage and startup time (I have no numbers on the impact). However, this pattern is incompatible with Node.js's cjs-module-lexer, which only detects getters that return a simple identifier or member expression not function calls like require().

Why this change makes sense

  1. Minimal performance impact:
    • All parsers share the same utilities (lib/read.js, lib/utils.js), so most code is loaded anyway
    • Only the urlencoded parser has a significant external dependency (qs). In typical express apps, qs is already in the require cache since Express uses it for query string parsing so it should not be a problem
    • Node.js's require cache makes subsequent loads essentially free
  2. ESM interop: Direct exports enable named imports: import { json } from 'body-parser'
  3. Tree-shaking: Bundlers can now perform module-level pruning, ensuring unused parsers are not included in the bundle.

The lazy-loading optimization is more valuable when body-parser is used standalone, but in a typical express app, the benefits of static analysis outweigh the minimal startup time savings in my opinion.

Other Option

We could also add the exports field to our package.json and export each parser as subpath:

{
  "exports": {
    ".": "./index.js",
    "./json": "./lib/types/json.js",
    "./raw": "./lib/types/raw.js",
    "./text": "./lib/types/text.js",
    "./urlencoded": "./lib/types/urlencoded.js"
  }
}

This would allow users to import individual parsers from subpaths:

// CommonJS
const json = require('body-parser/json')

// ESM
import json from 'body-parser/json'

This change is fully backward compatible and could also be combined with the change mentioned above. This way the main import trades lazy loading for better static analysis while users who care about startup time can rely on subpath exports to load only the parsers they need.

What do you think @UlisesGascon @jonchurch?

Phillip9587 avatar Dec 16 '25 21:12 Phillip9587