mirror of https://github.com/facebook/jest.git
922 lines
27 KiB
TypeScript
922 lines
27 KiB
TypeScript
/**
|
|
* 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.
|
|
*/
|
|
|
|
import * as path from 'path';
|
|
import chalk = require('chalk');
|
|
import slash = require('slash');
|
|
import type {IModuleMap} from 'jest-haste-map';
|
|
import {tryRealpath} from 'jest-util';
|
|
import ModuleNotFoundError from './ModuleNotFoundError';
|
|
import defaultResolver, {
|
|
type AsyncResolver,
|
|
type Resolver as ResolverInterface,
|
|
type SyncResolver,
|
|
} from './defaultResolver';
|
|
import {clearFsCache} from './fileWalkers';
|
|
import isBuiltinModule from './isBuiltinModule';
|
|
import nodeModulesPaths, {GlobalPaths} from './nodeModulesPaths';
|
|
import shouldLoadAsEsm, {clearCachedLookups} from './shouldLoadAsEsm';
|
|
import type {ResolverConfig} from './types';
|
|
|
|
export type FindNodeModuleConfig = {
|
|
basedir: string;
|
|
conditions?: Array<string>;
|
|
extensions?: Array<string>;
|
|
moduleDirectory?: Array<string>;
|
|
paths?: Array<string>;
|
|
resolver?: string | null;
|
|
rootDir?: string;
|
|
throwIfNotFound?: boolean;
|
|
};
|
|
|
|
export type ResolveModuleConfig = {
|
|
conditions?: Array<string>;
|
|
skipNodeResolution?: boolean;
|
|
paths?: Array<string>;
|
|
};
|
|
|
|
const NATIVE_PLATFORM = 'native';
|
|
|
|
// We might be inside a symlink.
|
|
const resolvedCwd = tryRealpath(process.cwd());
|
|
const {NODE_PATH} = process.env;
|
|
const nodePaths = NODE_PATH
|
|
? NODE_PATH.split(path.delimiter)
|
|
.filter(Boolean)
|
|
// The resolver expects absolute paths.
|
|
.map(p => path.resolve(resolvedCwd, p))
|
|
: undefined;
|
|
|
|
export default class Resolver {
|
|
private readonly _options: ResolverConfig;
|
|
private readonly _moduleMap: IModuleMap;
|
|
private readonly _moduleIDCache: Map<string, string>;
|
|
private readonly _moduleNameCache: Map<string, string>;
|
|
private readonly _modulePathCache: Map<string, Array<string>>;
|
|
private readonly _supportsNativePlatform: boolean;
|
|
|
|
constructor(moduleMap: IModuleMap, options: ResolverConfig) {
|
|
this._options = {
|
|
defaultPlatform: options.defaultPlatform,
|
|
extensions: options.extensions,
|
|
hasCoreModules:
|
|
options.hasCoreModules === undefined ? true : options.hasCoreModules,
|
|
moduleDirectories: options.moduleDirectories || ['node_modules'],
|
|
moduleNameMapper: options.moduleNameMapper,
|
|
modulePaths: options.modulePaths,
|
|
platforms: options.platforms,
|
|
resolver: options.resolver,
|
|
rootDir: options.rootDir,
|
|
};
|
|
this._supportsNativePlatform = options.platforms
|
|
? options.platforms.includes(NATIVE_PLATFORM)
|
|
: false;
|
|
this._moduleMap = moduleMap;
|
|
this._moduleIDCache = new Map();
|
|
this._moduleNameCache = new Map();
|
|
this._modulePathCache = new Map();
|
|
}
|
|
|
|
static ModuleNotFoundError = ModuleNotFoundError;
|
|
|
|
static tryCastModuleNotFoundError(
|
|
error: unknown,
|
|
): ModuleNotFoundError | null {
|
|
if (error instanceof ModuleNotFoundError) {
|
|
return error;
|
|
}
|
|
|
|
const casted = error as ModuleNotFoundError;
|
|
if (casted.code === 'MODULE_NOT_FOUND') {
|
|
return ModuleNotFoundError.duckType(casted);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static clearDefaultResolverCache(): void {
|
|
clearFsCache();
|
|
clearCachedLookups();
|
|
}
|
|
|
|
static findNodeModule(
|
|
path: string,
|
|
options: FindNodeModuleConfig,
|
|
): string | null {
|
|
const resolverModule = loadResolver(options.resolver);
|
|
let resolver: SyncResolver = defaultResolver;
|
|
|
|
if (typeof resolverModule === 'function') {
|
|
resolver = resolverModule;
|
|
} else if (typeof resolverModule.sync === 'function') {
|
|
resolver = resolverModule.sync;
|
|
}
|
|
|
|
const paths = options.paths;
|
|
|
|
try {
|
|
return resolver(path, {
|
|
basedir: options.basedir,
|
|
conditions: options.conditions,
|
|
defaultResolver,
|
|
extensions: options.extensions,
|
|
moduleDirectory: options.moduleDirectory,
|
|
paths: paths ? [...(nodePaths || []), ...paths] : nodePaths,
|
|
rootDir: options.rootDir,
|
|
});
|
|
} catch (error) {
|
|
// we always wanna throw if it's an internal import
|
|
if (options.throwIfNotFound || path.startsWith('#')) {
|
|
throw error;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static async findNodeModuleAsync(
|
|
path: string,
|
|
options: FindNodeModuleConfig,
|
|
): Promise<string | null> {
|
|
const resolverModule = loadResolver(options.resolver);
|
|
let resolver: ResolverInterface = defaultResolver;
|
|
|
|
if (typeof resolverModule === 'function') {
|
|
resolver = resolverModule;
|
|
} else if (
|
|
typeof resolverModule.async === 'function' ||
|
|
typeof resolverModule.sync === 'function'
|
|
) {
|
|
const asyncOrSync = resolverModule.async || resolverModule.sync;
|
|
|
|
if (asyncOrSync == null) {
|
|
throw new Error(`Unable to load resolver at ${options.resolver}`);
|
|
}
|
|
|
|
resolver = asyncOrSync;
|
|
}
|
|
|
|
const paths = options.paths;
|
|
|
|
try {
|
|
const result = await resolver(path, {
|
|
basedir: options.basedir,
|
|
conditions: options.conditions,
|
|
defaultResolver,
|
|
extensions: options.extensions,
|
|
moduleDirectory: options.moduleDirectory,
|
|
paths: paths ? [...(nodePaths || []), ...paths] : nodePaths,
|
|
rootDir: options.rootDir,
|
|
});
|
|
return result;
|
|
} catch (error: unknown) {
|
|
// we always wanna throw if it's an internal import
|
|
if (options.throwIfNotFound || path.startsWith('#')) {
|
|
throw error;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// unstable as it should be replaced by https://github.com/nodejs/modules/issues/393, and we don't want people to use it
|
|
static unstable_shouldLoadAsEsm = shouldLoadAsEsm;
|
|
|
|
resolveModuleFromDirIfExists(
|
|
dirname: string,
|
|
moduleName: string,
|
|
options?: ResolveModuleConfig,
|
|
): string | null {
|
|
const {extensions, key, moduleDirectory, paths, skipResolution} =
|
|
this._prepareForResolution(dirname, moduleName, options);
|
|
|
|
let module;
|
|
|
|
// 1. If we have already resolved this module for this directory name,
|
|
// return a value from the cache.
|
|
const cacheResult = this._moduleNameCache.get(key);
|
|
if (cacheResult) {
|
|
return cacheResult;
|
|
}
|
|
|
|
// 2. Check if the module is a haste module.
|
|
module = this.getModule(moduleName);
|
|
if (module) {
|
|
this._moduleNameCache.set(key, module);
|
|
return module;
|
|
}
|
|
|
|
// 3. Check if the module is a node module and resolve it based on
|
|
// the node module resolution algorithm. If skipNodeResolution is given we
|
|
// ignore all modules that look like node modules (ie. are not relative
|
|
// requires). This enables us to speed up resolution when we build a
|
|
// dependency graph because we don't have to look at modules that may not
|
|
// exist and aren't mocked.
|
|
const resolveNodeModule = (name: string, throwIfNotFound = false) => {
|
|
// Only skip default resolver
|
|
if (this.isCoreModule(name) && !this._options.resolver) {
|
|
return name;
|
|
}
|
|
|
|
return Resolver.findNodeModule(name, {
|
|
basedir: dirname,
|
|
conditions: options?.conditions,
|
|
extensions,
|
|
moduleDirectory,
|
|
paths,
|
|
resolver: this._options.resolver,
|
|
rootDir: this._options.rootDir,
|
|
throwIfNotFound,
|
|
});
|
|
};
|
|
|
|
if (!skipResolution) {
|
|
module = resolveNodeModule(moduleName, Boolean(process.versions.pnp));
|
|
|
|
if (module) {
|
|
this._moduleNameCache.set(key, module);
|
|
return module;
|
|
}
|
|
}
|
|
|
|
// 4. Resolve "haste packages" which are `package.json` files outside of
|
|
// `node_modules` folders anywhere in the file system.
|
|
try {
|
|
const hasteModulePath = this._getHasteModulePath(moduleName);
|
|
if (hasteModulePath) {
|
|
// try resolving with custom resolver first to support extensions,
|
|
// then fallback to require.resolve
|
|
const resolvedModule =
|
|
resolveNodeModule(hasteModulePath) ||
|
|
require.resolve(hasteModulePath);
|
|
this._moduleNameCache.set(key, resolvedModule);
|
|
return resolvedModule;
|
|
}
|
|
} catch {}
|
|
|
|
return null;
|
|
}
|
|
|
|
async resolveModuleFromDirIfExistsAsync(
|
|
dirname: string,
|
|
moduleName: string,
|
|
options?: ResolveModuleConfig,
|
|
): Promise<string | null> {
|
|
const {extensions, key, moduleDirectory, paths, skipResolution} =
|
|
this._prepareForResolution(dirname, moduleName, options);
|
|
|
|
let module;
|
|
|
|
// 1. If we have already resolved this module for this directory name,
|
|
// return a value from the cache.
|
|
const cacheResult = this._moduleNameCache.get(key);
|
|
if (cacheResult) {
|
|
return cacheResult;
|
|
}
|
|
|
|
// 2. Check if the module is a haste module.
|
|
module = this.getModule(moduleName);
|
|
if (module) {
|
|
this._moduleNameCache.set(key, module);
|
|
return module;
|
|
}
|
|
|
|
// 3. Check if the module is a node module and resolve it based on
|
|
// the node module resolution algorithm. If skipNodeResolution is given we
|
|
// ignore all modules that look like node modules (ie. are not relative
|
|
// requires). This enables us to speed up resolution when we build a
|
|
// dependency graph because we don't have to look at modules that may not
|
|
// exist and aren't mocked.
|
|
const resolveNodeModule = async (name: string, throwIfNotFound = false) => {
|
|
// Only skip default resolver
|
|
if (this.isCoreModule(name) && !this._options.resolver) {
|
|
return name;
|
|
}
|
|
|
|
return Resolver.findNodeModuleAsync(name, {
|
|
basedir: dirname,
|
|
conditions: options?.conditions,
|
|
extensions,
|
|
moduleDirectory,
|
|
paths,
|
|
resolver: this._options.resolver,
|
|
rootDir: this._options.rootDir,
|
|
throwIfNotFound,
|
|
});
|
|
};
|
|
|
|
if (!skipResolution) {
|
|
module = await resolveNodeModule(
|
|
moduleName,
|
|
Boolean(process.versions.pnp),
|
|
);
|
|
|
|
if (module) {
|
|
this._moduleNameCache.set(key, module);
|
|
return module;
|
|
}
|
|
}
|
|
|
|
// 4. Resolve "haste packages" which are `package.json` files outside of
|
|
// `node_modules` folders anywhere in the file system.
|
|
try {
|
|
const hasteModulePath = this._getHasteModulePath(moduleName);
|
|
if (hasteModulePath) {
|
|
// try resolving with custom resolver first to support extensions,
|
|
// then fallback to require.resolve
|
|
const resolvedModule =
|
|
(await resolveNodeModule(hasteModulePath)) ||
|
|
// QUESTION: should this be async?
|
|
require.resolve(hasteModulePath);
|
|
this._moduleNameCache.set(key, resolvedModule);
|
|
return resolvedModule;
|
|
}
|
|
} catch {}
|
|
|
|
return null;
|
|
}
|
|
|
|
resolveModule(
|
|
from: string,
|
|
moduleName: string,
|
|
options: ResolveModuleConfig,
|
|
): string {
|
|
const dirname = path.dirname(from);
|
|
const module =
|
|
this.resolveStubModuleName(from, moduleName, options) ||
|
|
this.resolveModuleFromDirIfExists(dirname, moduleName, options);
|
|
if (module) return module;
|
|
|
|
// 5. Throw an error if the module could not be found. `resolve.sync` only
|
|
// produces an error based on the dirname but we have the actual current
|
|
// module name available.
|
|
this._throwModNotFoundError(from, moduleName);
|
|
}
|
|
|
|
async resolveModuleAsync(
|
|
from: string,
|
|
moduleName: string,
|
|
options?: ResolveModuleConfig,
|
|
): Promise<string> {
|
|
const dirname = path.dirname(from);
|
|
const module =
|
|
(await this.resolveStubModuleNameAsync(from, moduleName, options)) ||
|
|
(await this.resolveModuleFromDirIfExistsAsync(
|
|
dirname,
|
|
moduleName,
|
|
options,
|
|
));
|
|
|
|
if (module) return module;
|
|
|
|
// 5. Throw an error if the module could not be found. `resolve` only
|
|
// produces an error based on the dirname but we have the actual current
|
|
// module name available.
|
|
this._throwModNotFoundError(from, moduleName);
|
|
}
|
|
|
|
/**
|
|
* _prepareForResolution is shared between the sync and async module resolution
|
|
* methods, to try to keep them as DRY as possible.
|
|
*/
|
|
private _prepareForResolution(
|
|
dirname: string,
|
|
moduleName: string,
|
|
options?: ResolveModuleConfig,
|
|
) {
|
|
const paths = options?.paths || this._options.modulePaths;
|
|
const moduleDirectory = this._options.moduleDirectories;
|
|
const stringifiedOptions = options ? JSON.stringify(options) : '';
|
|
const key = dirname + path.delimiter + moduleName + stringifiedOptions;
|
|
const defaultPlatform = this._options.defaultPlatform;
|
|
const extensions = [...this._options.extensions];
|
|
|
|
if (this._supportsNativePlatform) {
|
|
extensions.unshift(
|
|
...this._options.extensions.map(ext => `.${NATIVE_PLATFORM}${ext}`),
|
|
);
|
|
}
|
|
if (defaultPlatform) {
|
|
extensions.unshift(
|
|
...this._options.extensions.map(ext => `.${defaultPlatform}${ext}`),
|
|
);
|
|
}
|
|
|
|
const skipResolution =
|
|
options && options.skipNodeResolution && !moduleName.includes(path.sep);
|
|
|
|
return {extensions, key, moduleDirectory, paths, skipResolution};
|
|
}
|
|
|
|
/**
|
|
* _getHasteModulePath attempts to return the path to a haste module.
|
|
*/
|
|
private _getHasteModulePath(moduleName: string) {
|
|
const parts = moduleName.split('/');
|
|
const hastePackage = this.getPackage(parts.shift()!);
|
|
if (hastePackage) {
|
|
return path.join(path.dirname(hastePackage), ...parts);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private _throwModNotFoundError(from: string, moduleName: string): never {
|
|
const relativePath =
|
|
slash(path.relative(this._options.rootDir, from)) || '.';
|
|
|
|
throw new ModuleNotFoundError(
|
|
`Cannot find module '${moduleName}' from '${relativePath}'`,
|
|
moduleName,
|
|
);
|
|
}
|
|
|
|
private _getMapModuleName(matches: RegExpMatchArray | null) {
|
|
return matches
|
|
? (moduleName: string) =>
|
|
moduleName.replaceAll(
|
|
/\$(\d+)/g,
|
|
(_, index) => matches[Number.parseInt(index, 10)] || '',
|
|
)
|
|
: (moduleName: string) => moduleName;
|
|
}
|
|
|
|
private _isAliasModule(moduleName: string): boolean {
|
|
const moduleNameMapper = this._options.moduleNameMapper;
|
|
if (!moduleNameMapper) {
|
|
return false;
|
|
}
|
|
|
|
return moduleNameMapper.some(({regex}) => regex.test(moduleName));
|
|
}
|
|
|
|
isCoreModule(moduleName: string): boolean {
|
|
return (
|
|
this._options.hasCoreModules &&
|
|
(isBuiltinModule(moduleName) || moduleName.startsWith('node:')) &&
|
|
!this._isAliasModule(moduleName)
|
|
);
|
|
}
|
|
|
|
getModule(name: string): string | null {
|
|
return this._moduleMap.getModule(
|
|
name,
|
|
this._options.defaultPlatform,
|
|
this._supportsNativePlatform,
|
|
);
|
|
}
|
|
|
|
getModulePath(from: string, moduleName: string): string {
|
|
if (moduleName[0] !== '.' || path.isAbsolute(moduleName)) {
|
|
return moduleName;
|
|
}
|
|
return path.normalize(`${path.dirname(from)}/${moduleName}`);
|
|
}
|
|
|
|
getPackage(name: string): string | null {
|
|
return this._moduleMap.getPackage(
|
|
name,
|
|
this._options.defaultPlatform,
|
|
this._supportsNativePlatform,
|
|
);
|
|
}
|
|
|
|
getMockModule(
|
|
from: string,
|
|
name: string,
|
|
options: Pick<ResolveModuleConfig, 'conditions'>,
|
|
): string | null {
|
|
const mock = this._moduleMap.getMockModule(name);
|
|
if (mock) {
|
|
return mock;
|
|
} else {
|
|
const moduleName = this.resolveStubModuleName(from, name, options);
|
|
if (moduleName) {
|
|
return this.getModule(moduleName) || moduleName;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async getMockModuleAsync(
|
|
from: string,
|
|
name: string,
|
|
options: Pick<ResolveModuleConfig, 'conditions'>,
|
|
): Promise<string | null> {
|
|
const mock = this._moduleMap.getMockModule(name);
|
|
if (mock) {
|
|
return mock;
|
|
} else {
|
|
const moduleName = await this.resolveStubModuleNameAsync(
|
|
from,
|
|
name,
|
|
options,
|
|
);
|
|
if (moduleName) {
|
|
return this.getModule(moduleName) || moduleName;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getModulePaths(from: string): Array<string> {
|
|
const cachedModule = this._modulePathCache.get(from);
|
|
if (cachedModule) {
|
|
return cachedModule;
|
|
}
|
|
|
|
const moduleDirectory = this._options.moduleDirectories;
|
|
const paths = nodeModulesPaths(from, {moduleDirectory});
|
|
if (paths.at(-1) === undefined) {
|
|
// circumvent node-resolve bug that adds `undefined` as last item.
|
|
paths.pop();
|
|
}
|
|
this._modulePathCache.set(from, paths);
|
|
return paths;
|
|
}
|
|
|
|
getGlobalPaths(moduleName?: string): Array<string> {
|
|
if (!moduleName || moduleName[0] === '.' || this.isCoreModule(moduleName)) {
|
|
return [];
|
|
}
|
|
|
|
return GlobalPaths;
|
|
}
|
|
|
|
getModuleID(
|
|
virtualMocks: Map<string, boolean>,
|
|
from: string,
|
|
moduleName = '',
|
|
options: ResolveModuleConfig,
|
|
): string {
|
|
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,
|
|
options,
|
|
);
|
|
const mockPath = this._getMockPath(from, moduleName, options);
|
|
|
|
const sep = path.delimiter;
|
|
const id =
|
|
moduleType +
|
|
sep +
|
|
(absolutePath ? absolutePath + sep : '') +
|
|
(mockPath ? mockPath + sep : '') +
|
|
(stringifiedOptions ? stringifiedOptions + sep : '');
|
|
|
|
this._moduleIDCache.set(key, id);
|
|
return id;
|
|
}
|
|
|
|
async getModuleIDAsync(
|
|
virtualMocks: Map<string, boolean>,
|
|
from: string,
|
|
moduleName = '',
|
|
options: ResolveModuleConfig,
|
|
): Promise<string> {
|
|
const stringifiedOptions = options ? JSON.stringify(options) : '';
|
|
const key = from + path.delimiter + moduleName + stringifiedOptions;
|
|
const cachedModuleID = this._moduleIDCache.get(key);
|
|
if (cachedModuleID) {
|
|
return cachedModuleID;
|
|
}
|
|
if (moduleName.startsWith('data:')) {
|
|
return moduleName;
|
|
}
|
|
|
|
const moduleType = this._getModuleType(moduleName);
|
|
const absolutePath = await this._getAbsolutePathAsync(
|
|
virtualMocks,
|
|
from,
|
|
moduleName,
|
|
options,
|
|
);
|
|
const mockPath = await this._getMockPathAsync(from, moduleName, options);
|
|
|
|
const sep = path.delimiter;
|
|
const id =
|
|
moduleType +
|
|
sep +
|
|
(absolutePath ? absolutePath + sep : '') +
|
|
(mockPath ? mockPath + sep : '') +
|
|
(stringifiedOptions ? stringifiedOptions + sep : '');
|
|
|
|
this._moduleIDCache.set(key, id);
|
|
return id;
|
|
}
|
|
|
|
private _getModuleType(moduleName: string): 'node' | 'user' {
|
|
return this.isCoreModule(moduleName) ? 'node' : 'user';
|
|
}
|
|
|
|
private _getAbsolutePath(
|
|
virtualMocks: Map<string, boolean>,
|
|
from: string,
|
|
moduleName: string,
|
|
options: ResolveModuleConfig,
|
|
): string | null {
|
|
if (this.isCoreModule(moduleName)) {
|
|
return moduleName;
|
|
}
|
|
if (moduleName.startsWith('data:')) {
|
|
return moduleName;
|
|
}
|
|
return this._isModuleResolved(from, moduleName, options)
|
|
? this.getModule(moduleName)
|
|
: this._getVirtualMockPath(virtualMocks, from, moduleName, options);
|
|
}
|
|
|
|
private async _getAbsolutePathAsync(
|
|
virtualMocks: Map<string, boolean>,
|
|
from: string,
|
|
moduleName: string,
|
|
options: ResolveModuleConfig,
|
|
): Promise<string | null> {
|
|
if (this.isCoreModule(moduleName)) {
|
|
return moduleName;
|
|
}
|
|
if (moduleName.startsWith('data:')) {
|
|
return moduleName;
|
|
}
|
|
const isModuleResolved = await this._isModuleResolvedAsync(
|
|
from,
|
|
moduleName,
|
|
options,
|
|
);
|
|
return isModuleResolved
|
|
? this.getModule(moduleName)
|
|
: this._getVirtualMockPathAsync(virtualMocks, from, moduleName, options);
|
|
}
|
|
|
|
private _getMockPath(
|
|
from: string,
|
|
moduleName: string,
|
|
options: Pick<ResolveModuleConfig, 'conditions'>,
|
|
): string | null {
|
|
return this.isCoreModule(moduleName)
|
|
? null
|
|
: this.getMockModule(from, moduleName, options);
|
|
}
|
|
|
|
private async _getMockPathAsync(
|
|
from: string,
|
|
moduleName: string,
|
|
options: Pick<ResolveModuleConfig, 'conditions'>,
|
|
): Promise<string | null> {
|
|
return this.isCoreModule(moduleName)
|
|
? null
|
|
: this.getMockModuleAsync(from, moduleName, options);
|
|
}
|
|
|
|
private _getVirtualMockPath(
|
|
virtualMocks: Map<string, boolean>,
|
|
from: string,
|
|
moduleName: string,
|
|
options: ResolveModuleConfig,
|
|
): string {
|
|
const virtualMockPath = this.getModulePath(from, moduleName);
|
|
return virtualMocks.get(virtualMockPath)
|
|
? virtualMockPath
|
|
: moduleName
|
|
? this.resolveModule(from, moduleName, options)
|
|
: from;
|
|
}
|
|
|
|
private async _getVirtualMockPathAsync(
|
|
virtualMocks: Map<string, boolean>,
|
|
from: string,
|
|
moduleName: string,
|
|
options?: ResolveModuleConfig,
|
|
): Promise<string> {
|
|
const virtualMockPath = this.getModulePath(from, moduleName);
|
|
return virtualMocks.get(virtualMockPath)
|
|
? virtualMockPath
|
|
: moduleName
|
|
? this.resolveModuleAsync(from, moduleName, options)
|
|
: from;
|
|
}
|
|
|
|
private _isModuleResolved(
|
|
from: string,
|
|
moduleName: string,
|
|
options: Pick<ResolveModuleConfig, 'conditions'>,
|
|
): boolean {
|
|
return !!(
|
|
this.getModule(moduleName) ||
|
|
this.getMockModule(from, moduleName, options)
|
|
);
|
|
}
|
|
|
|
private async _isModuleResolvedAsync(
|
|
from: string,
|
|
moduleName: string,
|
|
options: Pick<ResolveModuleConfig, 'conditions'>,
|
|
): Promise<boolean> {
|
|
return !!(
|
|
this.getModule(moduleName) ||
|
|
(await this.getMockModuleAsync(from, moduleName, options))
|
|
);
|
|
}
|
|
|
|
resolveStubModuleName(
|
|
from: string,
|
|
moduleName: string,
|
|
options: Pick<ResolveModuleConfig, 'conditions'>,
|
|
): string | null {
|
|
const dirname = path.dirname(from);
|
|
|
|
const {extensions, moduleDirectory, paths} = this._prepareForResolution(
|
|
dirname,
|
|
moduleName,
|
|
);
|
|
const moduleNameMapper = this._options.moduleNameMapper;
|
|
const resolver = this._options.resolver;
|
|
|
|
if (moduleNameMapper) {
|
|
for (const {moduleName: mappedModuleName, regex} of moduleNameMapper) {
|
|
if (regex.test(moduleName)) {
|
|
// Note: once a moduleNameMapper matches the name, it must result
|
|
// in a module, or else an error is thrown.
|
|
const matches = moduleName.match(regex);
|
|
const mapModuleName = this._getMapModuleName(matches);
|
|
const possibleModuleNames = Array.isArray(mappedModuleName)
|
|
? mappedModuleName
|
|
: [mappedModuleName];
|
|
let module: string | null = null;
|
|
for (const possibleModuleName of possibleModuleNames) {
|
|
const updatedName = mapModuleName(possibleModuleName);
|
|
module =
|
|
this.getModule(updatedName) ||
|
|
Resolver.findNodeModule(updatedName, {
|
|
basedir: dirname,
|
|
conditions: options?.conditions,
|
|
extensions,
|
|
moduleDirectory,
|
|
paths,
|
|
resolver,
|
|
rootDir: this._options.rootDir,
|
|
});
|
|
|
|
if (module) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!module) {
|
|
throw createNoMappedModuleFoundError(
|
|
moduleName,
|
|
mapModuleName,
|
|
mappedModuleName,
|
|
regex,
|
|
resolver,
|
|
);
|
|
}
|
|
return module;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async resolveStubModuleNameAsync(
|
|
from: string,
|
|
moduleName: string,
|
|
options?: Pick<ResolveModuleConfig, 'conditions'>,
|
|
): Promise<string | null> {
|
|
const dirname = path.dirname(from);
|
|
|
|
const {extensions, moduleDirectory, paths} = this._prepareForResolution(
|
|
dirname,
|
|
moduleName,
|
|
);
|
|
const moduleNameMapper = this._options.moduleNameMapper;
|
|
const resolver = this._options.resolver;
|
|
|
|
if (moduleNameMapper) {
|
|
for (const {moduleName: mappedModuleName, regex} of moduleNameMapper) {
|
|
if (regex.test(moduleName)) {
|
|
// Note: once a moduleNameMapper matches the name, it must result
|
|
// in a module, or else an error is thrown.
|
|
const matches = moduleName.match(regex);
|
|
const mapModuleName = this._getMapModuleName(matches);
|
|
const possibleModuleNames = Array.isArray(mappedModuleName)
|
|
? mappedModuleName
|
|
: [mappedModuleName];
|
|
let module: string | null = null;
|
|
for (const possibleModuleName of possibleModuleNames) {
|
|
const updatedName = mapModuleName(possibleModuleName);
|
|
|
|
module =
|
|
this.getModule(updatedName) ||
|
|
(await Resolver.findNodeModuleAsync(updatedName, {
|
|
basedir: dirname,
|
|
conditions: options?.conditions,
|
|
extensions,
|
|
moduleDirectory,
|
|
paths,
|
|
resolver,
|
|
rootDir: this._options.rootDir,
|
|
}));
|
|
|
|
if (module) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!module) {
|
|
throw createNoMappedModuleFoundError(
|
|
moduleName,
|
|
mapModuleName,
|
|
mappedModuleName,
|
|
regex,
|
|
resolver,
|
|
);
|
|
}
|
|
return module;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const createNoMappedModuleFoundError = (
|
|
moduleName: string,
|
|
mapModuleName: (moduleName: string) => string,
|
|
mappedModuleName: string | Array<string>,
|
|
regex: RegExp,
|
|
resolver?: ((...args: Array<unknown>) => unknown) | string | null,
|
|
) => {
|
|
const mappedAs = Array.isArray(mappedModuleName)
|
|
? JSON.stringify(mappedModuleName.map(mapModuleName), null, 2)
|
|
: mappedModuleName;
|
|
const original = Array.isArray(mappedModuleName)
|
|
? `${
|
|
JSON.stringify(mappedModuleName, null, 6) // using 6 because of misalignment when nested below
|
|
.slice(0, -1) + ' '.repeat(4)
|
|
}]` /// align last bracket correctly as well
|
|
: mappedModuleName;
|
|
|
|
const error = new Error(
|
|
chalk.red(`${chalk.bold('Configuration error')}:
|
|
|
|
Could not locate module ${chalk.bold(moduleName)} mapped as:
|
|
${chalk.bold(mappedAs)}.
|
|
|
|
Please check your configuration for these entries:
|
|
{
|
|
"moduleNameMapper": {
|
|
"${regex.toString()}": "${chalk.bold(original)}"
|
|
},
|
|
"resolver": ${chalk.bold(String(resolver))}
|
|
}`),
|
|
);
|
|
|
|
error.name = '';
|
|
|
|
return error;
|
|
};
|
|
|
|
type ResolverSyncObject = {sync: SyncResolver; async?: AsyncResolver};
|
|
type ResolverAsyncObject = {sync?: SyncResolver; async: AsyncResolver};
|
|
export type ResolverObject = ResolverSyncObject | ResolverAsyncObject;
|
|
|
|
function loadResolver(
|
|
resolver: string | undefined | null,
|
|
): SyncResolver | ResolverObject {
|
|
if (resolver == null) {
|
|
return defaultResolver;
|
|
}
|
|
|
|
const loadedResolver = require(resolver);
|
|
|
|
if (loadedResolver == null) {
|
|
throw new Error(`Resolver located at ${resolver} does not export anything`);
|
|
}
|
|
|
|
if (typeof loadedResolver === 'function') {
|
|
return loadedResolver as SyncResolver;
|
|
}
|
|
|
|
if (
|
|
typeof loadedResolver === 'object' &&
|
|
(loadedResolver.sync != null || loadedResolver.async != null)
|
|
) {
|
|
return loadedResolver as ResolverObject;
|
|
}
|
|
|
|
throw new Error(
|
|
`Resolver located at ${resolver} does not export a function or an object with "sync" and "async" props`,
|
|
);
|
|
}
|