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 conditions from test environments #11863

Merged
merged 2 commits into from Sep 13, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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-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>;
Copy link
Member Author

Choose a reason for hiding this comment

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

this could probably be a property, but making it into a function seems fine for a bit more flexibility

}

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