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