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