Skip to content

Commit

Permalink
feat: support . in exports field (#11919)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed Oct 17, 2021
1 parent b5aec03 commit bc3c921
Show file tree
Hide file tree
Showing 26 changed files with 140 additions and 74 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,7 @@
### Features

- `[jest-config]` Add `testEnvironmentOptions.html` to apply to jsdom input ([11950](https://github.com/facebook/jest/pull/11950))
- `[jest-resolver]` Support default export (`.`) in `exports` field _if_ `main` is missing ([#11919](https://github.com/facebook/jest/pull/11919))

### Fixes

Expand Down
5 changes: 0 additions & 5 deletions e2e/__tests__/resolveConditions.test.ts
Expand Up @@ -7,15 +7,10 @@

import {resolve} from 'path';
import {onNodeVersions} from '@jest/test-utils';
import {runYarnInstall} from '../Utils';
import runJest from '../runJest';

const dir = resolve(__dirname, '..', 'resolve-conditions');

beforeAll(() => {
runYarnInstall(dir);
});

// The versions where vm.Module exists and commonjs with "exports" is not broken
onNodeVersions('>=12.16.0', () => {
test('resolves package exports correctly with custom resolver', () => {
Expand Down
2 changes: 1 addition & 1 deletion e2e/resolve-conditions/__tests__/browser.test.mjs
Expand Up @@ -7,7 +7,7 @@
* @jest-environment <rootDir>/browser-env.js
*/

import {fn} from '../fake-dual-dep';
import {fn} from 'fake-dual-dep';

test('returns correct message', () => {
expect(fn()).toEqual('hello from browser');
Expand Down
2 changes: 1 addition & 1 deletion e2e/resolve-conditions/__tests__/node.test.mjs
Expand Up @@ -7,7 +7,7 @@
* @jest-environment <rootDir>/node-env.js
*/

import {fn} from '../fake-dual-dep';
import {fn} from 'fake-dual-dep';

test('returns correct message', () => {
expect(fn()).toEqual('hello from node');
Expand Down
2 changes: 1 addition & 1 deletion e2e/resolve-conditions/__tests__/resolveCjs.test.cjs
Expand Up @@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

const {fn} = require('../fake-dep');
const {fn} = require('fake-dep');

test('returns correct message', () => {
expect(fn()).toEqual('hello from CJS');
Expand Down
2 changes: 1 addition & 1 deletion e2e/resolve-conditions/__tests__/resolveEsm.test.mjs
Expand Up @@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

import {fn} from '../fake-dep';
import {fn} from 'fake-dep';

test('returns correct message', () => {
expect(fn()).toEqual('hello from ESM');
Expand Down
File renamed without changes.
File renamed without changes.
4 changes: 0 additions & 4 deletions e2e/resolve-conditions/package.json
Expand Up @@ -6,13 +6,9 @@
"mjs",
"json"
],
"resolver": "<rootDir>/resolver.js",
"testMatch": [
"<rootDir>/**/*.test.*"
],
"transform": {}
},
"dependencies": {
"resolve.exports": "^1.1.0"
}
}
30 changes: 0 additions & 30 deletions e2e/resolve-conditions/resolver.js

This file was deleted.

21 changes: 0 additions & 21 deletions e2e/resolve-conditions/yarn.lock

This file was deleted.

1 change: 1 addition & 0 deletions packages/jest-resolve/package.json
Expand Up @@ -22,6 +22,7 @@
"jest-util": "^27.2.5",
"jest-validate": "^27.2.5",
"resolve": "^1.20.0",
"resolve.exports": "^1.1.0",
"slash": "^3.0.0"
},
"devDependencies": {
Expand Down
Empty file.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/jest-resolve/src/__mocks__/package.json
@@ -0,0 +1,6 @@
{
"name": "__mocks__",
"version": "1.0.0",
"dependencies": {
}
}
51 changes: 43 additions & 8 deletions packages/jest-resolve/src/__tests__/resolve.test.ts
Expand Up @@ -126,26 +126,61 @@ describe('findNodeModule', () => {
});
});

it('passes packageFilter to the resolve module when using the default resolver', () => {
it('wraps passed packageFilter to the resolve module when using the default resolver', () => {
const packageFilter = jest.fn();

// A resolver that delegates to defaultResolver with a packageFilter implementation
userResolver.mockImplementation((request, opts) =>
opts.defaultResolver(request, {...opts, packageFilter}),
);

Resolver.findNodeModule('test', {
basedir: '/',
Resolver.findNodeModule('./test', {
basedir: path.resolve(__dirname, '../__mocks__/'),
resolver: require.resolve('../__mocks__/userResolver'),
});

expect(mockResolveSync).toHaveBeenCalledWith(
'test',
expect.objectContaining({
packageFilter,
}),
expect(packageFilter).toHaveBeenCalledWith(
expect.objectContaining({name: '__mocks__'}),
expect.any(String),
);
});

describe('conditions', () => {
const conditionsRoot = path.resolve(__dirname, '../__mocks__/conditions');

test('resolves without exports, just main', () => {
const result = Resolver.findNodeModule('main', {
basedir: conditionsRoot,
conditions: ['require'],
});

expect(result).toEqual(
path.resolve(conditionsRoot, './node_modules/main/file.js'),
);
});

test('resolves with import', () => {
const result = Resolver.findNodeModule('import', {
basedir: conditionsRoot,
conditions: ['import'],
});

expect(result).toEqual(
path.resolve(conditionsRoot, './node_modules/import/file.js'),
);
});

test('resolves with require', () => {
const result = Resolver.findNodeModule('require', {
basedir: conditionsRoot,
conditions: ['require'],
});

expect(result).toEqual(
path.resolve(conditionsRoot, './node_modules/require/file.js'),
);
});
});
});

describe('resolveModule', () => {
Expand Down
63 changes: 61 additions & 2 deletions packages/jest-resolve/src/defaultResolver.ts
Expand Up @@ -5,8 +5,13 @@
* LICENSE file in the root directory of this source tree.
*/

import {resolve} from 'path';
import pnpResolver from 'jest-pnp-resolver';
import {Opts as ResolveOpts, sync as resolveSync} from 'resolve';
import {sync as resolveSync} from 'resolve';
import {
Options as ResolveExportsOptions,
resolve as resolveExports,
} from 'resolve.exports';
import type {Config} from '@jest/types';
import {
PkgJson,
Expand All @@ -16,13 +21,19 @@ import {
realpathSync,
} from './fileWalkers';

interface ResolverOptions extends ResolveOpts {
// copy from `resolve`'s types so we don't have their types in our definition
// files
interface ResolverOptions {
basedir: Config.Path;
browser?: boolean;
conditions?: Array<string>;
defaultResolver: typeof defaultResolver;
extensions?: Array<string>;
moduleDirectory?: Array<string>;
paths?: Array<Config.Path>;
rootDir?: Config.Path;
packageFilter?: (pkg: PkgJson, dir: string) => PkgJson;
pathFilter?: (pkg: PkgJson, path: string, relativePath: string) => string;
}

// https://github.com/facebook/jest/pull/10617
Expand All @@ -48,6 +59,10 @@ export default function defaultResolver(
...options,
isDirectory,
isFile,
packageFilter: createPackageFilter(
options.conditions,
options.packageFilter,
),
preserveSymlinks: false,
readPackageSync,
realpathSync,
Expand All @@ -65,3 +80,47 @@ export default function defaultResolver(
function readPackageSync(_: unknown, file: Config.Path): PkgJson {
return readPackageCached(file);
}

function createPackageFilter(
conditions?: Array<string>,
userFilter?: ResolverOptions['packageFilter'],
): ResolverOptions['packageFilter'] {
function attemptExportsFallback(pkg: PkgJson) {
const options: ResolveExportsOptions = conditions
? {conditions, unsafe: true}
: // no conditions were passed - let's assume this is Jest internal and it should be `require`
{browser: false, require: true};

try {
return resolveExports(pkg, '.', options);
} catch {
return undefined;
}
}

return function packageFilter(pkg, packageDir) {
let filteredPkg = pkg;

if (userFilter) {
filteredPkg = userFilter(filteredPkg, packageDir);
}

if (filteredPkg.main != null) {
return filteredPkg;
}

const indexInRoot = resolve(packageDir, './index.js');

// if the module contains an `index.js` file in root, `resolve` will request
// that if there is no `main`. Since we don't wanna break that, add this
// check
if (isFile(indexInRoot)) {
return filteredPkg;
}

return {
...filteredPkg,
main: attemptExportsFallback(filteredPkg),
};
};
}
8 changes: 8 additions & 0 deletions yarn.lock
Expand Up @@ -13026,6 +13026,7 @@ fsevents@^1.2.7:
jest-util: ^27.2.5
jest-validate: ^27.2.5
resolve: ^1.20.0
resolve.exports: ^1.1.0
slash: ^3.0.0
languageName: unknown
linkType: soft
Expand Down Expand Up @@ -18944,6 +18945,13 @@ react-native@0.64.0:
languageName: node
linkType: hard

"resolve.exports@npm:^1.1.0":
version: 1.1.0
resolution: "resolve.exports@npm:1.1.0"
checksum: d04d2ce651fac14fe6ba13b377690f790cbbe91e6211b8fbec97ee08282e278875c74073a9b6243143a64e33d95eefb479e1dd4965664edc73b28b712100b36c
languageName: node
linkType: hard

"resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.15.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.3.2":
version: 1.20.0
resolution: "resolve@npm:1.20.0"
Expand Down

0 comments on commit bc3c921

Please sign in to comment.