Skip to content

no named exports for ESM projects #666

Closed

Description

@dynst

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.'

body-parser/index.js

Lines 30 to 34 in d96b63d

Object.defineProperty(exports, 'json', {
configurable: true,
enumerable: true,
get: () => require('./lib/types/json')
})

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:

Activity

  1. added theissue type on Nov 24, 2025
  2. bjohansebas commented on Nov 25, 2025

    @bjohansebas
    Member

    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?

  3. UlisesGascon commented on Nov 25, 2025

    @UlisesGascon
    Member

    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?

  4. Phillip9587 commented on Dec 16, 2025

    @Phillip9587
    Member

    @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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions