Skip to content

require.main can go into an infinite loop #10478

@IvanVergiliev

Description

@IvanVergiliev

🐛 Bug Report

Calling require.main can go into an infinite loop in certain situations, causing tests to hang forever. I'd be happy to PR a fix, but there are at least 2 different ways to approach this and I don't know which one is better from an overall design point of view.

To Reproduce

Steps to reproduce the behavior:

  • I've added an e2e/ test that reproduces the behaviour in my clone (linked below). tl;dr the setup is the following:
  1. module requires mock
  2. mock requires module, causing a cyclic import.
  3. jest.mock('./mock')
  4. Call require.main from within module.

The last step causes the require.main property getter to go into an infinite loop because of a weird interaction between require.parent and moduleRegistry.

Expected behavior

I would expect the test to either fail with a clear error message ("import loop detected" or similar), or succeed.

Link to repl or repo (highly encouraged)

e2e test that reproduces the behavior. Running the test causes an infinite loop.

I root caused this to a weird interaction between require.parent and moduleRegistry when mocking a module. It's a bit tricky, but the situation is roughly as follows:

  1. When mock is first loaded, it goes through Runtime._generateMock since I'm mocking it.
  2. Runtime._generateMock clears the _moduleRegistry and _mockRegistry instance variables for the duration of the mock generation.
  3. At this point, require.parent, which is defined as follows:
    Object.defineProperty(localModule, 'parent', {
      enumerable: true,
      get() {
        const key = from || '';
        return moduleRegistry.get(key) || null;
      },
    });

returns null for mock.ts, since moduleRegistry is empty. However, from === module.ts, which is relevant at a later point.
4. Since mock requires module, and moduleRegistry is empty at this point, jest starts loading module again, and sets moduleRegistry['module'] accordingly.
5. When module tries to call require.main, we now have a cyclic dependency, and the while loop that determines main goes into an infinite loop. This is because moduleRegistry.get(key) above is now set, since we've loaded module.ts and populated it in the moduleRegistry.

I'm attaching a diagram where I've tried to explain this visually.

Jest infinite loop

Here are some ways I see to fix this:

  1. Keep a Set of visited moduleIds when computing require.main, and throw if we detect a loop.
  2. Set require.parent directly instead of using a getter. This will ensure that it always has the same value and doesn't change when the underlying moduleRegistry changes.

I tried both of these and they both avoid the infinite loops. 2 is probably nicer since it leads to a successful test, but I don't know if it could have adverse side effects. I saw that the original PR introducing module.parent has a discussion about the getter.

envinfo

I don't think the issue is env-specific since I managed to root-cause it to specific code, but posting for completeness:

  System:
    OS: macOS 10.15.6
  Binaries:
    Node: 10.15.3 - ~/.nvm/versions/node/v10.15.3/bin/node
    Yarn: 1.22.5 - /usr/local/bin/yarn
    npm: 6.10.3 - ~/.nvm/versions/node/v10.15.3/bin/npm
  npmPackages:
    jest: ^25.5.4 => 25.5.4

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions