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