Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support . in exports field #11919

Merged
merged 8 commits into from Oct 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
4 changes: 0 additions & 4 deletions e2e/resolve-conditions/package.json
Expand Up @@ -6,13 +6,9 @@
"mjs",
"json"
],
"resolver": "<rootDir>/resolver.js",
Copy link
Member Author

@SimenB SimenB Oct 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need this anymore as this just tests that the condition node, browser etc of the main entry point are supported, and since we add support to jest itself for the entry point (and the things we test don't have a main), the rest works.

"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

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.

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(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we now wrap the function, it's not actually passed as is through, so I had to make some changes so packageFilter is actually called and assert on that

'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