1import { PackageJSONConfig } from '@expo/config';
2import npmPackageArg from 'npm-package-arg';
3
4import { getVersionsAsync, SDKVersion } from '../../../api/getVersions';
5import { getVersionedNativeModulesAsync } from './bundledNativeModules';
6
7const debug = require('debug')(
8  'expo:doctor:dependencies:getVersionedPackages'
9) as typeof console.log;
10
11export type DependencyList = Record<string, string>;
12
13/** Adds `react-dom`, `react`, and `react-native` to the list of known package versions (`relatedPackages`) */
14function normalizeSdkVersionObject(version?: SDKVersion): Record<string, string> {
15  if (!version) {
16    return {};
17  }
18  const { relatedPackages, facebookReactVersion, facebookReactNativeVersion } = version;
19
20  const reactVersion = facebookReactVersion
21    ? {
22        react: facebookReactVersion,
23        'react-dom': facebookReactVersion,
24      }
25    : undefined;
26
27  return {
28    ...relatedPackages,
29    ...reactVersion,
30    'react-native': facebookReactNativeVersion,
31  };
32}
33
34/** Get the known versions for a given SDK, combines all sources. */
35export async function getCombinedKnownVersionsAsync({
36  projectRoot,
37  sdkVersion,
38  skipCache,
39}: {
40  projectRoot: string;
41  sdkVersion?: string;
42  skipCache?: boolean;
43}) {
44  const bundledNativeModules = sdkVersion
45    ? await getVersionedNativeModulesAsync(projectRoot, sdkVersion)
46    : {};
47  const versionsForSdk = await getRemoteVersionsForSdkAsync({ sdkVersion, skipCache });
48  return {
49    ...versionsForSdk,
50    ...bundledNativeModules,
51  };
52}
53
54/** @returns a key/value list of known dependencies and their version (including range). */
55export async function getRemoteVersionsForSdkAsync({
56  sdkVersion,
57  skipCache,
58}: { sdkVersion?: string; skipCache?: boolean } = {}): Promise<DependencyList> {
59  const { sdkVersions } = await getVersionsAsync({ skipCache });
60
61  // We only want versioned dependencies so skip if they cannot be found.
62  if (!sdkVersion || !(sdkVersion in sdkVersions)) {
63    debug(
64      `Skipping versioned dependencies because the SDK version is not found. (sdkVersion: ${sdkVersion}, available: ${Object.keys(
65        sdkVersions
66      ).join(', ')})`
67    );
68    return {};
69  }
70
71  const version = sdkVersions[sdkVersion as keyof typeof sdkVersions] as unknown as SDKVersion;
72
73  return normalizeSdkVersionObject(version);
74}
75
76/**
77 * Versions a list of `packages` against a given `sdkVersion` based on local and remote versioning resources.
78 *
79 * @param projectRoot
80 * @param param1
81 * @returns
82 */
83export async function getVersionedPackagesAsync(
84  projectRoot: string,
85  {
86    packages,
87    sdkVersion,
88    pkg,
89  }: {
90    /** List of npm packages to process. */
91    packages: string[];
92    /** Target SDK Version number to version the `packages` for. */
93    sdkVersion: string;
94    pkg: PackageJSONConfig;
95  }
96): Promise<{
97  packages: string[];
98  messages: string[];
99  excludedNativeModules: { name: string; bundledNativeVersion: string }[];
100}> {
101  const versionsForSdk = await getCombinedKnownVersionsAsync({
102    projectRoot,
103    sdkVersion,
104    skipCache: true,
105  });
106
107  let nativeModulesCount = 0;
108  let othersCount = 0;
109  const excludedNativeModules: { name: string; bundledNativeVersion: string }[] = [];
110
111  const versionedPackages = packages.map((arg) => {
112    const { name, type, raw } = npmPackageArg(arg);
113
114    if (['tag', 'version', 'range'].includes(type) && name && versionsForSdk[name]) {
115      // Unimodule packages from npm registry are modified to use the bundled version.
116      // Some packages have the recommended version listed in https://exp.host/--/api/v2/versions.
117      if (pkg?.expo?.install?.exclude?.includes(name)) {
118        othersCount++;
119        excludedNativeModules.push({ name, bundledNativeVersion: versionsForSdk[name] });
120        return raw;
121      }
122      nativeModulesCount++;
123      return `${name}@${versionsForSdk[name]}`;
124    } else {
125      // Other packages are passed through unmodified.
126      othersCount++;
127      return raw;
128    }
129  });
130
131  const messages = getOperationLog({
132    othersCount,
133    nativeModulesCount,
134    sdkVersion,
135  });
136
137  return {
138    packages: versionedPackages,
139    messages,
140    excludedNativeModules,
141  };
142}
143
144/** Craft a set of messages regarding the install operations. */
145export function getOperationLog({
146  nativeModulesCount,
147  sdkVersion,
148  othersCount,
149}: {
150  nativeModulesCount: number;
151  othersCount: number;
152  sdkVersion: string;
153}): string[] {
154  return [
155    nativeModulesCount > 0 &&
156      `${nativeModulesCount} SDK ${sdkVersion} compatible native ${
157        nativeModulesCount === 1 ? 'module' : 'modules'
158      }`,
159    othersCount > 0 && `${othersCount} other ${othersCount === 1 ? 'package' : 'packages'}`,
160  ].filter(Boolean) as string[];
161}
162