Skip to content

[Bug]: require() applies the node export condition in the jsdom environment #16476

Open

Description

@overbalance

Disclosure: I found the issue while refactoring OpenTelemetry package.json files, Claude designed and ran the repro

Version

30.5.2 (also 30.0.0; 29.7.0 is not affected)

Steps to reproduce

  1. Create a local package pkg/package.json:

    {
      "name": "pkg",
      "version": "1.0.0",
      "exports": {
        "node": "./node.js",
        "default": "./browser.js"
      }
    }

    with pkg/node.js containing module.exports = 'node'; and pkg/browser.js containing module.exports = 'browser';.

  2. Add it as a dependency ("pkg": "file:./pkg") and install jest@30 and jest-environment-jsdom@30.

  3. Add test/jsdom.test.js:

    /**
     * @jest-environment jsdom
     */
    test('require() under jsdom does not apply the node condition', () => {
      expect(require('pkg')).toBe('browser');
    });
  4. Run jest.

Expected behavior

The test passes. jsdom's export conditions are ['browser'], so require('pkg') should skip the node branch and resolve ./browser.js. This is what Jest 29.7.0 does.

Actual behavior

Expected: "browser"
Received: "node"

Setting testEnvironmentOptions.customExportConditions: ['browser'] gives the same result, so a jsdom test cannot opt out of the node condition for require().

Additional context

In jest-runtime, the CommonJS condition list always contains node:

this.cjsConditions = [...new Set(['require', ...(supportsSyncEvaluate ? ['module-sync'] : []), 'node', 'default', ...envExportConditions])];
this.esmConditions = [...new Set(['import', 'module-sync', 'default', ...envExportConditions])];

node was added in #15647, which fixed a beta regression after the move to unrs-resolver in #15619 (see #15653). Before that, the list was ['require', 'default', ...envExportConditions], as it still is in 30.0.0-beta.3 and 29.7.0. jest-environment-node already supplies node through its own customExportConditions (['node', 'node-addons']), so the hard-coded entry only changes environments that leave node out, such as jsdom. It also makes CJS and ESM resolve differently in the same environment, since esmConditions has no node.

This affects packages that follow the Node.js recommendation to branch on node and put the universal implementation under default (https://nodejs.org/api/packages.html#conditional-exports). Under jsdom, Jest 30 loads their Node.js implementation.

A possible fix is to drop node from cjsConditions and leave it to the environment's exportConditions(), unless #15647 needed it for a case the environment conditions do not cover.

Environment

System:
  OS: macOS 27.0
  CPU: (10) arm64 Apple M4
Binaries:
  Node: 26.9.0
  npm: 12.1.0
npmPackages:
  jest: ^30.5.2 => 30.5.2

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions