1import findUp from 'find-up';
2import fs from 'fs-extra';
3import path from 'path';
4
5import { SearchOptions } from '../types';
6
7/**
8 * Path to the `package.json` of the closest project in the current working dir.
9 */
10export const projectPackageJsonPath = findUp.sync('package.json', { cwd: process.cwd() }) as string;
11
12// This won't happen in usual scenarios, but we need to unwrap the optional path :)
13if (!projectPackageJsonPath) {
14  throw new Error(`Couldn't find "package.json" up from path "${process.cwd()}"`);
15}
16
17/**
18 * Merges autolinking options from different sources (the later the higher priority)
19 * - options defined in package.json's `expo.autolinking` field
20 * - platform-specific options from the above (e.g. `expo.autolinking.ios`)
21 * - options provided to the CLI command
22 */
23export async function mergeLinkingOptionsAsync<OptionsType extends SearchOptions>(
24  providedOptions: OptionsType
25): Promise<OptionsType> {
26  const packageJson = require(projectPackageJsonPath);
27  const baseOptions = packageJson.expo?.autolinking;
28  const platformOptions = providedOptions.platform && baseOptions?.[providedOptions.platform];
29  const finalOptions = Object.assign(
30    {},
31    baseOptions,
32    platformOptions,
33    providedOptions
34  ) as OptionsType;
35
36  // Makes provided paths absolute or falls back to default paths if none was provided.
37  finalOptions.searchPaths = await resolveSearchPathsAsync(finalOptions.searchPaths, process.cwd());
38
39  finalOptions.nativeModulesDir = await resolveNativeModulesDirAsync(
40    finalOptions.nativeModulesDir,
41    process.cwd()
42  );
43
44  return finalOptions;
45}
46
47/**
48 * Resolves autolinking search paths. If none is provided, it accumulates all node_modules when
49 * going up through the path components. This makes workspaces work out-of-the-box without any configs.
50 */
51export async function resolveSearchPathsAsync(
52  searchPaths: string[] | null,
53  cwd: string
54): Promise<string[]> {
55  return searchPaths && searchPaths.length > 0
56    ? searchPaths.map((searchPath) => path.resolve(cwd, searchPath))
57    : await findDefaultPathsAsync(cwd);
58}
59
60/**
61 * Looks up for workspace's `node_modules` paths.
62 */
63async function findDefaultPathsAsync(cwd: string): Promise<string[]> {
64  const paths = [];
65  let dir = cwd;
66  let pkgJsonPath: string | undefined;
67
68  while ((pkgJsonPath = await findUp('package.json', { cwd: dir }))) {
69    dir = path.dirname(path.dirname(pkgJsonPath));
70    paths.push(path.join(pkgJsonPath, '..', 'node_modules'));
71
72    // This stops the infinite loop when the package.json is placed at the root dir.
73    if (path.dirname(dir) === dir) {
74      break;
75    }
76  }
77  return paths;
78}
79
80/**
81 * Finds the real path to custom native modules directory.
82 * - When {@link cwd} is inside the project directory, the path is searched relatively
83 * to the project root (directory with the `package.json` file).
84 * - When {@link cwd} is outside project directory (no `package.json` found), it is relative to
85 * the current working directory (the {@link cwd} param).
86 *
87 * @param nativeModulesDir path to custom native modules directory. Defaults to `"./modules"` if null.
88 * @param cwd current working directory
89 * @returns resolved native modules directory or `null` if it is not found or doesn't exist.
90 */
91async function resolveNativeModulesDirAsync(
92  nativeModulesDir: string | null | undefined,
93  cwd: string
94): Promise<string | null> {
95  const packageJsonPath = await findUp('package.json', { cwd });
96  const projectRoot = packageJsonPath != null ? path.join(packageJsonPath, '..') : cwd;
97  const resolvedPath = path.resolve(projectRoot, nativeModulesDir || 'modules');
98  return fs.existsSync(resolvedPath) ? resolvedPath : null;
99}
100