1import JsonFile from '@expo/json-file'; 2import resolveFrom from 'resolve-from'; 3import semver from 'semver'; 4 5import { getCombinedKnownVersionsAsync } from './getVersionedPackages'; 6 7const debug = require('debug')('expo:doctor:dependencies:getMissingPackages') as typeof console.log; 8 9export type ResolvedPackage = { 10 /** Module ID pointing to the library `package.json`. */ 11 file: string; 12 /** NPM package name. */ 13 pkg: string; 14 /** Required version range. */ 15 version?: string; 16 /** If the dependency should be installed as a `devDependency` */ 17 dev?: boolean; 18}; 19 20/** Given a set of required packages, this method returns a list of missing packages. */ 21export function collectMissingPackages( 22 projectRoot: string, 23 requiredPackages: ResolvedPackage[] 24): { 25 missing: ResolvedPackage[]; 26 resolutions: Record<string, string>; 27} { 28 const resolutions: Record<string, string> = {}; 29 30 const missingPackages = requiredPackages.filter((p) => { 31 const resolved = resolveFrom.silent(projectRoot, p.file); 32 if (!resolved || !versionSatisfiesRequiredPackage(resolved, p)) { 33 return true; 34 } 35 resolutions[p.pkg] = resolved; 36 return false; 37 }); 38 39 return { missing: missingPackages, resolutions }; 40} 41 42export function versionSatisfiesRequiredPackage( 43 packageJsonFilePath: string, 44 resolvedPackage: Pick<ResolvedPackage, 'version' | 'pkg'> 45): boolean { 46 // If the version is specified, check that it satisfies the installed version. 47 if (!resolvedPackage.version) { 48 debug(`Required package "${resolvedPackage.pkg}" found (no version constraint specified).`); 49 return true; 50 } 51 52 const pkgJson = JsonFile.read(packageJsonFilePath); 53 if ( 54 // package.json has version. 55 typeof pkgJson.version === 'string' && 56 // semver satisfaction. 57 semver.satisfies(pkgJson.version, resolvedPackage.version) 58 ) { 59 return true; 60 } 61 debug( 62 `Installed package "${resolvedPackage.pkg}" does not satisfy version constraint "${resolvedPackage.version}" (version: "${pkgJson.version}")` 63 ); 64 return false; 65} 66 67/** 68 * Collect missing packages given a list of required packages. 69 * Any missing packages will be versioned to the known versions for the current SDK. 70 * 71 * @param projectRoot 72 * @param props.requiredPackages list of required packages to check for 73 * @returns list of missing packages and resolutions to existing packages. 74 */ 75export async function getMissingPackagesAsync( 76 projectRoot: string, 77 { 78 sdkVersion, 79 requiredPackages, 80 }: { 81 sdkVersion?: string; 82 requiredPackages: ResolvedPackage[]; 83 } 84): Promise<{ 85 missing: ResolvedPackage[]; 86 resolutions: Record<string, string>; 87}> { 88 const results = collectMissingPackages(projectRoot, requiredPackages); 89 if (!results.missing.length) { 90 return results; 91 } 92 93 // Ensure the versions are right for the SDK that the project is currently using. 94 await mutatePackagesWithKnownVersionsAsync(projectRoot, sdkVersion, results.missing); 95 96 return results; 97} 98 99export async function mutatePackagesWithKnownVersionsAsync( 100 projectRoot: string, 101 sdkVersion: string | undefined, 102 packages: ResolvedPackage[] 103) { 104 // Ensure the versions are right for the SDK that the project is currently using. 105 const relatedPackages = await getCombinedKnownVersionsAsync({ projectRoot, sdkVersion }); 106 for (const pkg of packages) { 107 if ( 108 // Only use the SDK versions if the package does not already have a hardcoded version. 109 // We do this because some packages have API coded into the CLI which expects an exact version. 110 !pkg.version && 111 pkg.pkg in relatedPackages 112 ) { 113 pkg.version = relatedPackages[pkg.pkg]; 114 } 115 } 116 return packages; 117} 118