diff --git a/jest/setupUnitTests.js b/jest/setupUnitTests.js index a6ea7c372..a48825526 100644 --- a/jest/setupUnitTests.js +++ b/jest/setupUnitTests.js @@ -5,6 +5,7 @@ jest.mock('@react-native-community/cli-tools', () => { return { ...jest.requireActual('@react-native-community/cli-tools'), + fetch: jest.fn(), logger: { success: jest.fn(), info: jest.fn(), diff --git a/packages/cli/src/commands/init/__tests__/templateName.test.js b/packages/cli/src/commands/init/__tests__/templateName.test.js index 27fc9c44d..d374637dc 100644 --- a/packages/cli/src/commands/init/__tests__/templateName.test.js +++ b/packages/cli/src/commands/init/__tests__/templateName.test.js @@ -1,8 +1,5 @@ // @flow import {processTemplateName} from '../templateName'; -import {fetch} from '../../../tools/fetch'; - -jest.mock('../../../tools/fetch', () => ({fetch: jest.fn()})); const RN_NPM_PACKAGE = 'react-native'; const ABS_RN_PATH = '/path/to/react-native'; diff --git a/packages/cli/src/commands/init/templateName.js b/packages/cli/src/commands/init/templateName.js index 42ff684a9..fc0c5c767 100644 --- a/packages/cli/src/commands/init/templateName.js +++ b/packages/cli/src/commands/init/templateName.js @@ -1,10 +1,8 @@ // @flow import path from 'path'; import {URL} from 'url'; -import {fetch} from '../../tools/fetch'; const FILE_PROTOCOL = /file:/; -const HTTP_PROTOCOL = /https?:/; const TARBALL = /\.tgz$/; const VERSION_POSTFIX = /(.*)(-\d+\.\d+\.\d+)/; const VERSIONED_PACKAGE = /(@?.+)(@)(.+)/; diff --git a/packages/cli/src/commands/upgrade/__tests__/upgrade.test.js b/packages/cli/src/commands/upgrade/__tests__/upgrade.test.js index 9f5e11826..af689b03e 100644 --- a/packages/cli/src/commands/upgrade/__tests__/upgrade.test.js +++ b/packages/cli/src/commands/upgrade/__tests__/upgrade.test.js @@ -5,8 +5,7 @@ import fs from 'fs'; import snapshotDiff from 'snapshot-diff'; import stripAnsi from 'strip-ansi'; import upgrade from '../upgrade'; -import {fetch} from '../../../tools/fetch'; -import logger from '../../../tools/logger'; +import {fetch, logger} from '@react-native-community/cli-tools'; import loadConfig from '../../../tools/config'; import merge from '../../../tools/merge'; @@ -43,11 +42,9 @@ jest.mock('../../../tools/packageManager', () => ({ mockPushLog('$ yarn add', ...args); }, })); -jest.mock('../../../tools/fetch', () => ({ - fetch: jest.fn(() => Promise.resolve('patch')), -})); jest.mock('@react-native-community/cli-tools', () => ({ ...jest.requireActual('@react-native-community/cli-tools'), + fetch: jest.fn(), logger: { info: jest.fn((...args) => mockPushLog('info', args)), error: jest.fn((...args) => mockPushLog('error', args)), @@ -58,6 +55,10 @@ jest.mock('@react-native-community/cli-tools', () => ({ }, })); +const mockFetch = (value = '', status = 200) => { + (fetch: any).mockImplementation(() => Promise.resolve({data: value, status})); +}; + const mockExecaDefault = (command, args) => { mockPushLog('$', 'execa', command, args); if (command === 'npm' && args[3] === '--json') { @@ -121,7 +122,7 @@ test('uses latest version of react-native when none passed', async () => { }, 60000); test('applies patch in current working directory when nested', async () => { - (fetch: any).mockImplementation(() => Promise.resolve(samplePatch)); + mockFetch(samplePatch, 200); (execa: any).mockImplementation(mockExecaNested); const config = {...ctx, root: '/project/root/NestedApp'}; await upgrade.func([newVersion], config, opts); @@ -162,7 +163,7 @@ test('warns when dependency upgrade version is in semver range', async () => { }, 60000); test('fetches empty patch and installs deps', async () => { - (fetch: any).mockImplementation(() => Promise.resolve('')); + mockFetch(); await upgrade.func([newVersion], ctx, opts); expect(flushOutput()).toMatchInlineSnapshot(` "info Fetching diff between v0.57.8 and v0.58.4... @@ -178,7 +179,7 @@ test('fetches empty patch and installs deps', async () => { }, 60000); test('fetches regular patch, adds remote, applies patch, installs deps, removes remote,', async () => { - (fetch: any).mockImplementation(() => Promise.resolve(samplePatch)); + mockFetch(samplePatch, 200); await upgrade.func( [newVersion], merge(ctx, { @@ -217,7 +218,7 @@ test('fetches regular patch, adds remote, applies patch, installs deps, removes ); }, 60000); test('fetches regular patch, adds remote, applies patch, installs deps, removes remote when updated from nested directory', async () => { - (fetch: any).mockImplementation(() => Promise.resolve(samplePatch)); + mockFetch(samplePatch, 200); (execa: any).mockImplementation(mockExecaNested); const config = {...ctx, root: '/project/root/NestedApp'}; await upgrade.func([newVersion], config, opts); @@ -242,7 +243,7 @@ test('fetches regular patch, adds remote, applies patch, installs deps, removes `); }, 60000); test('cleans up if patching fails,', async () => { - (fetch: any).mockImplementation(() => Promise.resolve(samplePatch)); + mockFetch(samplePatch, 200); (execa: any).mockImplementation((command, args) => { mockPushLog('$', 'execa', command, args); if (command === 'npm' && args[3] === '--json') { @@ -296,7 +297,7 @@ test('cleans up if patching fails,', async () => { `); }, 60000); test('works with --name-ios and --name-android', async () => { - (fetch: any).mockImplementation(() => Promise.resolve(samplePatch)); + mockFetch(samplePatch, 200); await upgrade.func( [newVersion], merge(ctx, { diff --git a/packages/cli/src/commands/upgrade/upgrade.js b/packages/cli/src/commands/upgrade/upgrade.js index d3cbb5085..b20b788e9 100644 --- a/packages/cli/src/commands/upgrade/upgrade.js +++ b/packages/cli/src/commands/upgrade/upgrade.js @@ -5,9 +5,8 @@ import chalk from 'chalk'; import semver from 'semver'; import execa from 'execa'; import type {ConfigT} from 'types'; -import {logger, CLIError} from '@react-native-community/cli-tools'; +import {logger, CLIError, fetch} from '@react-native-community/cli-tools'; import * as PackageManager from '../../tools/packageManager'; -import {fetch} from '../../tools/fetch'; import legacyUpgrade from './legacyUpgrade'; type FlagsT = { @@ -46,8 +45,13 @@ const getPatch = async (currentVersion, newVersion, config) => { logger.info(`Fetching diff between v${currentVersion} and v${newVersion}...`); try { - patch = await fetch(`${rawDiffUrl}/${currentVersion}..${newVersion}.diff`); + const {data} = await fetch( + `${rawDiffUrl}/${currentVersion}..${newVersion}.diff`, + ); + + patch = data; } catch (error) { + logger.error(error.message); logger.error( `Failed to fetch diff for react-native@${newVersion}. Maybe it's not released yet?`, ); diff --git a/packages/cli/src/tools/fetch.js b/packages/cli/src/tools/fetch.js deleted file mode 100644 index 5c9ad2fc2..000000000 --- a/packages/cli/src/tools/fetch.js +++ /dev/null @@ -1,17 +0,0 @@ -// @flow -import https from 'https'; - -export const fetch = (url: string) => - new Promise((resolve, reject) => { - const request = https.get(url, response => { - if (response.statusCode < 200 || response.statusCode > 299) { - reject( - new Error(`Failed to load page, status code: ${response.statusCode}`), - ); - } - const body = []; - response.on('data', chunk => body.push(chunk)); - response.on('end', () => resolve(body.join(''))); - }); - request.on('error', err => reject(err)); - }); diff --git a/packages/cli/src/tools/releaseChecker/getLatestRelease.js b/packages/cli/src/tools/releaseChecker/getLatestRelease.js index 7f41a2304..d3980e8a2 100644 --- a/packages/cli/src/tools/releaseChecker/getLatestRelease.js +++ b/packages/cli/src/tools/releaseChecker/getLatestRelease.js @@ -1,10 +1,10 @@ /** * @flow */ -import https from 'https'; import semver from 'semver'; import logger from '../logger'; import cacheManager from './releaseCacheManager'; +import {fetch} from '@react-native-community/cli-tools'; export type Release = { version: string, @@ -71,8 +71,6 @@ function buildChangelogUrl(version: string) { */ 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), }; @@ -81,16 +79,22 @@ async function getLatestRnDiffPurgeVersion(name: string, eTag: ?string) { options.headers['If-None-Match'] = eTag; } - const response = await httpsGet(options); + const {data, status, headers} = await fetch( + 'https://api.github.com/repos/react-native-community/rn-diff-purge/tags', + options, + ); // Remote is newer. - if (response.statusCode === 200) { - const latestVersion = JSON.parse(response.body)[0].name.substring(8); + if (status === 200) { + const body: Array = data; + const latestVersion = 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); + const eTagHeader = headers.get('eTag'); + + logger.debug(`Saving ${eTagHeader} to cache`); + cacheManager.set(name, 'eTag', eTagHeader); cacheManager.set(name, 'latestVersion', latestVersion); } @@ -98,7 +102,7 @@ async function getLatestRnDiffPurgeVersion(name: string, eTag: ?string) { } // Cache is still valid. - if (response.statusCode === 304) { + if (status === 304) { const latestVersion = cacheManager.get(name, 'latestVersion'); if (latestVersion) { return latestVersion; @@ -113,28 +117,3 @@ type Headers = { 'User-Agent': mixed, [header: string]: mixed, }; - -function httpsGet(options) { - 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)); - }); -} diff --git a/packages/tools/src/fetch.ts b/packages/tools/src/fetch.ts new file mode 100644 index 000000000..73cc8cdd5 --- /dev/null +++ b/packages/tools/src/fetch.ts @@ -0,0 +1,37 @@ +import nodeFetch, { + RequestInit as FetchOptions, + Response, + Request, + Headers, +} from 'node-fetch'; +import {CLIError} from './errors'; + +async function unwrapFetchResult(response: Response) { + const data = await response.text(); + + try { + return JSON.parse(data); + } catch (e) { + return data; + } +} + +export default async function fetch( + url: string | Request, + options?: FetchOptions, +): Promise<{status: number, data: any, headers: Headers}> { + const result = await nodeFetch(url, options); + const data = await unwrapFetchResult(result); + + if (result.status >= 400) { + throw new CLIError( + `Fetch request failed with status ${result.status}: ${data}.`, + ); + } + + return { + status: result.status, + headers: result.headers, + data, + }; +} diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index d99e20bf0..0a6f4fafa 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -2,5 +2,6 @@ export {default as logger} from './logger'; export {default as groupFilesByType} from './groupFilesByType'; export {default as isPackagerRunning} from './isPackagerRunning'; export {default as getDefaultUserTerminal} from './getDefaultUserTerminal'; +export {default as fetch} from './fetch'; export * from './errors'; diff --git a/packages/tools/src/isPackagerRunning.ts b/packages/tools/src/isPackagerRunning.ts index 11b276368..ad97fb9ca 100644 --- a/packages/tools/src/isPackagerRunning.ts +++ b/packages/tools/src/isPackagerRunning.ts @@ -6,7 +6,7 @@ * */ -import fetch from 'node-fetch'; +import fetch from './fetch'; /** * Indicates whether or not the packager is running. It returns a promise that @@ -19,10 +19,9 @@ async function isPackagerRunning( packagerPort: string = process.env.RCT_METRO_PORT || '8081', ): Promise<'running' | 'not_running' | 'unrecognized'> { try { - const result = await fetch(`http://localhost:${packagerPort}/status`); - const body = await result.text(); + const {data} = await fetch(`http://localhost:${packagerPort}/status`); - return body === 'packager-status:running' ? 'running' : 'unrecognized'; + return data === 'packager-status:running' ? 'running' : 'unrecognized'; } catch (_error) { return 'not_running'; }