import path from 'path' ; import fs from 'fs' ; import chalk from 'chalk' ; import semver from 'semver' ; import execa from 'execa' ; import { Config } from '@react-native-community/cli-types' ; import { logger , CLIError , fetch } from '@react-native-community/cli-tools' ; import * as PackageManager from '../../tools/packageManager' ; import installPods from '../../tools/installPods' ; type UpgradeError = { message : string ; stderr : string ; } ; type RepoNameType = 'react-native' | 'react-native-tvos' ; const repos = { 'react-native' : { rawDiffUrl : 'https://raw.githubusercontent.com/react-native-community/rn-diff-purge/diffs/diffs' , webDiffUrl : 'https://react-native-community.github.io/upgrade-helper' , dependencyName : 'react-native' , } , 'react-native-tvos' : { rawDiffUrl : 'https://raw.githubusercontent.com/react-native-tvos/rn-diff-purge-tv/diffs/diffs' , webDiffUrl : 'https://react-native-community.github.io/upgrade-helper' , dependencyName : 'react-native@npm:react-native-tvos' , } , } ; const isConnected = ( output ) : boolean => { return ! output . includes ( 'the host is inaccessible' ) ; } ; const checkForErrors = ( output ) : void => { if ( ! output ) { return ; } if ( ! isConnected ( output ) ) { throw new CLIError ( 'Upgrade failed. You do not seem to have an internet connection.' ) ; } if ( output . includes ( 'npm ERR' ) ) { throw new CLIError ( `Upgrade failed with the following errors:\n${ output }` ) ; } if ( output . includes ( 'npm WARN' ) ) { logger . warn ( output ) ; } } ; const getLatestRNVersion = async ( repoName ) : Promise < string > => { logger . info ( 'No version passed. Fetching latest...' ) ; const { stdout , stderr } = await execa ( 'npm' , [ 'info' , repoName , 'version' ] ) ; checkForErrors ( stderr ) ; return stdout ; } ; const getRNPeerDeps = async ( version , repoName ) : Promise < { [ key ] : string ; } > => { const { stdout , stderr } = await execa ( 'npm' , [ 'info' , `${ repoName }@${ version }` , 'peerDependencies' , '--json' , ] ) ; checkForErrors ( stderr ) ; return JSON . parse ( stdout ) ; } ; const getPatch = async ( currentVersion , newVersion , config , repoName ) => { let patch ; logger . info ( `Fetching diff between v${ currentVersion } and v${ newVersion }...` ) ; try { const { data } = await fetch ( `${ repos [ repoName ] . rawDiffUrl }/${ currentVersion }..${ newVersion }.diff` ) ; patch = data ; } catch ( error ) { logger . error ( ( error as UpgradeError ) . message ) ; logger . error ( `Failed to fetch diff for react-native@${ newVersion }. Maybe it's not released yet?` ) ; logger . info ( `For available releases to diff see: ${ chalk . underline . dim ( 'https://github.com/react-native-community/rn-diff-purge#diff-table-full-table-here' ) }` ) ; return null ; } let patchWithRenamedProjects = patch ; Object . keys ( config . project ) . forEach ( ( platform ) => { if ( ! config . project [ platform ] ) { return ; } if ( platform === 'ios' ) { patchWithRenamedProjects = patchWithRenamedProjects . replace ( new RegExp ( 'RnDiffApp' , 'g' ) , config . project [ platform ] ! . projectName . replace ( '.xcodeproj' , '' ) ) ; } else if ( platform === 'android' ) { patchWithRenamedProjects = patchWithRenamedProjects . replace ( new RegExp ( 'com\\.rndiffapp' , 'g' ) , config . project [ platform ] ! . packageName ) . replace ( new RegExp ( 'com\\.rndiffapp' . split ( '.' ) . join ( '/' ) , 'g' ) , config . project [ platform ] ! . packageName . split ( '.' ) . join ( '/' ) ) ; } else { logger . warn ( `Unsupported platform: "${ platform }". \`upgrade\` only supports iOS and Android.` ) ; } } ) ; return patchWithRenamedProjects ; } ; const getVersionToUpgradeTo = async ( argv , currentVersion , projectDir , repoName ) => { const argVersion = argv [ 0 ] ; const semverCoercedVersion = semver . coerce ( argVersion ) ; const newVersion = argVersion ? semver . valid ( argVersion ) || ( semverCoercedVersion ? semverCoercedVersion . version : null ) : await getLatestRNVersion ( repoName ) ; if ( ! newVersion ) { logger . error ( `Provided version "${ argv [ 0 ] }" is not allowed. Please pass a valid semver version` ) ; return null ; } if ( semver . gt ( currentVersion , newVersion ) ) { logger . error ( `Trying to upgrade from newer version "${ currentVersion }" to older "${ newVersion }"` ) ; return null ; } if ( semver . eq ( currentVersion , newVersion ) ) { const { dependencies : { 'react-native' : version } , } = require ( path . join ( projectDir , 'package.json' ) ) ; const parsedVersion = version . split ( '@' ) [ version . split ( '@' ) . length - 1 ] ; if ( semver . satisfies ( newVersion , parsedVersion ) ) { logger . warn ( `Specified version "${ newVersion }" is already installed in node_modules and it satisfies "${ parsedVersion }" semver range. No need to upgrade` ) ; return null ; } logger . error ( `Dependency mismatch. Specified version "${ newVersion }" is already installed in node_modules and it doesn't satisfy "${ parsedVersion }" semver range of your "react-native" dependency. Please re-install your dependencies` ) ; return null ; } return newVersion ; } ; const installDeps = async ( root , newVersion , repoName ) => { logger . info ( `Installing "react-native@${ newVersion }" and its peer dependencies...` ) ; const peerDeps = await getRNPeerDeps ( newVersion , repoName ) ; const deps = [ `${ repos [ repoName ] . dependencyName }@${ newVersion }` , ... Object . keys ( peerDeps ) . map ( ( module ) => `${ module }@${ peerDeps [ module ] }` ) , ] ; await PackageManager . install ( deps , { silent : true , root , } ) ; await execa ( 'git' , [ 'add' , 'package.json' ] ) ; try { await execa ( 'git' , [ 'add' , 'yarn.lock' ] ) ; } catch ( error ) { } try { await execa ( 'git' , [ 'add' , 'package-lock.json' ] ) ; } catch ( error ) { } } ; const installCocoaPodsDeps = async ( projectDir ) => { if ( process . platform === 'darwin' ) { try { logger . info ( `Installing CocoaPods dependencies ${ chalk . dim ( '(this may take a few minutes)' ) }` ) ; await installPods ( { directory : projectDir . split ( '/' ) . pop ( ) || '' , } ) ; } catch ( error ) { if ( ( error as UpgradeError ) . stderr ) { logger . debug ( `"pod install" or "pod repo update" failed. Error output:\n${ ( error as UpgradeError ) . stderr }` ) ; } logger . error ( 'Installation of CocoaPods dependencies failed. Try to install them manually by running "pod install" in "ios" directory after finishing upgrade' ) ; } } } ; const applyPatch = async ( currentVersion , newVersion , tmpPatchFile , repoName ) => { const defaultExcludes = [ 'package.json' ] ; let filesThatDontExist = [ ] ; let filesThatFailedToApply = [ ] ; const { stdout : relativePathFromRoot } = await execa ( 'git' , [ 'rev-parse' , '--show-prefix' , ] ) ; try { try { const excludes = defaultExcludes . map ( ( e ) => `--exclude=${ path . join ( relativePathFromRoot , e ) }` ) ; await execa ( 'git' , [ 'apply' , '--binary' , '--check' , tmpPatchFile , ... excludes , '-p2' , '--3way' , `--directory=${ relativePathFromRoot }` , ] ) ; logger . info ( 'Applying diff...' ) ; } catch ( error ) { const errorLines = ( error as UpgradeError ) . stderr . split ( '\n' ) ; filesThatDontExist = [ ... errorLines . filter ( ( x ) => x . includes ( 'does not exist in index' ) ) . map ( ( x ) => x . replace ( /^error: (.*): does not exist in index$/ , '$1' ) ) , ] . filter ( Boolean ) ; filesThatFailedToApply = errorLines . filter ( ( x ) => x . includes ( 'patch does not apply' ) ) . map ( ( x ) => x . replace ( /^error: (.*): patch does not apply$/ , '$1' ) ) . filter ( Boolean ) ; logger . info ( 'Applying diff...' ) ; logger . warn ( `Excluding files that exist in the template, but not in your project:\n${ filesThatDontExist . map ( ( file ) => `  - ${ chalk . bold ( file ) }` ) . join ( '\n' ) }` ) ; if ( filesThatFailedToApply . length ) { logger . error ( `Excluding files that failed to apply the diff:\n${ filesThatFailedToApply . map ( ( file ) => `  - ${ chalk . bold ( file ) }` ) . join ( '\n' ) }\nPlease make sure to check the actual changes after the upgrade command is finished.\nYou can find them in our Upgrade Helper web app: ${ chalk . underline . dim ( `${ repos [ repoName ] . webDiffUrl }/?from=${ currentVersion }&to=${ newVersion }` ) }` ) ; } } finally { const excludes = [ ... defaultExcludes , ... filesThatDontExist , ... filesThatFailedToApply , ] . map ( ( e ) => `--exclude=${ path . join ( relativePathFromRoot , e ) }` ) ; await execa ( 'git' , [ 'apply' , tmpPatchFile , ... excludes , '-p2' , '--3way' , `--directory=${ relativePathFromRoot }` , ] ) ; } } catch ( error ) { if ( ( error as UpgradeError ) . stderr ) { logger . debug ( `"git apply" failed. Error output:\n${ ( error as UpgradeError ) . stderr }` ) ; } logger . error ( 'Automatically applying diff failed. We did our best to automatically upgrade as many files as possible' ) ; return false ; } return true ; } ; /**
 * Upgrade application to a new version of React Native.
 */ async function upgrade ( argv , ctx ) { const tmpPatchFile = 'tmp-upgrade-rn.patch' ; const projectDir = ctx . root ; const { name : rnName , version : currentVersion } = require ( path . join ( projectDir , 'node_modules/react-native/package.json' ) ) ; const repoName = rnName === 'react-native-tvos' ? 'react-native-tvos' : 'react-native' ; const newVersion = await getVersionToUpgradeTo ( argv , currentVersion , projectDir , repoName ) ; if ( ! newVersion ) { return ; } const patch = await getPatch ( currentVersion , newVersion , ctx , repoName ) ; if ( patch === null ) { return ; } if ( patch === '' ) { logger . info ( 'Diff has no changes to apply, proceeding further' ) ; await installDeps ( projectDir , newVersion , repoName ) ; await installCocoaPodsDeps ( projectDir ) ; logger . success ( `Upgraded React Native to v${ newVersion } 🎉. Now you can review and commit the changes` ) ; return ; } let patchSuccess ; try { fs . writeFileSync ( tmpPatchFile , patch ) ; patchSuccess = await applyPatch ( currentVersion , newVersion , tmpPatchFile , repoName ) ; } catch ( error ) { throw new Error ( ( error as UpgradeError ) . stderr || ( error as string ) ) ; } finally { try { fs . unlinkSync ( tmpPatchFile ) ; } catch ( e ) { } const { stdout } = await execa ( 'git' , [ 'status' , '-s' ] ) ; if ( ! patchSuccess ) { if ( stdout ) { logger . warn ( 'Continuing after failure. Some of the files are upgraded but you will need to deal with conflicts manually' ) ; await installDeps ( projectDir , newVersion , repoName ) ; logger . info ( 'Running "git status" to check what changed...' ) ; await execa ( 'git' , [ 'status' ] , { stdio : 'inherit' } ) ; } else { logger . error ( 'Patch failed to apply for unknown reason. Please fall back to manual way of upgrading' ) ; } } else { await installDeps ( projectDir , newVersion , repoName ) ; await installCocoaPodsDeps ( projectDir ) ; logger . info ( 'Running "git status" to check what changed...' ) ; await execa ( 'git' , [ 'status' ] , { stdio : 'inherit' } ) ; } if ( ! patchSuccess ) { if ( stdout ) { logger . warn ( 'Please run "git diff" to review the conflicts and resolve them' ) ; } if ( process . platform === 'darwin' ) { logger . warn ( 'After resolving conflicts don\'t forget to run "pod install" inside "ios" directory' ) ; } logger . info ( `You may find these resources helpful:
• Release notes: ${ chalk . underline . dim ( `https://github.com/facebook/react-native/releases/tag/v${ newVersion }` ) }
• Manual Upgrade Helper: ${ chalk . underline . dim ( `${ repos [ repoName ] . webDiffUrl }/?from=${ currentVersion }&to=${ newVersion }` ) }
• Git diff: ${ chalk . underline . dim ( `${ repos [ repoName ] . rawDiffUrl }/${ currentVersion }..${ newVersion }.diff` ) }` ) ; throw new CLIError ( 'Upgrade failed. Please see the messages above for details' ) ; } } logger . success ( `Upgraded React Native to v${ newVersion } 🎉. Now you can review and commit the changes` ) ; } const upgradeCommand = { name : 'upgrade [version]' , description : "Upgrade your app's template files to the specified or latest npm version using `rn-diff-purge` project. Only valid semver versions are allowed." , func : upgrade , } ; export default upgradeCommand ;