1import JsonFile from '@expo/json-file'; 2import chalk from 'chalk'; 3import path from 'path'; 4import semver from 'semver'; 5 6import { EXPO_DIR } from '../../Constants'; 7import logger from '../../Logger'; 8import { Task } from '../../TasksRunner'; 9import * as Workspace from '../../Workspace'; 10import { Parcel, TaskArgs } from '../types'; 11 12const { green, yellow, cyan } = chalk; 13 14/** 15 * Updates versions of packages to be published in other workspace projects depending on them. 16 */ 17export const updateWorkspaceProjects = new Task<TaskArgs>( 18 { 19 name: 'updateWorkspaceProjects', 20 filesToStage: ['**/package.json', 'yarn.lock'], 21 }, 22 async (parcels: Parcel[]) => { 23 logger.info('\n Updating workspace projects...'); 24 25 const workspaceInfo = await Workspace.getInfoAsync(); 26 const dependenciesKeys = [ 27 'dependencies', 28 'devDependencies', 29 'peerDependencies', 30 'optionalDependencies', 31 ]; 32 33 const parcelsObject = parcels.reduce((acc, parcel) => { 34 acc[parcel.pkg.packageName] = parcel; 35 return acc; 36 }, {}); 37 38 await Promise.all( 39 Object.entries(workspaceInfo).map(async ([projectName, projectInfo]) => { 40 const projectDependencies = [ 41 ...projectInfo.workspaceDependencies, 42 ...projectInfo.mismatchedWorkspaceDependencies, 43 ] 44 .map((dependencyName) => parcelsObject[dependencyName]) 45 .filter(Boolean); 46 47 // If this project doesn't depend on any package we're going to publish. 48 if (projectDependencies.length === 0) { 49 return; 50 } 51 52 // Get copy of project's `package.json`. 53 const projectPackageJsonPath = path.join(EXPO_DIR, projectInfo.location, 'package.json'); 54 const projectPackageJson = await JsonFile.readAsync(projectPackageJsonPath); 55 const batch = logger.batch(); 56 57 batch.log(' ', green(projectName)); 58 59 // Iterate through different dependencies types. 60 for (const dependenciesKey of dependenciesKeys) { 61 const dependenciesObject = projectPackageJson[dependenciesKey]; 62 63 if (!dependenciesObject) { 64 continue; 65 } 66 67 for (const { pkg, state } of projectDependencies) { 68 const currentVersionRange = dependenciesObject[pkg.packageName]; 69 70 if ( 71 !currentVersionRange || 72 !shouldUpdateDependencyVersion(projectName, currentVersionRange, state.releaseVersion) 73 ) { 74 continue; 75 } 76 77 // Leave tilde and caret as they are, just replace the version. 78 const newVersionRange = currentVersionRange.replace( 79 /([\^~]?).*/, 80 `$1${state.releaseVersion}` 81 ); 82 dependenciesObject[pkg.packageName] = newVersionRange; 83 84 batch.log( 85 ' ', 86 `Updating ${yellow(`${dependenciesKey}.${pkg.packageName}`)}`, 87 `from ${cyan(currentVersionRange)} to ${cyan(newVersionRange)}` 88 ); 89 } 90 } 91 92 // Save project's `package.json`. 93 await JsonFile.writeAsync(projectPackageJsonPath, projectPackageJson); 94 95 // Flush batched logs if there is at least one version change in the project. 96 if (batch.batchedLogs.length > 1) { 97 batch.flush(); 98 } 99 }) 100 ); 101 } 102); 103 104/** 105 * Returns boolean indicating if the version range should be updated. Our policy assumes that `expo` package controls versions 106 * of other expo packages (e.g. expo-modules-core, expo-modules-autolinking). Any other package (or workspace project) 107 * doesn't need to be updated as long as the new version still satisfies the version range. 108 * 109 * @param packageName Name of the package to update 110 * @param currentRange Current version range of the dependency 111 * @param version The new version of the dependency 112 */ 113function shouldUpdateDependencyVersion(packageName: string, currentRange: string, version: string) { 114 return packageName === 'expo' || !semver.satisfies(version, currentRange); 115} 116