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: pass conditions when resolving modules #11859

Merged
merged 11 commits into from Sep 13, 2021
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 2 additions & 2 deletions e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap
Expand Up @@ -41,7 +41,7 @@ FAIL __tests__/index.js
12 | module.exports = () => 'test';
13 |

at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:566:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:574:17)
at Object.require (index.js:10:1)
`;

Expand Down Expand Up @@ -70,6 +70,6 @@ FAIL __tests__/index.js
12 | module.exports = () => 'test';
13 |

at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:566:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:574:17)
at Object.require (index.js:10:1)
`;
Expand Up @@ -37,6 +37,6 @@ FAIL __tests__/test.js
| ^
9 |

at Resolver.resolveModule (../../packages/jest-resolve/build/resolver.js:318:11)
at Resolver.resolveModule (../../packages/jest-resolve/build/resolver.js:321:11)
at Object.require (index.js:8:18)
`;
35 changes: 35 additions & 0 deletions e2e/__tests__/resolveConditions.test.ts
@@ -0,0 +1,35 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

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 || >=13.7.0', () => {
test('resolves package exports correctly with custom resolver', () => {
// run multiple times to ensure there are noe caching errors
for (let i = 0; i < 5; i++) {
const {exitCode} = runJest(dir, [], {
nodeOptions: '--experimental-vm-modules',
});
try {
expect(exitCode).toBe(0);
} catch (error) {
console.log(`Test failed on iteration ${i + 1}`);
throw error;
}
}
});
});
12 changes: 12 additions & 0 deletions e2e/resolve-conditions/__tests__/resolveCjs.test.cjs
@@ -0,0 +1,12 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

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

test('returns correct message', () => {
expect(fn()).toEqual('hello from CJS');
});
12 changes: 12 additions & 0 deletions e2e/resolve-conditions/__tests__/resolveEsm.test.mjs
@@ -0,0 +1,12 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

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

test('returns correct message', () => {
expect(fn()).toEqual('hello from ESM');
});
10 changes: 10 additions & 0 deletions e2e/resolve-conditions/fake-dep/module.cjs
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

module.exports = {
fn: () => 'hello from CJS',
};
10 changes: 10 additions & 0 deletions e2e/resolve-conditions/fake-dep/module.mjs
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

export function fn() {
return 'hello from ESM'
}
10 changes: 10 additions & 0 deletions e2e/resolve-conditions/fake-dep/package.json
@@ -0,0 +1,10 @@
{
"name": "fake-dep",
"version": "1.0.0",
"exports": {
".": {
"import": "./module.mjs",
"require": "./module.cjs"
}
}
}
18 changes: 18 additions & 0 deletions e2e/resolve-conditions/package.json
@@ -0,0 +1,18 @@
{
"jest": {
"moduleFileExtensions": [
"js",
"cjs",
"mjs",
"json"
],
"resolver": "<rootDir>/resolver.js",
"testMatch": [
"<rootDir>/**/*.test.*"
],
"transform": {}
},
"dependencies": {
"resolve.exports": "^1.0.2"
}
}
34 changes: 34 additions & 0 deletions e2e/resolve-conditions/resolver.js
@@ -0,0 +1,34 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

const {resolve: resolveExports} = require('resolve.exports');

module.exports = (path, options) => {
return options.defaultResolver(path, {
...options,
pathFilter: options.conditions
? createPathFilter(options.conditions)
: undefined,
});
};

function createPathFilter(conditions) {
return function pathFilter(pkg, _path, relativePath) {
// this `index` thing can backfire, but `resolve` adds it: https://github.com/browserify/resolve/blob/f1b51848ecb7f56f77bfb823511d032489a13eab/lib/sync.js#L192
const path = relativePath === 'index' ? '.' : relativePath;

return (
resolveExports(pkg, path, {
conditions,
// `resolve.exports adds `import` unless `require` is `false`, so let's add this ugly thing
require: !conditions.includes('import'),
}) || relativePath
);
};
}
21 changes: 21 additions & 0 deletions e2e/resolve-conditions/yarn.lock
@@ -0,0 +1,21 @@
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!

__metadata:
version: 4
cacheKey: 7

"resolve.exports@npm:^1.0.2":
version: 1.0.2
resolution: "resolve.exports@npm:1.0.2"
checksum: 012a46e3ae41c53762abf5b50ea1b4adf2de617bbea1dbc7bf6e609c1ceaedee7782acbc92d443951d5dd0c3a8fb1090ce73285a9ccc24b530e33b5e09ae196f
languageName: node
linkType: hard

"root-workspace-0b6124@workspace:.":
version: 0.0.0-use.local
resolution: "root-workspace-0b6124@workspace:."
dependencies:
resolve.exports: ^1.0.2
languageName: unknown
linkType: soft
2 changes: 2 additions & 0 deletions packages/jest-resolve/src/__tests__/resolve.test.ts
Expand Up @@ -105,6 +105,7 @@ describe('findNodeModule', () => {
const newPath = Resolver.findNodeModule('test', {
basedir: '/',
browser: true,
conditions: ['conditions, woooo'],
extensions: ['js'],
moduleDirectory: ['node_modules'],
paths: ['/something'],
Expand All @@ -116,6 +117,7 @@ describe('findNodeModule', () => {
expect(userResolver.mock.calls[0][1]).toStrictEqual({
basedir: '/',
browser: true,
conditions: ['conditions, woooo'],
defaultResolver,
extensions: ['js'],
moduleDirectory: ['node_modules'],
Expand Down
12 changes: 11 additions & 1 deletion packages/jest-resolve/src/defaultResolver.ts
Expand Up @@ -19,7 +19,16 @@ type ResolverOptions = {
moduleDirectory?: Array<string>;
paths?: Array<Config.Path>;
rootDir?: Config.Path;
packageFilter?: (pkg: any, pkgfile: string) => any;
packageFilter?: (
pkg: Record<string, unknown>,
pkgfile: string,
) => Record<string, unknown>;
pathFilter?: (
pkg: Record<string, unknown>,
path: string,
relativePath: string,
) => string;
conditions?: Array<string>;
};

// https://github.com/facebook/jest/pull/10617
Expand Down Expand Up @@ -48,6 +57,7 @@ export default function defaultResolver(
isFile,
moduleDirectory: options.moduleDirectory,
packageFilter: options.packageFilter,
pathFilter: options.pathFilter,
paths: options.paths,
preserveSymlinks: false,
readPackageSync,
Expand Down
27 changes: 21 additions & 6 deletions packages/jest-resolve/src/resolver.ts
Expand Up @@ -23,6 +23,7 @@ import type {ResolverConfig} from './types';
type FindNodeModuleConfig = {
basedir: Config.Path;
browser?: boolean;
conditions?: Array<string>;
extensions?: Array<string>;
moduleDirectory?: Array<string>;
paths?: Array<Config.Path>;
Expand All @@ -32,6 +33,7 @@ type FindNodeModuleConfig = {
};

export type ResolveModuleConfig = {
conditions?: Array<string>;
skipNodeResolution?: boolean;
paths?: Array<Config.Path>;
};
Expand Down Expand Up @@ -113,6 +115,7 @@ export default class Resolver {
return resolver(path, {
basedir: options.basedir,
browser: options.browser,
conditions: options.conditions,
defaultResolver,
extensions: options.extensions,
moduleDirectory: options.moduleDirectory,
Expand All @@ -137,7 +140,8 @@ export default class Resolver {
): Config.Path | null {
const paths = options?.paths || this._options.modulePaths;
const moduleDirectory = this._options.moduleDirectories;
const key = dirname + path.delimiter + moduleName;
const stringifiedOptions = options ? JSON.stringify(options) : '';
const key = dirname + path.delimiter + moduleName + stringifiedOptions;
const defaultPlatform = this._options.defaultPlatform;
const extensions = this._options.extensions.slice();
let module;
Expand Down Expand Up @@ -183,6 +187,7 @@ export default class Resolver {

return Resolver.findNodeModule(name, {
basedir: dirname,
conditions: options?.conditions,
extensions,
moduleDirectory,
paths,
Expand Down Expand Up @@ -321,23 +326,31 @@ export default class Resolver {
virtualMocks: Map<string, boolean>,
from: Config.Path,
moduleName = '',
options?: ResolveModuleConfig,
): string {
const key = from + path.delimiter + moduleName;
const stringifiedOptions = options ? JSON.stringify(options) : '';
const key = from + path.delimiter + moduleName + stringifiedOptions;
const cachedModuleID = this._moduleIDCache.get(key);
if (cachedModuleID) {
return cachedModuleID;
}

const moduleType = this._getModuleType(moduleName);
const absolutePath = this._getAbsolutePath(virtualMocks, from, moduleName);
const absolutePath = this._getAbsolutePath(
virtualMocks,
from,
moduleName,
options,
);
const mockPath = this._getMockPath(from, moduleName);

const sep = path.delimiter;
const id =
moduleType +
sep +
(absolutePath ? absolutePath + sep : '') +
(mockPath ? mockPath + sep : '');
(mockPath ? mockPath + sep : '') +
(stringifiedOptions ? stringifiedOptions + sep : '');

this._moduleIDCache.set(key, id);
return id;
Expand All @@ -351,13 +364,14 @@ export default class Resolver {
virtualMocks: Map<string, boolean>,
from: Config.Path,
moduleName: string,
options?: ResolveModuleConfig,
): Config.Path | null {
if (this.isCoreModule(moduleName)) {
return moduleName;
}
return this._isModuleResolved(from, moduleName)
? this.getModule(moduleName)
: this._getVirtualMockPath(virtualMocks, from, moduleName);
: this._getVirtualMockPath(virtualMocks, from, moduleName, options);
}

private _getMockPath(
Expand All @@ -373,12 +387,13 @@ export default class Resolver {
virtualMocks: Map<string, boolean>,
from: Config.Path,
moduleName: string,
options?: ResolveModuleConfig,
): Config.Path {
const virtualMockPath = this.getModulePath(from, moduleName);
return virtualMocks.get(virtualMockPath)
? virtualMockPath
: moduleName
? this.resolveModule(from, moduleName)
? this.resolveModule(from, moduleName, options)
: from;
}

Expand Down