1'use strict';
2
3const findYarnWorkspaceRoot = require('find-yarn-workspace-root');
4const fs = require('fs');
5const path = require('path');
6
7module.exports = function(basePreset) {
8  // Explicitly catch and log errors since Jest sometimes suppresses error messages
9  try {
10    return _createJestPreset(basePreset);
11  } catch (e) {
12    console.error(`${e.name}: ${e.message}`);
13    throw e;
14  }
15};
16
17function _createJestPreset(basePreset) {
18  // Jest does not support chained presets so we flatten this preset before exporting it
19  return {
20    ...basePreset,
21    clearMocks: true,
22    roots: ['<rootDir>/src'],
23    transform: {
24      '^.+\\.jsx?$': 'babel-jest',
25      '^.+\\.tsx?$': [
26        'ts-jest',
27        {
28          tsconfig: _createTypeScriptConfiguration(),
29          babelConfig: _createBabelConfiguration(),
30        },
31      ],
32      ...basePreset.transform,
33    },
34  };
35}
36
37function _createBabelConfiguration() {
38  // "true" signals that ts-jest should use Babel's default configuration lookup, which will use the
39  // configuration file written above
40  return true;
41}
42
43function _createTypeScriptConfiguration() {
44  let tsConfigFileName = 'tsconfig.json';
45  // The path to tsconfig.json is resolved relative to cwd
46  let tsConfigPath = path.resolve(tsConfigFileName);
47
48  let jestTsConfigDirectory = path.join('.expo');
49  let jestTsConfig = {
50    extends: tsConfigPath,
51    compilerOptions: {
52      // Explicitly specify "module" so that ts-jest doesn't provide its default
53      module: 'esnext',
54      // Explicitly specify all the "node_modules/@types" paths up to the workspace or package root
55      typeRoots: [
56        ..._getDefaultTypeRoots(jestTsConfigDirectory),
57        path.join(__dirname, 'ts-declarations'),
58      ],
59    },
60  };
61
62  let jestTsConfigJson = JSON.stringify(jestTsConfig, null, 2);
63  // The TypeScript configuration needs to be under the project directory so that TypeScript finds
64  // type declaration packages that are installed in the project or workspace root (writing it to a
65  // temporary directory would not work, for example)
66  let jestTsConfigPath = path.join(jestTsConfigDirectory, 'tsconfig.jest.json');
67
68  // NOTE: remove this existsSync call once we require Node 10.12+
69  if (!fs.existsSync(jestTsConfigPath)) {
70    fs.mkdirSync(path.dirname(jestTsConfigPath), { recursive: true });
71  }
72  fs.writeFileSync(jestTsConfigPath, jestTsConfigJson);
73  return jestTsConfigPath;
74}
75
76/**
77 * By default, TypeScript looks for type declarations in "node_modules/@types" of the current
78 * directory and all ancestor directories. When overriding the "typeRoots" option, TypeScript no
79 * longer follows this algorithm and we need to re-implement the default behavior. This function
80 * returns the default type roots that TypeScript would have used, except we stop at the workspace
81 * root for better isolation.
82 */
83function _getDefaultTypeRoots(currentDirectory) {
84  currentDirectory = path.resolve(currentDirectory);
85  let typeRoots = ['./node_modules/@types'];
86
87  // If the TypeScript configuration is in a subdirectory of a package, find the package's directory
88  // since find-yarn-workspace-root works only from workspace packages
89  let packageDirectory = _findPackageDirectory(currentDirectory);
90  if (!packageDirectory) {
91    return typeRoots;
92  }
93
94  let workspaceRootPath = findYarnWorkspaceRoot(packageDirectory);
95
96  // If the TypeScript configuration is in a Yarn workspace, workspace's npm dependencies may be
97  // installed in the workspace root. If the configuration is in a non-workspace package, its
98  // dependencies are installed only in the package's directory.
99  let rootPath = workspaceRootPath || packageDirectory;
100
101  let relativeAncestorDirectoryPath = '..';
102  while (currentDirectory !== rootPath) {
103    typeRoots.push(path.join(relativeAncestorDirectoryPath, 'node_modules/@types'));
104    currentDirectory = path.dirname(currentDirectory);
105    relativeAncestorDirectoryPath = path.join(relativeAncestorDirectoryPath, '..');
106  }
107
108  return typeRoots;
109}
110
111function _findPackageDirectory(currentDirectory) {
112  while (currentDirectory !== path.dirname(currentDirectory)) {
113    if (fs.existsSync(path.join(currentDirectory, 'package.json'))) {
114      return currentDirectory;
115    }
116    currentDirectory = path.dirname(currentDirectory);
117  }
118  return null;
119}
120