forked from react-native-community/cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: notify of new major RN version at most once a week (react-nativ…
…e-community#268) Summary: --------- This PR addresses the first part of react-native-community#189: notifying the user when a newer version of React Native is available to upgrade to when using the CLI. **Behaviour** * The remote release check mechanism uses the [GitHub API](https://developer.github.com/v3/repos/releases/#get-the-latest-release). * The mechanism is triggered for any CLI command. * Nothing will be displayed if the project is already up-to-date. * The release data returned from the API call is cached per-project. Caching is necessary to not hit the GitHub API rate limit. * For the time being, there is no way to "mute" this behaviour if a project is intentionally using an older RN version because we have not discussed yet how the UX for that will be. **Caveats** The GitHub API we are using is specific to [Releases](https://help.github.com/en/articles/creating-releases), so we need to make sure that each new version of React Native has one. For example, 0.59.1 has been cut and published on NPM on March 14th but the release has been made only on the 25th. During that interval, the CLI would have not notified users of the newer version. Test Plan: ---------- * Clone this branch * Install dependencies with `yarn install` * Test the behaviour in newer and older projects by calling `cli.js` directly from the cloned repository as described in `CONTRIBUTING.md`. Screenshots: --------- Linking a module in an older project: ![Screenshot from 2019-03-25 19-22-14](https://user-images.githubusercontent.com/1713151/54944577-1a523680-4f34-11e9-8d28-5c4b3e00b8f9.png) Output when a project is up-to-date (showing debug messages as normally there would be no output): ![Screenshot from 2019-03-25 19-35-42](https://user-images.githubusercontent.com/1713151/54945093-55a13500-4f35-11e9-879e-69780ae7a59c.png)
- Loading branch information
1 parent
7dec654
commit 77944a8
Showing
5 changed files
with
272 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
packages/cli/src/tools/releaseChecker/getLatestRelease.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
/** | ||
* @flow | ||
*/ | ||
import https from 'https'; | ||
import semver from 'semver'; | ||
import logger from '../logger'; | ||
import cacheManager from './releaseCacheManager'; | ||
|
||
export type Release = { | ||
version: string, | ||
changelogUrl: string, | ||
}; | ||
|
||
/** | ||
* Checks via GitHub API if there is a newer stable React Native release and, | ||
* if it exists, returns the release data. | ||
* | ||
* If the latest release is not newer or if it's a prerelease, the function | ||
* will return undefined. | ||
*/ | ||
export default async function getLatestRelease( | ||
name: string, | ||
currentVersion: string, | ||
) { | ||
logger.debug('Checking for a newer version of React Native'); | ||
try { | ||
logger.debug(`Current version: ${currentVersion}`); | ||
|
||
const cachedLatest = cacheManager.get(name, 'latestVersion'); | ||
|
||
if (cachedLatest) { | ||
logger.debug(`Cached release version: ${cachedLatest}`); | ||
} | ||
|
||
const aWeek = 7 * 24 * 60 * 60 * 1000; | ||
const lastChecked = cacheManager.get(name, 'lastChecked'); | ||
const now = new Date(); | ||
if (lastChecked && now - new Date(lastChecked) < aWeek) { | ||
logger.debug('Cached release is still recent, skipping remote check'); | ||
return; | ||
} | ||
|
||
logger.debug('Checking for newer releases on GitHub'); | ||
const eTag = cacheManager.get(name, 'eTag'); | ||
const latestVersion = await getLatestRnDiffPurgeVersion(name, eTag); | ||
logger.debug(`Latest release: ${latestVersion}`); | ||
|
||
if ( | ||
semver.compare(latestVersion, currentVersion) === 1 && | ||
!semver.prerelease(latestVersion) | ||
) { | ||
return { | ||
version: latestVersion, | ||
changelogUrl: buildChangelogUrl(latestVersion), | ||
}; | ||
} | ||
} catch (e) { | ||
logger.debug( | ||
'Something went wrong with remote version checking, moving on', | ||
); | ||
logger.debug(e); | ||
} | ||
} | ||
|
||
function buildChangelogUrl(version: string) { | ||
return `https://github.com/facebook/react-native/releases/tag/v${version}`; | ||
} | ||
|
||
/** | ||
* Returns the most recent React Native version available to upgrade to. | ||
*/ | ||
async function getLatestRnDiffPurgeVersion(name: string, eTag: ?string) { | ||
const options = { | ||
hostname: 'api.github.com', | ||
path: '/repos/react-native-community/rn-diff-purge/tags', | ||
// https://developer.github.com/v3/#user-agent-required | ||
headers: ({'User-Agent': 'React-Native-CLI'}: Headers), | ||
}; | ||
|
||
if (eTag) { | ||
options.headers['If-None-Match'] = eTag; | ||
} | ||
|
||
const response = await httpsGet(options); | ||
|
||
// Remote is newer. | ||
if (response.statusCode === 200) { | ||
const latestVersion = JSON.parse(response.body)[0].name.substring(8); | ||
|
||
// Update cache only if newer release is stable. | ||
if (!semver.prerelease(latestVersion)) { | ||
logger.debug(`Saving ${response.eTag} to cache`); | ||
cacheManager.set(name, 'eTag', response.eTag); | ||
cacheManager.set(name, 'latestVersion', latestVersion); | ||
} | ||
|
||
return latestVersion; | ||
} | ||
|
||
// Cache is still valid. | ||
if (response.statusCode === 304) { | ||
const latestVersion = cacheManager.get(name, 'latestVersion'); | ||
if (latestVersion) { | ||
return latestVersion; | ||
} | ||
} | ||
|
||
// Should be returned only if something went wrong. | ||
return '0.0.0'; | ||
} | ||
|
||
type Headers = { | ||
'User-Agent': string, | ||
[header: string]: string, | ||
}; | ||
|
||
function httpsGet(options: any) { | ||
return new Promise((resolve, reject) => { | ||
https | ||
.get(options, result => { | ||
let body = ''; | ||
|
||
result.setEncoding('utf8'); | ||
result.on('data', data => { | ||
body += data; | ||
}); | ||
|
||
result.on('end', () => { | ||
resolve({ | ||
body, | ||
eTag: result.headers.etag, | ||
statusCode: result.statusCode, | ||
}); | ||
}); | ||
|
||
result.on('error', error => reject(error)); | ||
}) | ||
.on('error', error => reject(error)); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
/** | ||
* @flow | ||
*/ | ||
import chalk from 'chalk'; | ||
import logger from '../logger'; | ||
import type {Release} from './getLatestRelease'; | ||
import cacheManager from './releaseCacheManager'; | ||
|
||
/** | ||
* Notifies the user that a newer version of React Native is available. | ||
*/ | ||
export default function printNewRelease( | ||
name: string, | ||
latestRelease: Release, | ||
currentVersion: string, | ||
) { | ||
logger.info( | ||
`React Native v${ | ||
latestRelease.version | ||
} is now available (your project is running on v${currentVersion}).`, | ||
); | ||
logger.info(`Changelog: ${chalk.dim.underline(latestRelease.changelogUrl)}.`); | ||
logger.info(`To upgrade, run "${chalk.bold('react-native upgrade')}".`); | ||
|
||
cacheManager.set(name, 'lastChecked', new Date().toISOString()); | ||
} |
69 changes: 69 additions & 0 deletions
69
packages/cli/src/tools/releaseChecker/releaseCacheManager.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
/** | ||
* @flow | ||
*/ | ||
import path from 'path'; | ||
import fs from 'fs'; | ||
import os from 'os'; | ||
import mkdirp from 'mkdirp'; | ||
import logger from '../logger'; | ||
|
||
type ReleaseCacheKey = 'eTag' | 'lastChecked' | 'latestVersion'; | ||
type Cache = {[key: ReleaseCacheKey]: string}; | ||
|
||
function loadCache(name: string): ?Cache { | ||
try { | ||
const cacheRaw = fs.readFileSync( | ||
path.resolve(getCacheRootPath(), name), | ||
'utf8', | ||
); | ||
const cache = JSON.parse(cacheRaw); | ||
return cache; | ||
} catch (e) { | ||
if (e.code === 'ENOENT') { | ||
// Create cache file since it doesn't exist. | ||
saveCache(name, {}); | ||
} | ||
logger.debug('No release cache found'); | ||
} | ||
} | ||
|
||
function saveCache(name: string, cache: Cache) { | ||
fs.writeFileSync( | ||
path.resolve(getCacheRootPath(), name), | ||
JSON.stringify(cache, null, 2), | ||
); | ||
} | ||
|
||
/** | ||
* Returns the path string of `$HOME/.react-native-cli`. | ||
* | ||
* In case it doesn't exist, it will be created. | ||
*/ | ||
function getCacheRootPath() { | ||
const cachePath = path.resolve(os.homedir(), '.react-native-cli', 'cache'); | ||
if (!fs.existsSync(cachePath)) { | ||
mkdirp(cachePath); | ||
} | ||
|
||
return cachePath; | ||
} | ||
|
||
function get(name: string, key: ReleaseCacheKey): ?string { | ||
const cache = loadCache(name); | ||
if (cache) { | ||
return cache[key]; | ||
} | ||
} | ||
|
||
function set(name: string, key: ReleaseCacheKey, value: string) { | ||
const cache = loadCache(name); | ||
if (cache) { | ||
cache[key] = value; | ||
saveCache(name, cache); | ||
} | ||
} | ||
|
||
export default { | ||
get, | ||
set, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters