How to handle peer dependencies of a JS library with a single export index file in Jest?

55 Views Asked by At

Apology for the winding question title which might not make sense at first sight. Let me explain.

I have a library package named foo-bar-ui for some shared UI components. All the components are exported from a single index file.

// index.js
export * from './lib/ComponentA';
export * from './lib/ComponentB';
export * from './lib/ComponentC';

And in the application package, I can happily import things from this library in below format.

import { ComponentA, ComponentB } from 'foo-bar-ui';

And one day I added a new third party dependency react-xyz for ComponentC. It's only used by ComponentC, hence I declared it as a peer dependency in the library package. Any client application consuming this library can install react-xyz on its own if it needs to use ComponentC.

However, my current application only consumes ComponentA and ComponentB. Webpack bundling has no complaint at all as it does tree-shaking, which discards ComponentC completely hence there is no warning about missing react-xyz. But Jest complains on that. Below execution failure is observed in client applications consuming the library package, who did not install react-xyz as they do not use ComponentC.

Test suite failed to run

    Cannot find module 'react-xyz' from 'node_modules/foo-bar-ui/lib/ComponentC.js'

    Require stack:
      node_modules/foo-bar-ui/lib/ComponentC.js
      node_modules/foo-bar-ui/index.js'

The immediate solution seems to be exporting each individual components separately, like what lodash-es does, e.g. import isNil from 'lodash-es/isNil';. And Node.js subpath exports seems to be the tool doing the job. But I found it problematic with TypeScript, ESLint and Jest all together with module resolution issues.

What's the ultimate recommended approach to solve this problem? It seems straightforward but oddly I could not land on any easy and succinct solution right away.

2

There are 2 best solutions below

1
Teneff On

It's best if you cover both of the scenarios, when the library is installed and when it's not. And jest.mock supports "virtual" modules, allowing you to mock non-existent modules, so your test can look like this:

describe('ComponentC', () => {
  describe('with react-xyz', () => {
    beforeAll(() => {
      jest.mock('react-xyz', () => {
        // return some implementation
      }, { virtual: true });
    })
  });

  describe('without react-xyz', () => {
    beforeAll(() => {
      jest.mock('react-xyz', () => {
        // throw new Error('Cannot find module')
      }, { virtual: true });
    })
  });
})
0
Ruifeng Ma On

For the current being, a temporary solution I opted in is to stop using the single index and directly take the exports from the individual components.

So in a client application, instead of

import { ComponentA, ComponentB } from 'foo-bar-ui';

I would have

import { ComponentA } from 'foo-bar-ui/dist/features/componentA';
import { ComponentB } from 'foo-bar-ui/dist/features/componentB';

This works but it's just that the import statements are not as neat as before.

We could try to eliminate the /dist/features portion. Subpath exports could be of help, but I found it to be not working well with TypeScript, ESLint and Jest etc.