mirror of
https://github.com/hanzoai/jest.git
synced 2026-09-24 16:59:57 +00:00
feat(resolver): add check for sync resolver (#16064)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
### Features
|
||||
|
||||
- `[@jest/transform]` Add `canTransformSync(filename)` on `ScriptTransformer` so callers can pick the sync vs async transform path ([#16062](https://github.com/jestjs/jest/pull/16062))
|
||||
- `[jest-resolve]` Add `canResolveSync()` on `Resolver` so callers can detect when a user-configured resolver only exports an `async` hook ([#16064](https://github.com/jestjs/jest/pull/16064))
|
||||
- `[jest-runtime]` Use synchronous `evaluate()` for ES modules without top-level `await` on Node versions that support it (v24.9+), and prefer the synchronous transform path when a sync transformer is configured ([#16062](https://github.com/jestjs/jest/pull/16062))
|
||||
|
||||
## 30.3.0
|
||||
|
||||
@@ -41,7 +41,7 @@ exports[`moduleNameMapper wrong array configuration 1`] = `
|
||||
12 | module.exports = () => 'test';
|
||||
13 |
|
||||
|
||||
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1117:17)
|
||||
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1144:17)
|
||||
at Object.require (index.js:10:1)
|
||||
at Object.require (__tests__/index.js:10:20)"
|
||||
`;
|
||||
@@ -71,7 +71,7 @@ exports[`moduleNameMapper wrong configuration 1`] = `
|
||||
12 | module.exports = () => 'test';
|
||||
13 |
|
||||
|
||||
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1117:17)
|
||||
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1144:17)
|
||||
at Object.require (index.js:10:1)
|
||||
at Object.require (__tests__/index.js:10:20)"
|
||||
`;
|
||||
|
||||
@@ -26,7 +26,7 @@ exports[`shows a proper error from deep requires 1`] = `
|
||||
12 | test('dummy', () => {
|
||||
13 | expect(1).toBe(1);
|
||||
|
||||
at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/index.js:863:11)
|
||||
at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/index.js:890:11)
|
||||
at Object.<anonymous> (node_modules/discord.js/src/index.js:21:12)
|
||||
at Object.require (__tests__/test.js:10:1)"
|
||||
`;
|
||||
|
||||
@@ -37,7 +37,7 @@ exports[`show error message with matching files 1`] = `
|
||||
| ^
|
||||
9 |
|
||||
|
||||
at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/index.js:863:11)
|
||||
at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/index.js:890:11)
|
||||
at Object.require (index.js:8:18)
|
||||
at Object.require (__tests__/test.js:8:11)"
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async: function userResolverAsync(path, options) {
|
||||
return Promise.resolve('module');
|
||||
},
|
||||
sync: function userResolverSync(path, options) {
|
||||
return 'module';
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
sync: function userResolver(path, options) {
|
||||
return 'module';
|
||||
},
|
||||
};
|
||||
@@ -857,3 +857,55 @@ describe('Resolver.getGlobalPaths()', () => {
|
||||
expect(globalPaths).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canResolveSync', () => {
|
||||
// The other suites auto-mock `userResolver`/`userResolverAsync`, which
|
||||
// changes their shape; load the real modules here to test detection.
|
||||
beforeAll(() => {
|
||||
jest.unmock('../__mocks__/userResolver');
|
||||
jest.unmock('../__mocks__/userResolverAsync');
|
||||
});
|
||||
afterAll(() => {
|
||||
jest
|
||||
.mock('../__mocks__/userResolver')
|
||||
.mock('../__mocks__/userResolverAsync');
|
||||
});
|
||||
|
||||
it('returns true when no user resolver is configured', () => {
|
||||
const moduleMap = ModuleMap.create('/');
|
||||
const resolver = new Resolver(moduleMap, {} as ResolverConfig);
|
||||
expect(resolver.canResolveSync()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when the user resolver exports a plain function', () => {
|
||||
const moduleMap = ModuleMap.create('/');
|
||||
const resolver = new Resolver(moduleMap, {
|
||||
resolver: require.resolve('../__mocks__/userResolver'),
|
||||
} as ResolverConfig);
|
||||
expect(resolver.canResolveSync()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when the user resolver exports a `sync` hook', () => {
|
||||
const moduleMap = ModuleMap.create('/');
|
||||
const resolver = new Resolver(moduleMap, {
|
||||
resolver: require.resolve('../__mocks__/userResolverSync'),
|
||||
} as ResolverConfig);
|
||||
expect(resolver.canResolveSync()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when the user resolver exports both `sync` and `async`', () => {
|
||||
const moduleMap = ModuleMap.create('/');
|
||||
const resolver = new Resolver(moduleMap, {
|
||||
resolver: require.resolve('../__mocks__/userResolverDual'),
|
||||
} as ResolverConfig);
|
||||
expect(resolver.canResolveSync()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the user resolver only exports `async`', () => {
|
||||
const moduleMap = ModuleMap.create('/');
|
||||
const resolver = new Resolver(moduleMap, {
|
||||
resolver: require.resolve('../__mocks__/userResolverAsync'),
|
||||
} as ResolverConfig);
|
||||
expect(resolver.canResolveSync()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,6 +59,7 @@ export default class Resolver {
|
||||
private readonly _moduleNameCache: Map<string, string>;
|
||||
private readonly _modulePathCache: Map<string, Array<string>>;
|
||||
private readonly _supportsNativePlatform: boolean;
|
||||
private _canResolveSync: boolean | undefined;
|
||||
|
||||
constructor(moduleMap: IModuleMap, options: ResolverConfig) {
|
||||
this._options = {
|
||||
@@ -341,6 +342,34 @@ export default class Resolver {
|
||||
return null;
|
||||
}
|
||||
|
||||
// True when synchronous module resolution is available with the configured
|
||||
// resolver. False when a user has configured a resolver that only exports
|
||||
// an `async` hook — in that case `findNodeModule` silently falls back to
|
||||
// the default resolver, which will not honor the user's mappings, so
|
||||
// callers that need correctness should defer to the async API.
|
||||
//
|
||||
// The contract is "plain function or `.sync` hook = sync"; an `async
|
||||
// function` in either slot is a user-side contract violation we don't
|
||||
// defend against here.
|
||||
//
|
||||
// Memoized: the resolver config is immutable per-`Resolver`. A throwing
|
||||
// `loadResolver` (malformed user resolver) is treated as `false` so
|
||||
// callers route to the async path, which will surface the actual error.
|
||||
canResolveSync(): boolean {
|
||||
if (this._canResolveSync != null) return this._canResolveSync;
|
||||
let result: boolean;
|
||||
try {
|
||||
const resolverModule = loadResolver(this._options.resolver);
|
||||
result =
|
||||
typeof resolverModule === 'function' ||
|
||||
typeof resolverModule.sync === 'function';
|
||||
} catch {
|
||||
result = false;
|
||||
}
|
||||
this._canResolveSync = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
resolveModule(
|
||||
from: string,
|
||||
moduleName: string,
|
||||
|
||||
Reference in New Issue
Block a user