Skip to content

Commit

Permalink
feat: add React Native support
Browse files Browse the repository at this point in the history
  • Loading branch information
acostalima committed Feb 1, 2021
1 parent 4ccbc5c commit 1a32836
Show file tree
Hide file tree
Showing 15 changed files with 340 additions and 44 deletions.
32 changes: 17 additions & 15 deletions .aegir.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
'use strict'

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

let echo = new EchoServer()

module.exports = {
hooks: {
pre: async () => {
const server = await echo.start()
const { address, port } = server.server.address()
return {
env: { ECHO_SERVER : format({ protocol: 'http:', hostname: address, port })}
}
},
post: async () => {
await echo.stop()
}
}
}
// module.exports = {
// hooks: {
// pre: async () => {
// const server = await echo.start()
// const { address, port } = server.server.address()
// return {
// env: { ECHO_SERVER : format({ protocol: 'http:', hostname: address, port })}
// }
// },
// post: async () => {
// await echo.stop()
// }
// }
// }

echo.start()
69 changes: 58 additions & 11 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm install
- run: npx aegir lint
- uses: gozala/typescript-error-reporter-action@v1.0.8
- run: npx aegir build
- run: npx aegir dep-check
- uses: ipfs/aegir/actions/bundle-size@master
name: size
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v2
- run: npm install
- run: npx aegir lint
- uses: gozala/typescript-error-reporter-action@v1.0.8
- run: npx aegir build
- run: npx aegir dep-check
- uses: ipfs/aegir/actions/bundle-size@master
name: size
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
test-node:
needs: check
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -64,4 +64,51 @@ jobs:
steps:
- uses: actions/checkout@v2
- run: npm install
- run: npx xvfb-maybe aegir test -t electron-renderer --bail
- run: npx xvfb-maybe aegir test -t electron-renderer --bail
test-react-native-android:
runs-on: macos-latest
env:
CI: 1
strategy:
matrix:
android-api: [28]
android-target: [default]
node: [12, 14]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- run: npm install
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.android-api }}
target: ${{ matrix.android-target }}
arch: x86_64
profile: pixel
script: |
npx aegir test -t react-native-android
test-react-native-ios:
runs-on: macos-latest
env:
CI: 1
strategy:
matrix:
apple-runtime: ["iOS 14.2"]
node: [12, 14]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- run: npm install
- name: Create and run iOS simulator
run: |
SIMULATOR_RUNTIME=$(echo "${{ matrix.apple-runtime }}" | sed 's/[ \.]/-/g')
SIMULATOR_ID=$(xcrun simctl create "iPhone 11" com.apple.CoreSimulator.SimDeviceType.iPhone-11 com.apple.CoreSimulator.SimRuntime.$SIMULATOR_RUNTIME)
echo "IOS_SIMULATOR=$SIMULATOR_ID" >> $GITHUB_ENV
xcrun simctl boot $SIMULATOR_ID &
- run: npx aegir test -t react-native-ios
- name: Shutdown iOS simulator
run: |
xcrun simctl shutdown $IOS_SIMULATOR
17 changes: 15 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
"./test/files/glob-source.spec.js": false,
"electron-fetch": false
},
"react-native": {
"./src/http/fetch.js": "./src/http/fetch.react-native.js",
"./src/temp-dir.js": "./src/temp-dir.browser.js",
"./src/path-join.js": "./src/path-join.browser.js"
},
"types": "dist/src/index.d.ts",
"typesVersions": {
"*": {
Expand All @@ -34,6 +39,8 @@
"test:node": "aegir test -t node",
"test:electron": "aegir test -t electron-main",
"test:electron-renderer": "aegir test -t electron-renderer",
"test:react-native:android": "aegir test -t react-native-android",
"test:react-native:ios": "aegir test -t react-native-ios",
"lint": "aegir lint",
"release": "aegir release --docs",
"release-minor": "aegir release --type minor --docs",
Expand All @@ -57,6 +64,7 @@
"native-abort-controller": "0.0.3",
"native-fetch": "2.0.1",
"node-fetch": "^2.6.1",
"react-native-fetch-api": "^1.0.2",
"stream-to-it": "^0.2.2",
"web-encoding": "^1.0.6"
},
Expand All @@ -68,7 +76,12 @@
"it-all": "^1.0.4",
"it-drain": "^1.0.3",
"it-last": "^1.0.4",
"uint8arrays": "^2.0.5"
"react-native-polyfill-globals": "^3.0.0",
"react-native-test-runner": "^2.3.0",
"react-native-url-polyfill": "^1.2.0",
"text-encoding": "^0.7.0",
"uint8arrays": "^2.0.5",
"web-streams-polyfill": "^3.0.1"
},
"eslintConfig": {
"extends": "ipfs",
Expand All @@ -86,4 +99,4 @@
"Marcin Rataj <lidel@lidel.org>",
"Roderik van der Veer <roderik@settlemint.com>"
]
}
}
12 changes: 12 additions & 0 deletions rn-test.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict'

module.exports = {
require: require.resolve('./rn-test.require.js'),
runner: 'mocha',
patches: [{
path: require.resolve('react-native-polyfill-globals/patches/react-native+0.63.3.patch')
}, {
path: require.resolve('react-native-polyfill-globals/patches/react-native-url-polyfill+1.2.0.patch'),
cwd: __dirname
}]
}
9 changes: 9 additions & 0 deletions rn-test.require.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict'

const { polyfill: polyfillReadableStream } = require('react-native-polyfill-globals/src/readable-stream')
const { polyfill: polyfillEncoding } = require('react-native-polyfill-globals/src/encoding')
const { polyfill: polyfillURL } = require('react-native-polyfill-globals/src/url')

polyfillReadableStream()
polyfillEncoding()
polyfillURL()
4 changes: 3 additions & 1 deletion src/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const IS_NODE = typeof require === 'function' && typeof process !== 'undefined'
// @ts-ignore - we either ignore worker scope or dom scope
const IS_WEBWORKER = typeof importScripts === 'function' && typeof self !== 'undefined' && typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope
const IS_TEST = typeof process !== 'undefined' && typeof process.env !== 'undefined' && process.env.NODE_ENV === 'test'
const IS_REACT_NATIVE = typeof navigator !== 'undefined' && navigator.product === 'ReactNative'

module.exports = {
isTest: IS_TEST,
Expand All @@ -22,5 +23,6 @@ module.exports = {
*/
isBrowser: IS_BROWSER,
isWebWorker: IS_WEBWORKER,
isEnvWithDom: IS_ENV_WITH_DOM
isEnvWithDom: IS_ENV_WITH_DOM,
isReactNative: IS_REACT_NATIVE
}
6 changes: 4 additions & 2 deletions src/fetch.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
'use strict'

const { isElectronMain } = require('./env')
const { isElectronMain, isReactNative } = require('./env')

if (isElectronMain) {
if (isReactNative) {
module.exports = require('react-native-fetch-api')
} else if (isElectronMain) {
module.exports = require('electron-fetch')
} else {
// use window.fetch if it is available, fall back to node-fetch if not
Expand Down
10 changes: 7 additions & 3 deletions src/http/fetch.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
'use strict'

// Electron has `XMLHttpRequest` and should get the browser implementation
// instead of node.
if (typeof XMLHttpRequest === 'function') {
const { isReactNative } = require('../env')

if (isReactNative) {
module.exports = require('./fetch.react-native')
} else if (typeof XMLHttpRequest === 'function') {
// Electron has `XMLHttpRequest` and should get the browser implementation
// instead of node.
module.exports = require('./fetch.browser')
} else {
module.exports = require('./fetch.node')
Expand Down
136 changes: 136 additions & 0 deletions src/http/fetch.react-native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
'use strict'

const { TimeoutError, AbortError } = require('./error')
const { Response, Request, Headers, fetch } = require('../fetch')

/**
* @typedef {import('../types').FetchOptions} FetchOptions
* @typedef {import('../types').ProgressFn} ProgressFn
*/

/**
* Fetch with progress
*
* @param {string | Request} url
* @param {FetchOptions} [options]
* @returns {Promise<Response>}
*/
const fetchWithProgress = (url, options = {}) => {
const request = new XMLHttpRequest()
request.open(options.method || 'GET', url.toString(), true)

const { timeout, headers } = options

if (timeout && timeout > 0 && timeout < Infinity) {
request.timeout = timeout
}

if (options.overrideMimeType != null) {
request.overrideMimeType(options.overrideMimeType)
}

if (headers) {
for (const [name, value] of new Headers(headers)) {
request.setRequestHeader(name, value)
}
}

if (options.signal) {
options.signal.onabort = () => request.abort()
}

if (options.onUploadProgress) {
request.upload.onprogress = options.onUploadProgress
}

request.responseType = 'blob'

return new Promise((resolve, reject) => {
/**
* @param {Event} event
*/
const handleEvent = (event) => {
switch (event.type) {
case 'error': {
resolve(Response.error())
break
}
case 'load': {
resolve(
new ResponseWithURL(request.responseURL, request.response, {
status: request.status,
statusText: request.statusText,
headers: parseHeaders(request.getAllResponseHeaders())
})
)
break
}
case 'timeout': {
reject(new TimeoutError())
break
}
case 'abort': {
reject(new AbortError())
break
}
default: {
break
}
}
}
request.onerror = handleEvent
request.onload = handleEvent
request.ontimeout = handleEvent
request.onabort = handleEvent

request.send(/** @type {BodyInit} */(options.body))
})
}

const fetchWithStreaming = fetch

/**
* @param {string | Request} url
* @param {FetchOptions} options
*/
const fetchWith = (url, options = {}) =>
(options.onUploadProgress != null)
? fetchWithProgress(url, options)
: fetchWithStreaming(url, options)

/**
* Parse Headers from a XMLHttpRequest
*
* @param {string} input
* @returns {Headers}
*/
const parseHeaders = (input) => {
const headers = new Headers()
for (const line of input.trim().split(/[\r\n]+/)) {
const index = line.indexOf(': ')
if (index > 0) {
headers.set(line.slice(0, index), line.slice(index + 1))
}
}

return headers
}

class ResponseWithURL extends Response {
/**
* @param {string} url
* @param {BodyInit} body
* @param {ResponseInit} options
*/
constructor (url, body, options) {
super(body, options)
Object.defineProperty(this, 'url', { value: url })
}
}

module.exports = {
fetch: fetchWith,
Request,
Headers,
ResponseWithURL
}
1 change: 1 addition & 0 deletions src/supports.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

module.exports = {
// in React Native: global === window === self
supportsFileReader: typeof self !== 'undefined' && 'FileReader' in self,
supportsWebRTC: 'RTCPeerConnection' in globalThis &&
(typeof navigator !== 'undefined' && typeof navigator.mediaDevices !== 'undefined' && 'getUserMedia' in navigator.mediaDevices),
Expand Down
1 change: 1 addition & 0 deletions src/text-decoder.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use strict'

// This is fine for React Native as long as the environment is polyfilled with text-encoding
module.exports = require('web-encoding').TextDecoder
5 changes: 2 additions & 3 deletions src/text-encoder.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
'use strict'

const { TextEncoder } = require('web-encoding')

module.exports = TextEncoder
// This is fine for React Native as long as the environment is polyfilled with text-encoding
module.exports = require('web-encoding').TextEncoder
Loading

0 comments on commit 1a32836

Please sign in to comment.