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