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 ...versionsForSdk, 53 ...bundledNativeModules, 54 }; 55} 56 57/** @returns a key/value list of known dependencies and their version (including range). */ 58export async function getRemoteVersionsForSdkAsync({ 59 sdkVersion, 60 skipCache, 61}: { sdkVersion?: string; skipCache?: boolean } = {}): Promise<DependencyList> { 62 if (env.EXPO_OFFLINE) { 63 Log.warn('Dependency validation is unreliable in offline-mode'); 64 return {}; 65 } 66 67 try { 68 const { sdkVersions } = await getVersionsAsync({ skipCache }); 69 70 // We only want versioned dependencies so skip if they cannot be found. 71 if (!sdkVersion || !(sdkVersion in sdkVersions)) { 72 debug( 73 `Skipping versioned dependencies because the SDK version is not found. (sdkVersion: ${sdkVersion}, available: ${Object.keys( 74 sdkVersions 75 ).join(', ')})` 76 ); 77 return {}; 78 } 79 80 const version = sdkVersions[sdkVersion as keyof typeof sdkVersions] as unknown as SDKVersion; 81 82 return normalizeSdkVersionObject(version); 83 } catch (error: any) { 84 if (error instanceof CommandError && error.code === 'OFFLINE') { 85 return getRemoteVersionsForSdkAsync({ sdkVersion, skipCache }); 86 } 87 throw error; 88 } 89} 90 91/** 92 * Versions a list of `packages` against a given `sdkVersion` based on local and remote versioning resources. 93 * 94 * @param projectRoot 95 * @param param1 96 * @returns 97 */ 98export async function getVersionedPackagesAsync( 99 projectRoot: string, 100 { 101 packages, 102 sdkVersion, 103 pkg, 104 }: { 105 /** List of npm packages to process. */ 106 packages: string[]; 107 /** Target SDK Version number to version the `packages` for. */ 108 sdkVersion: string; 109 pkg: PackageJSONConfig; 110 } 111): Promise<{ 112 packages: string[]; 113 messages: string[]; 114 excludedNativeModules: { name: string; bundledNativeVersion: string }[]; 115}> { 116 const versionsForSdk = await getCombinedKnownVersionsAsync({ 117 projectRoot, 118 sdkVersion, 119 skipCache: true, 120 }); 121 122 let nativeModulesCount = 0; 123 let othersCount = 0; 124 const excludedNativeModules: { name: string; bundledNativeVersion: string }[] = []; 125 126 const versionedPackages = packages.map((arg) => { 127 const { name, type, raw } = npmPackageArg(arg); 128 129 if (['tag', 'version', 'range'].includes(type) && name && versionsForSdk[name]) { 130 // Unimodule packages from npm registry are modified to use the bundled version. 131 // Some packages have the recommended version listed in https://exp.host/--/api/v2/versions. 132 if (pkg?.expo?.install?.exclude?.includes(name)) { 133 othersCount++; 134 excludedNativeModules.push({ name, bundledNativeVersion: versionsForSdk[name] }); 135 return raw; 136 } 137 nativeModulesCount++; 138 return `${name}@${versionsForSdk[name]}`; 139 } else { 140 // Other packages are passed through unmodified. 141 othersCount++; 142 return raw; 143 } 144 }); 145 146 const messages = getOperationLog({ 147 othersCount, 148 nativeModulesCount, 149 sdkVersion, 150 }); 151 152 return { 153 packages: versionedPackages, 154 messages, 155 excludedNativeModules, 156 }; 157} 158 159/** Craft a set of messages regarding the install operations. */ 160export function getOperationLog({ 161 nativeModulesCount, 162 sdkVersion, 163 othersCount, 164}: { 165 nativeModulesCount: number; 166 othersCount: number; 167 sdkVersion: string; 168}): string[] { 169 return [ 170 nativeModulesCount > 0 && 171 `${nativeModulesCount} SDK ${sdkVersion} compatible native ${ 172 nativeModulesCount === 1 ? 'module' : 'modules' 173 }`, 174 othersCount > 0 && `${othersCount} other ${othersCount === 1 ? 'package' : 'packages'}`, 175 ].filter(Boolean) as string[]; 176} 177