Skip to content

Commit

Permalink
feat: support conditions from test environments
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed Sep 10, 2021
1 parent 22ec319 commit 4d0902c
Show file tree
Hide file tree
Showing 13 changed files with 130 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,7 @@
### Features

- `[jest-resolver, jest-runtime]` Pass `conditions` to custom resolvers to enable them to implement support for package.json `exports` field ([#11859](https://github.com/facebook/jest/pull/11859))
- `[jest-runtime]` Allow custom envs to specify `exportConditions` which is passed together with Jest's own conditions to custom resolvers ([#11863](https://github.com/facebook/jest/pull/11863))

### Fixes

Expand Down
2 changes: 1 addition & 1 deletion docs/Configuration.md
Expand Up @@ -851,7 +851,7 @@ module.exports = (request, options) => {
};
```

While Jest does not support [package `exports`](https://nodejs.org/api/packages.html#packages_package_entry_points) (beyond `main`), Jest will provide `conditions` as an option when calling custom resolvers, which can be used to implement support for `exports` in userland. Jest will pass `['import', 'default']` when running a test in ESM mode, and `['require', 'default']` when running with CJS.
While Jest does not support [package `exports`](https://nodejs.org/api/packages.html#packages_package_entry_points) (beyond `main`), Jest will provide `conditions` as an option when calling custom resolvers, which can be used to implement support for `exports` in userland. Jest will pass `['import', 'default']` when running a test in ESM mode, and `['require', 'default']` when running with CJS. Additionally, custom test environments can specify an `exportConditions` method which returns an array of conditions that will be passed along with Jest's defaults.

### `restoreMocks` \[boolean]

Expand Down
14 changes: 14 additions & 0 deletions e2e/resolve-conditions/__tests__/browser.test.mjs
@@ -0,0 +1,14 @@
/**
* 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.
*
* @jest-environment <rootDir>/browser-env.js
*/

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

test('returns correct message', () => {
expect(fn()).toEqual('hello from browser');
});
14 changes: 14 additions & 0 deletions e2e/resolve-conditions/__tests__/node.test.mjs
@@ -0,0 +1,14 @@
/**
* 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.
*
* @jest-environment <rootDir>/node-env.js
*/

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

test('returns correct message', () => {
expect(fn()).toEqual('hello from node');
});
16 changes: 16 additions & 0 deletions e2e/resolve-conditions/browser-env.js
@@ -0,0 +1,16 @@
/**
* 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 BrowserEnv = require('jest-environment-jsdom');

module.exports = class BrowserEnvWithConditions extends BrowserEnv {
exportConditions() {
return ['browser'];
}
};
10 changes: 10 additions & 0 deletions e2e/resolve-conditions/fake-dual-dep/browser.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 browser';
}
10 changes: 10 additions & 0 deletions e2e/resolve-conditions/fake-dual-dep/node.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 node';
}
10 changes: 10 additions & 0 deletions e2e/resolve-conditions/fake-dual-dep/package.json
@@ -0,0 +1,10 @@
{
"name": "fake-dual-dep",
"version": "1.0.0",
"exports": {
".": {
"node": "./node.mjs",
"browser": "./browser.mjs"
}
}
}
16 changes: 16 additions & 0 deletions e2e/resolve-conditions/node-env.js
@@ -0,0 +1,16 @@
/**
* 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 NodeEnv = require('jest-environment-node');

module.exports = class NodeEnvWithConditions extends NodeEnv {
exportConditions() {
return ['node'];
}
};
4 changes: 3 additions & 1 deletion e2e/resolve-conditions/resolver.js
Expand Up @@ -25,9 +25,11 @@ function createPathFilter(conditions) {

return (
resolveExports(pkg, path, {
// `resolve.exports adds `node` unless `browser` is `false`, so let's add this ugly thing
browser: conditions.includes('browser'),
conditions,
// `resolve.exports adds `import` unless `require` is `false`, so let's add this ugly thing
require: !conditions.includes('import'),
require: conditions.includes('require'),
}) || relativePath
);
};
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-environment-node/src/index.ts
Expand Up @@ -18,7 +18,7 @@ type Timer = {
unref: () => Timer;
};

class NodeEnvironment implements JestEnvironment {
class NodeEnvironment implements JestEnvironment<Timer> {
context: Context | null;
fakeTimers: LegacyFakeTimers<Timer> | null;
fakeTimersModern: ModernFakeTimers | null;
Expand Down
1 change: 1 addition & 0 deletions packages/jest-environment/src/index.ts
Expand Up @@ -42,6 +42,7 @@ export declare class JestEnvironment<Timer = unknown> {
setup(): Promise<void>;
teardown(): Promise<void>;
handleTestEvent?: Circus.EventHandler;
exportConditions?: () => Array<string>;
}

export type Module = NodeModule;
Expand Down
57 changes: 33 additions & 24 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -173,10 +173,6 @@ const supportsNodeColonModulePrefixInImport = (() => {
return stdout === 'true';
})();

// consider `node` & `browser` condition as well - maybe switching on `global.window` in the test env?
const esmConditions = ['import', 'default'];
const cjsConditions = ['require', 'default'];

export default class Runtime {
private readonly _cacheFS: Map<string, string>;
private readonly _config: Config.ProjectConfig;
Expand Down Expand Up @@ -228,6 +224,8 @@ export default class Runtime {
private _moduleImplementation?: typeof nativeModule.Module;
private readonly jestObjectCaches: Map<string, Jest>;
private jestGlobals?: JestGlobals;
private readonly esmConditions: Array<string>;
private readonly cjsConditions: Array<string>;

constructor(
config: Config.ProjectConfig,
Expand Down Expand Up @@ -292,6 +290,15 @@ export default class Runtime {
unmockRegExpCache.set(config, this._unmockList);
}

const envExportConditions = this._environment.exportConditions?.() ?? [];

this.esmConditions = Array.from(
new Set(['import', 'default', ...envExportConditions]),
);
this.cjsConditions = Array.from(
new Set(['require', 'default', ...envExportConditions]),
);

if (config.automock) {
config.setupFiles.forEach(filePath => {
if (filePath.includes(NODE_MODULES)) {
Expand All @@ -302,8 +309,8 @@ export default class Runtime {
// shouldn't really matter, but in theory this will make sure the caching is correct
{
conditions: this.unstable_shouldLoadAsEsm(filePath)
? esmConditions
: cjsConditions,
? this.esmConditions
: this.cjsConditions,
},
);
this._transitiveShouldMock.set(moduleID, false);
Expand Down Expand Up @@ -547,14 +554,14 @@ export default class Runtime {
referencingIdentifier,
path,
this._explicitShouldMockModule,
{conditions: esmConditions},
{conditions: this.esmConditions},
)
) {
return this.importMock(referencingIdentifier, path, context);
}

const resolved = this._resolveModule(referencingIdentifier, path, {
conditions: esmConditions,
conditions: this.esmConditions,
});

if (
Expand Down Expand Up @@ -604,7 +611,7 @@ export default class Runtime {
const [path, query] = (moduleName ?? '').split('?');

const modulePath = this._resolveModule(from, path, {
conditions: esmConditions,
conditions: this.esmConditions,
});

const module = await this.loadEsmModule(modulePath, query);
Expand Down Expand Up @@ -655,7 +662,7 @@ export default class Runtime {
this._virtualModuleMocks,
from,
moduleName,
{conditions: esmConditions},
{conditions: this.esmConditions},
);

if (this._moduleMockRegistry.has(moduleID)) {
Expand Down Expand Up @@ -703,7 +710,7 @@ export default class Runtime {

reexports.forEach(reexport => {
const resolved = this._resolveModule(modulePath, reexport, {
conditions: esmConditions,
conditions: this.esmConditions,
});

const exports = this.getExportsOfCjs(resolved);
Expand All @@ -727,7 +734,7 @@ export default class Runtime {
this._virtualMocks,
from,
moduleName,
isInternal ? undefined : {conditions: cjsConditions},
isInternal ? undefined : {conditions: this.cjsConditions},
);
let modulePath: string | undefined;

Expand Down Expand Up @@ -758,7 +765,7 @@ export default class Runtime {
modulePath = this._resolveModule(
from,
moduleName,
isInternal ? undefined : {conditions: cjsConditions},
isInternal ? undefined : {conditions: this.cjsConditions},
);
}

Expand Down Expand Up @@ -851,7 +858,7 @@ export default class Runtime {
this._virtualMocks,
from,
moduleName,
{conditions: cjsConditions},
{conditions: this.cjsConditions},
);

const mockRegistry = this._isolatedMockRegistry || this._mockRegistry;
Expand All @@ -871,7 +878,7 @@ export default class Runtime {

let modulePath =
this._resolver.getMockModule(from, moduleName) ||
this._resolveModule(from, moduleName, {conditions: cjsConditions});
this._resolveModule(from, moduleName, {conditions: this.cjsConditions});

let isManualMock =
manualMockOrStub &&
Expand Down Expand Up @@ -977,7 +984,7 @@ export default class Runtime {
try {
if (
this._shouldMock(from, moduleName, this._explicitShouldMock, {
conditions: cjsConditions,
conditions: this.cjsConditions,
})
) {
return this.requireMock<T>(from, moduleName);
Expand Down Expand Up @@ -1122,7 +1129,7 @@ export default class Runtime {
this._virtualMocks,
from,
moduleName,
{conditions: cjsConditions},
{conditions: this.cjsConditions},
);
this._explicitShouldMock.set(moduleID, true);
this._mockFactories.set(moduleID, mockFactory);
Expand All @@ -1143,7 +1150,7 @@ export default class Runtime {
this._virtualModuleMocks,
from,
moduleName,
{conditions: esmConditions},
{conditions: this.esmConditions},
);
this._explicitShouldMockModule.set(moduleID, true);
this._moduleMockFactories.set(moduleID, mockFactory);
Expand Down Expand Up @@ -1220,7 +1227,7 @@ export default class Runtime {
absolutePath,
moduleName,
// required to also resolve files without leading './' directly in the path
{conditions: cjsConditions, paths: [absolutePath]},
{conditions: this.cjsConditions, paths: [absolutePath]},
);
if (module) {
return module;
Expand All @@ -1234,7 +1241,9 @@ export default class Runtime {
);
}
try {
return this._resolveModule(from, moduleName, {conditions: cjsConditions});
return this._resolveModule(from, moduleName, {
conditions: this.cjsConditions,
});
} catch (err) {
const module = this._resolver.getMockModule(from, moduleName);

Expand Down Expand Up @@ -1589,7 +1598,7 @@ export default class Runtime {
private _generateMock(from: Config.Path, moduleName: string) {
const modulePath =
this._resolver.resolveStubModuleName(from, moduleName) ||
this._resolveModule(from, moduleName, {conditions: cjsConditions});
this._resolveModule(from, moduleName, {conditions: this.cjsConditions});
if (!this._mockMetaDataCache.has(modulePath)) {
// This allows us to handle circular dependencies while generating an
// automock
Expand Down Expand Up @@ -1772,7 +1781,7 @@ export default class Runtime {
this._virtualMocks,
from,
moduleName,
{conditions: cjsConditions},
{conditions: this.cjsConditions},
);
this._explicitShouldMock.set(moduleID, false);
return jestObject;
Expand All @@ -1782,7 +1791,7 @@ export default class Runtime {
this._virtualMocks,
from,
moduleName,
{conditions: cjsConditions},
{conditions: this.cjsConditions},
);
this._explicitShouldMock.set(moduleID, false);
this._transitiveShouldMock.set(moduleID, false);
Expand All @@ -1797,7 +1806,7 @@ export default class Runtime {
this._virtualMocks,
from,
moduleName,
{conditions: cjsConditions},
{conditions: this.cjsConditions},
);
this._explicitShouldMock.set(moduleID, true);
return jestObject;
Expand Down

0 comments on commit 4d0902c

Please sign in to comment.