Skip to content

Commit f559f98

Browse files
committed
feat: add React Native support
1 parent 909caae commit f559f98

File tree

14 files changed

+316
-44
lines changed

14 files changed

+316
-44
lines changed

.aegir.js

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
'use strict'
22

33
const EchoServer = require('aegir/utils/echo-server')
4-
const { format } =require('iso-url')
4+
const { format } = require('iso-url')
55

66
let echo = new EchoServer()
77

8-
module.exports = {
9-
hooks: {
10-
pre: async () => {
11-
const server = await echo.start()
12-
const { address, port } = server.server.address()
13-
return {
14-
env: { ECHO_SERVER : format({ protocol: 'http:', hostname: address, port })}
15-
}
16-
},
17-
post: async () => {
18-
await echo.stop()
19-
}
20-
}
21-
}
8+
// module.exports = {
9+
// hooks: {
10+
// pre: async () => {
11+
// const server = await echo.start()
12+
// const { address, port } = server.server.address()
13+
// return {
14+
// env: { ECHO_SERVER : format({ protocol: 'http:', hostname: address, port })}
15+
// }
16+
// },
17+
// post: async () => {
18+
// await echo.stop()
19+
// }
20+
// }
21+
// }
22+
23+
echo.start()

.github/workflows/main.yml

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ jobs:
1111
check:
1212
runs-on: ubuntu-latest
1313
steps:
14-
- uses: actions/checkout@v2
15-
- run: npm install
16-
- run: npx aegir lint
17-
- uses: gozala/[email protected]
18-
- run: npx aegir build
19-
- run: npx aegir dep-check
20-
- uses: ipfs/aegir/actions/bundle-size@master
21-
name: size
22-
with:
23-
github_token: ${{ secrets.GITHUB_TOKEN }}
14+
- uses: actions/checkout@v2
15+
- run: npm install
16+
- run: npx aegir lint
17+
- uses: gozala/[email protected]
18+
- run: npx aegir build
19+
- run: npx aegir dep-check
20+
- uses: ipfs/aegir/actions/bundle-size@master
21+
name: size
22+
with:
23+
github_token: ${{ secrets.GITHUB_TOKEN }}
2424
test-node:
2525
needs: check
2626
runs-on: ${{ matrix.os }}
@@ -64,4 +64,33 @@ jobs:
6464
steps:
6565
- uses: actions/checkout@v2
6666
- run: npm install
67-
- run: npx xvfb-maybe aegir test -t electron-renderer --bail
67+
- run: npx xvfb-maybe aegir test -t electron-renderer --bail
68+
test-react-native-android:
69+
runs-on: macos-latest
70+
steps:
71+
- uses: actions/checkout@v2
72+
- run: npm install
73+
- uses: reactivecircus/android-emulator-runner@v2
74+
with:
75+
api-level: 28
76+
target: default
77+
arch: x86_64
78+
profile: pixel
79+
avd-name: google-pixel
80+
script: |
81+
npx rn-test --platform android --emulator google-pixel 'test/**/*.spec.js'
82+
test-react-native-ios:
83+
runs-on: macos-latest
84+
steps:
85+
- uses: actions/checkout@v2
86+
- run: npm install
87+
- name: Create and run iOS simulator
88+
run: |
89+
SIMULATOR_RUNTIME=$(echo "iOS 14.4" | sed 's/[ \.]/-/g')
90+
SIMULATOR_ID=$(xcrun simctl create "iPhone 11" com.apple.CoreSimulator.SimDeviceType.iPhone-11 com.apple.CoreSimulator.SimRuntime.$SIMULATOR_RUNTIME)
91+
echo "IOS_SIMULATOR=$SIMULATOR_ID" >> $GITHUB_ENV
92+
xcrun simctl boot $SIMULATOR_ID &
93+
- run: npx rn-test --platform ios --simulator 'iPhone 11 (14.4)' --rn 0.62.0 'test/**/*.spec.js'
94+
- name: Shutdown iOS simulator
95+
run: |
96+
xcrun simctl shutdown $IOS_SIMULATOR

package.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
"./test/files/glob-source.spec.js": false,
1919
"electron-fetch": false
2020
},
21+
"react-native": {
22+
"./src/http/fetch.js": "./src/http/fetch.react-native.js"
23+
},
2124
"types": "dist/src/index.d.ts",
2225
"typesVersions": {
2326
"*": {
@@ -34,6 +37,8 @@
3437
"test:node": "aegir test -t node",
3538
"test:electron": "aegir test -t electron-main",
3639
"test:electron-renderer": "aegir test -t electron-renderer",
40+
"test:react-native:android": "aegir test -t react-native-android",
41+
"test:react-native:ios": "aegir test -t react-native-ios",
3742
"lint": "aegir lint",
3843
"release": "aegir release --docs",
3944
"release-minor": "aegir release --type minor --docs",
@@ -49,16 +54,17 @@
4954
"err-code": "^2.0.3",
5055
"fs-extra": "^9.0.1",
5156
"is-electron": "^2.2.0",
52-
"iso-url": "^1.0.0",
57+
"iso-url": "^1.1.3",
5358
"it-glob": "0.0.10",
5459
"it-to-stream": "^0.1.2",
5560
"merge-options": "^3.0.4",
5661
"nanoid": "^3.1.20",
5762
"native-abort-controller": "^1.0.3",
5863
"native-fetch": "2.0.1",
5964
"node-fetch": "^2.6.1",
65+
"react-native-fetch-api": "^1.0.2",
6066
"stream-to-it": "^0.2.2",
61-
"web-encoding": "^1.0.6"
67+
"web-encoding": "^1.1.0"
6268
},
6369
"devDependencies": {
6470
"@types/err-code": "^2.0.0",
@@ -68,6 +74,8 @@
6874
"it-all": "^1.0.4",
6975
"it-drain": "^1.0.3",
7076
"it-last": "^1.0.4",
77+
"react-native-polyfill-globals": "^3.0.0",
78+
"react-native-test-runner": "^3.0.2",
7179
"uint8arrays": "^2.0.5"
7280
},
7381
"eslintConfig": {

rn-test.config.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict'
2+
3+
module.exports = {
4+
require: require.resolve('./rn-test.require.js'),
5+
runner: 'mocha',
6+
modules: [
7+
'react-native-url-polyfill',
8+
'web-streams-polyfill'
9+
],
10+
patches: [{
11+
path: require.resolve('react-native-polyfill-globals/patches/react-native+0.63.3.patch')
12+
}, {
13+
path: require.resolve('react-native-polyfill-globals/patches/react-native-url-polyfill+1.2.0.patch')
14+
}]
15+
}

rn-test.require.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict'
2+
3+
const { polyfill: polyfillReadableStream } = require('react-native-polyfill-globals/src/readable-stream')
4+
const { polyfill: polyfillURL } = require('react-native-polyfill-globals/src/url')
5+
6+
polyfillURL()
7+
polyfillReadableStream()

src/env.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const IS_NODE = typeof require === 'function' && typeof process !== 'undefined'
1010
// @ts-ignore - we either ignore worker scope or dom scope
1111
const IS_WEBWORKER = typeof importScripts === 'function' && typeof self !== 'undefined' && typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope
1212
const IS_TEST = typeof process !== 'undefined' && typeof process.env !== 'undefined' && process.env.NODE_ENV === 'test'
13+
const IS_REACT_NATIVE = typeof navigator !== 'undefined' && navigator.product === 'ReactNative'
1314

1415
module.exports = {
1516
isTest: IS_TEST,
@@ -22,5 +23,6 @@ module.exports = {
2223
*/
2324
isBrowser: IS_BROWSER,
2425
isWebWorker: IS_WEBWORKER,
25-
isEnvWithDom: IS_ENV_WITH_DOM
26+
isEnvWithDom: IS_ENV_WITH_DOM,
27+
isReactNative: IS_REACT_NATIVE
2628
}

src/fetch.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
'use strict'
22

3-
const { isElectronMain } = require('./env')
3+
const { isElectronMain, isReactNative } = require('./env')
44

5-
if (isElectronMain) {
5+
if (isReactNative) {
6+
module.exports = require('react-native-fetch-api')
7+
} else if (isElectronMain) {
68
module.exports = require('electron-fetch')
79
} else {
810
// use window.fetch if it is available, fall back to node-fetch if not

src/http/fetch.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
'use strict'
22

3-
// Electron has `XMLHttpRequest` and should get the browser implementation
4-
// instead of node.
5-
if (typeof XMLHttpRequest === 'function') {
3+
const { isReactNative } = require('../env')
4+
5+
if (isReactNative) {
6+
module.exports = require('./fetch.react-native')
7+
} else if (typeof XMLHttpRequest === 'function') {
8+
// Electron has `XMLHttpRequest` and should get the browser implementation
9+
// instead of node.
610
module.exports = require('./fetch.browser')
711
} else {
812
module.exports = require('./fetch.node')

src/http/fetch.react-native.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
'use strict'
2+
3+
const { TimeoutError, AbortError } = require('./error')
4+
const { Response, Request, Headers, fetch } = require('../fetch')
5+
6+
/**
7+
* @typedef {import('../types').FetchOptions} FetchOptions
8+
* @typedef {import('../types').ProgressFn} ProgressFn
9+
*/
10+
11+
/**
12+
* Fetch with progress
13+
*
14+
* @param {string | Request} url
15+
* @param {FetchOptions} [options]
16+
* @returns {Promise<ResponseWithURL>}
17+
*/
18+
const fetchWithProgress = (url, options = {}) => {
19+
const request = new XMLHttpRequest()
20+
request.open(options.method || 'GET', url.toString(), true)
21+
22+
const { timeout, headers } = options
23+
24+
if (timeout && timeout > 0 && timeout < Infinity) {
25+
request.timeout = timeout
26+
}
27+
28+
if (options.overrideMimeType != null) {
29+
request.overrideMimeType(options.overrideMimeType)
30+
}
31+
32+
if (headers) {
33+
for (const [name, value] of new Headers(headers)) {
34+
request.setRequestHeader(name, value)
35+
}
36+
}
37+
38+
if (options.signal) {
39+
options.signal.onabort = () => request.abort()
40+
}
41+
42+
if (options.onUploadProgress) {
43+
request.upload.onprogress = options.onUploadProgress
44+
}
45+
46+
request.responseType = 'blob'
47+
48+
return new Promise((resolve, reject) => {
49+
/**
50+
* @param {Event} event
51+
*/
52+
const handleEvent = (event) => {
53+
switch (event.type) {
54+
case 'error': {
55+
resolve(Response.error())
56+
break
57+
}
58+
case 'load': {
59+
resolve(
60+
new ResponseWithURL(request.responseURL, request.response, {
61+
status: request.status,
62+
statusText: request.statusText,
63+
headers: parseHeaders(request.getAllResponseHeaders())
64+
})
65+
)
66+
break
67+
}
68+
case 'timeout': {
69+
reject(new TimeoutError())
70+
break
71+
}
72+
case 'abort': {
73+
reject(new AbortError())
74+
break
75+
}
76+
default: {
77+
break
78+
}
79+
}
80+
}
81+
request.onerror = handleEvent
82+
request.onload = handleEvent
83+
request.ontimeout = handleEvent
84+
request.onabort = handleEvent
85+
86+
request.send(/** @type {BodyInit} */(options.body))
87+
})
88+
}
89+
90+
const fetchWithStreaming = fetch
91+
92+
/**
93+
* @param {string | Request} url
94+
* @param {FetchOptions} options
95+
*/
96+
const fetchWith = (url, options = {}) =>
97+
(options.onUploadProgress != null)
98+
? fetchWithProgress(url, options)
99+
: fetchWithStreaming(url, options)
100+
101+
/**
102+
* Parse Headers from a XMLHttpRequest
103+
*
104+
* @param {string} input
105+
* @returns {Headers}
106+
*/
107+
const parseHeaders = (input) => {
108+
const headers = new Headers()
109+
for (const line of input.trim().split(/[\r\n]+/)) {
110+
const index = line.indexOf(': ')
111+
if (index > 0) {
112+
headers.set(line.slice(0, index), line.slice(index + 1))
113+
}
114+
}
115+
116+
return headers
117+
}
118+
119+
class ResponseWithURL extends Response {
120+
/**
121+
* @param {string} url
122+
* @param {BodyInit} body
123+
* @param {ResponseInit} options
124+
*/
125+
constructor (url, body, options) {
126+
super(body, options)
127+
Object.defineProperty(this, 'url', { value: url })
128+
}
129+
}
130+
131+
module.exports = {
132+
fetch: fetchWith,
133+
Request,
134+
Headers,
135+
ResponseWithURL
136+
}

src/supports.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict'
22

33
module.exports = {
4+
// in React Native: global === window === self
45
supportsFileReader: typeof self !== 'undefined' && 'FileReader' in self,
56
supportsWebRTC: 'RTCPeerConnection' in globalThis &&
67
(typeof navigator !== 'undefined' && typeof navigator.mediaDevices !== 'undefined' && 'getUserMedia' in navigator.mediaDevices),

src/text-encoder.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
'use strict'
22

3-
const { TextEncoder } = require('web-encoding')
4-
5-
module.exports = TextEncoder
3+
module.exports = require('web-encoding').TextEncoder

0 commit comments

Comments
 (0)