1const JsonFile = require('@expo/json-file'); 2const path = require('path'); 3 4/** 5 * Convert typescript paths to jest module mapping. 6 * 7 * @param {Record<string, string[]>} paths 8 * @param {string} [prefix="<rootDir>"] 9 * @return {Record<string, string>} 10 */ 11function jestMappingFromTypescriptPaths(paths, prefix = '<rootDir>') { 12 const mapping = {}; 13 14 for (const path in paths) { 15 if (!paths[path].length) { 16 console.warn(`Skipping empty typescript path map: ${path}`); 17 continue; 18 } 19 20 const jestRegex = convertTypescriptMatchToJestRegex(path); 21 const jestTarget = paths[path].map((target) => 22 convertTypescriptTargetToJestTarget(target, prefix) 23 ); 24 25 mapping[jestRegex] = jestTarget.length === 1 ? jestTarget[0] : jestTarget; 26 } 27 28 return mapping; 29} 30 31/** Convert a typescript match rule key to jest regex */ 32function convertTypescriptMatchToJestRegex(match) { 33 const regex = match 34 .split('/') 35 .map((segment) => 36 segment.trim() === '*' ? '(.*)' : segment.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&') 37 ) 38 .join('/'); 39 40 return `^${regex}$`; 41} 42 43/** Convert a typescript match rule value to jest regex target */ 44function convertTypescriptTargetToJestTarget(target, prefix = '<rootDir>') { 45 const segments = target.split('/').map((segment) => (segment.trim() === '*' ? '$1' : segment)); 46 return [prefix, ...segments].join('/'); 47} 48 49function mutateJestMappingFromConfig(jestConfig, configFile) { 50 const readJsonFile = JsonFile.default?.read || JsonFile.read; 51 52 try { 53 // The path to jsconfig.json or tsconfig.json is resolved relative to cwd 54 // See: _createTypeScriptConfiguration() in `createJestPreset` 55 const configPath = path.resolve(configFile); 56 const config = readJsonFile(configPath, { json5: true }); 57 let pathPrefix = '<rootDir>'; 58 59 if (config?.compilerOptions?.baseUrl) { 60 pathPrefix = path.join(pathPrefix, config.compilerOptions.baseUrl); 61 } 62 63 if (config?.compilerOptions?.paths) { 64 jestConfig.moduleNameMapper = { 65 ...jestMappingFromTypescriptPaths(config.compilerOptions.paths || {}, pathPrefix), 66 ...(jestConfig.moduleNameMapper || {}), 67 }; 68 } 69 70 return true; 71 } catch (error) { 72 // If the user is not using typescript, we can safely ignore this error 73 if (error.code === 'MODULE_NOT_FOUND' || error.code === 'ENOENT') { 74 return undefined; 75 } 76 77 // Other errors are unexpected, but should not block the jest configuration 78 return false; 79 } 80} 81 82/** Try to add the `moduleNameMapper` configuration from the typescript `paths` configuration. */ 83function withTypescriptMapping(jestConfig) { 84 const fromTsConfig = mutateJestMappingFromConfig(jestConfig, 'tsconfig.json'); 85 const fromJsConfig = !fromTsConfig 86 ? mutateJestMappingFromConfig(jestConfig, 'jsconfig.json') 87 : undefined; 88 89 if (fromTsConfig === false || fromJsConfig === false) { 90 console.warn('Failed to set custom typescript paths for jest.'); 91 console.warn('You need to configure jest moduleNameMapper manually.'); 92 console.warn( 93 'See: https://jestjs.io/docs/configuration#modulenamemapper-objectstring-string--arraystring' 94 ); 95 } 96 97 return jestConfig; 98} 99 100module.exports = { 101 _jestMappingFromTypescriptPaths: jestMappingFromTypescriptPaths, // Exported for testing 102 withTypescriptMapping, 103}; 104