├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── example ├── .gitignore ├── App.js ├── image.png ├── package.json └── yarn.lock ├── expo-browser-polyfill-1.0.1.tgz ├── package.json ├── src ├── DOM │ ├── Document.js │ ├── Element.js │ ├── HTMLCanvasElement.js │ ├── HTMLImageElement.js │ ├── HTMLVideoElement.js │ └── Node.js ├── console │ ├── count.js │ ├── index.js │ └── time.js ├── index.js ├── module.js ├── module.native.js ├── performance.js ├── process.js ├── resize.js └── window.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-expo"], 3 | "env": { 4 | "development": { 5 | "plugins": ["transform-react-jsx-source"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /example 3 | /node_modules 4 | test 5 | 6 | .babelrc 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present, 650 Industries, Inc. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @expo/browser-polyfill 2 | 3 | Browser polyfill for React Native 4 | 5 | ### Installation 6 | 7 | ```bash 8 | yarn add @expo/browser-polyfill 9 | ``` 10 | 11 | ### Usage 12 | 13 | Import the library into your JavaScript file: 14 | 15 | ```js 16 | import '@expo/browser-polyfill'; 17 | ``` 18 | 19 | ## Implements 20 | 21 | ## DOM 22 | 23 | DOM is provided with very low support, these are used for libs like pixi.js that validate type. 24 | 25 | ```js 26 | class Node 27 | class Element 28 | class Document 29 | class HTMLImageElement 30 | class Image 31 | class ImageBitmap 32 | class HTMLVideoElement 33 | class Video 34 | class HTMLCanvasElement 35 | class Canvas 36 | ``` 37 | 38 | ### Image, HTMLImageElement, ImageBitmap 39 | 40 | Image has support for loading callbacks, however the loaded uri must be passed to the src already. 41 | 42 | ```js 43 | const image = new Image(); 44 | image.src = ''; 45 | image.onload = () => { 46 | const { src, width, height } = image; 47 | }; 48 | image.addEventListener('loading', () => {}); 49 | image.addEventListener('error', () => {}); 50 | ``` 51 | 52 | ### Document 53 | 54 | ```js 55 | const element = document.createElement('div'); 56 | const fakeContext = element.getContext(''); 57 | ``` 58 | 59 | ### Element 60 | 61 | #### All sizes return the window size: 62 | 63 | ```js 64 | element.clientWidth; 65 | element.clientHeight; 66 | element.innerWidth; 67 | element.innerHeight; 68 | element.offsetWidth; 69 | element.offsetHeight; 70 | ``` 71 | 72 | #### Empty attributes that prevent libraries from crashing 73 | 74 | ```js 75 | element.tagName; 76 | element.addEventListener; 77 | element.removeEventListener; 78 | element.setAttributeNS; 79 | element.createElementNS; 80 | ``` 81 | 82 | ### Node 83 | 84 | ```js 85 | node.ownerDocument; 86 | node.className; 87 | node.appendChild; 88 | node.insertBefore; 89 | node.removeChild; 90 | node.setAttributeNS; 91 | node.getBoundingClientRect; 92 | ``` 93 | 94 | # External Libraries 95 | 96 | Some external node.js polyfills are added as well. 97 | 98 | ## [text-encoding](https://github.com/inexorabletash/text-encoding) 99 | 100 | ``` 101 | global.TextEncoder 102 | global.TextDecoder 103 | ``` 104 | 105 | ## [xmldom-qsa](https://github.com/zeligzhou/xmldom-qsa) 106 | 107 | ``` 108 | window.DOMParser 109 | ``` 110 | 111 | ## [react-native-console-time-polyfill](https://github.com/MaxGraey/react-native-console-time-polyfill) 112 | 113 | ``` 114 | console.time(label); 115 | console.timeEnd(label); 116 | console.count(label); 117 | ``` 118 | 119 | # Debug flags 120 | 121 | For debugging base64 image transformations toggle: 122 | 123 | ```js 124 | global.__debug_browser_polyfill_image = true; 125 | ``` 126 | 127 | By default `global.__debug_browser_polyfill_image` is false. 128 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | -------------------------------------------------------------------------------- /example/App.js: -------------------------------------------------------------------------------- 1 | import "@expo/browser-polyfill"; 2 | 3 | import { Asset } from "expo-asset"; 4 | import Constants from "expo-constants"; 5 | import * as FileSystem from "expo-file-system"; 6 | import { GLView } from "expo-gl"; 7 | import React from "react"; 8 | import { View, Text } from "react-native"; 9 | 10 | /* 11 | Issue: https://github.com/expo/browser-polyfill/issues/2 12 | Firebase doesn't work with this lib. 13 | Adding it here to test. 14 | */ 15 | import { initializeApp } from "firebase/app"; 16 | import { onAuthStateChanged, getAuth, signInAnonymously } from "firebase/auth"; 17 | import * as database from "firebase/database"; 18 | 19 | function setupFirebase() { 20 | var config = { 21 | apiKey: "AIzaSyAfgPq82VdNqEZ8hqnOvYdD7kSPiFK9W1k", 22 | authDomain: "keira-knightley-51df6.firebaseapp.com", 23 | databaseURL: "https://keira-knightley-51df6.firebaseio.com", 24 | projectId: "keira-knightley-51df6", 25 | storageBucket: "keira-knightley-51df6.appspot.com", 26 | messagingSenderId: "628588079444", 27 | }; 28 | initializeApp(config); 29 | // app.firestore().settings({ timestampsInSnapshots: true }); 30 | 31 | const _onAuthStateChanged = async (user) => { 32 | // app.database().forceDisallow(); 33 | // console.log(firebase.database()); 34 | // return; 35 | if (!user) { 36 | try { 37 | await signInAnonymously(getAuth()); 38 | } catch ({ message }) { 39 | alert(message); 40 | } 41 | } else { 42 | console.log("signed in"); 43 | const testRef = database.ref(database.getDatabase(), "/test_data"); 44 | try { 45 | const snapshot = await database.get(testRef); 46 | const key = snapshot.key; 47 | const value = snapshot.val(); 48 | console.log("Firebase Database: ", { key, value }); 49 | } catch ({ message }) { 50 | console.error("Error: Firebase Database: ", message); 51 | } 52 | } 53 | }; 54 | 55 | const auth = getAuth(); 56 | 57 | onAuthStateChanged(auth, _onAuthStateChanged); 58 | } 59 | 60 | const tests = { 61 | timer: () => { 62 | const tag = "test-timer"; 63 | console.time(tag); 64 | console.timeEnd(tag); 65 | console.count(tag); 66 | }, 67 | userAgent: () => { 68 | console.log("userAgent: ", global.userAgent); 69 | }, 70 | process: () => { 71 | console.log("process: ", global.process); 72 | }, 73 | navigator: () => { 74 | console.log("navigator: ", Object.keys(global.navigator)); 75 | }, 76 | performance: () => { 77 | console.log("performance: ", Object.keys(global.performance)); 78 | }, 79 | window: () => { 80 | console.log("location: ", Object.keys(window.location)); 81 | }, 82 | base64: async () => { 83 | const asset = Asset.fromModule(require("./image.png")); 84 | await asset.downloadAsync(); 85 | console.log("B64: ASSET:", asset.localUri); 86 | const data = (await FileSystem.readAsStringAsync(asset.localUri, { 87 | encoding: FileSystem.EncodingType.Base64, 88 | })).trim(); 89 | 90 | global.__debug_browser_polyfill_image = true; 91 | const pngPrefix = "data:image/png;base64,"; 92 | // console.log('B64: DATA: ', pngPrefix + data); 93 | const image = new global.HTMLImageElement(); 94 | image.addEventListener("loading", () => { 95 | console.log("B64: Loading Image"); 96 | }); 97 | image.addEventListener("error", () => { 98 | console.log("B64: Error Loading Image"); 99 | }); 100 | image.onload = () => { 101 | const { src, width, height } = image; 102 | console.log("B64: Loaded Image", { src, width, height }); 103 | }; 104 | image.src = pngPrefix + data; 105 | }, 106 | correctElementsCreated: () => { 107 | const { 108 | HTMLImageElement, 109 | ImageBitmap, 110 | HTMLVideoElement, 111 | HTMLCanvasElement, 112 | } = global; 113 | const types = [ 114 | { type: "img", inst: HTMLImageElement }, 115 | { type: "video", inst: HTMLVideoElement }, 116 | { type: "canvas", inst: HTMLCanvasElement }, 117 | { type: "img", inst: ImageBitmap }, 118 | ]; 119 | types.forEach((item) => { 120 | const element = document.createElement(item.type); 121 | console.log( 122 | "type", 123 | item.type, 124 | element instanceof item.inst, 125 | element instanceof Number 126 | ); 127 | }); 128 | }, 129 | elements: () => { 130 | const { 131 | HTMLImageElement, 132 | Image, 133 | ImageBitmap, 134 | HTMLVideoElement, 135 | Video, 136 | HTMLCanvasElement, 137 | Canvas, 138 | } = global; 139 | const elements = { 140 | HTMLImageElement, 141 | Image, 142 | ImageBitmap, 143 | HTMLVideoElement, 144 | Video, 145 | HTMLCanvasElement, 146 | Canvas, 147 | }; 148 | console.log( 149 | "Elements: ", 150 | Object.keys(elements).map((key) => ({ [key]: !!elements[key] })) 151 | ); 152 | }, 153 | loadImage: () => { 154 | const image = new global.HTMLImageElement(); 155 | image.addEventListener("loading", () => { 156 | console.log("Loading Image"); 157 | }); 158 | image.addEventListener("error", () => { 159 | console.log("Error Loading Image"); 160 | }); 161 | image.onload = () => { 162 | const { src, width, height } = image; 163 | console.log("Loaded Image", { src, width, height }); 164 | }; 165 | image.src = "https://avatars0.githubusercontent.com/u/9664363?s=40&v=4"; 166 | }, 167 | textDecoder: () => { 168 | console.log("TextDecoder: ", !!global.TextDecoder); 169 | const utfLabel = "utf-8"; 170 | const options = { fatal: true }; 171 | const decoder = new TextDecoder(utfLabel, options); 172 | 173 | let data = "{}"; 174 | let buffer = ""; 175 | buffer += decoder.decode(data, { stream: true }); 176 | console.log("TextDecoder buffer", buffer, " from: ", Object.keys(data)); 177 | }, 178 | context: () => { 179 | const canvas = document.createElement("canvas"); 180 | const context = canvas.getContext("2d"); 181 | const webgl = canvas.getContext("webgl"); 182 | console.log(context, webgl); 183 | }, 184 | domParser: () => { 185 | console.log("window.DOMParser: ", !!window.DOMParser); 186 | const parser = new window.DOMParser(); 187 | 188 | const html = ` 189 | 190 | 191 | 192 |
193 | some text content 194 |

Charlie Cheever

195 |

is my dad.

196 | 197 |
198 | 199 | 200 | `; 201 | const dom = parser.parseFromString(html); 202 | const container = dom.getElementById("container-id"); 203 | Object.keys(container).forEach((key) => { 204 | const obj = container[key]; 205 | console.log(`DOMParser: container.${key}: ${obj}`); 206 | }); 207 | }, 208 | }; 209 | 210 | const testGL = !Constants.isDevice; 211 | 212 | function useWindowDeviceSize() { 213 | const [size, setSize] = React.useState(null); 214 | React.useEffect(() => { 215 | const onLayout = () => { 216 | setSize({ 217 | width: window.innerWidth, 218 | height: window.innerHeight, 219 | scale: window.devicePixelRatio, 220 | }); 221 | console.log("Update Layout:", { 222 | width: window.innerWidth, 223 | height: window.innerHeight, 224 | scale: window.devicePixelRatio, 225 | }); 226 | }; 227 | 228 | window.addEventListener("resize", onLayout); 229 | 230 | return () => { 231 | window.removeEventListener("resize", onLayout); 232 | }; 233 | }, []); 234 | return size; 235 | } 236 | 237 | export default function App() { 238 | const size = useWindowDeviceSize(); 239 | 240 | React.useEffect(() => { 241 | if (!testGL) { 242 | runTests(); 243 | } 244 | setupFirebase(); 245 | }, []); 246 | 247 | const runTests = () => { 248 | Object.keys(tests).forEach((key) => { 249 | try { 250 | console.log("Run Test: ", key); 251 | tests[key](); 252 | } catch (error) { 253 | console.error(`Error running ${key} test: `, error); 254 | } 255 | }); 256 | }; 257 | 258 | return ( 259 | 260 | Check your console... 261 | {testGL && ( 262 | { 264 | global.__context = context; 265 | runTests(); 266 | }} 267 | /> 268 | )} 269 | 270 | ); 271 | } 272 | -------------------------------------------------------------------------------- /example/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo/browser-polyfill/36d63330cfe04a8502e7608fc65a986cec1c26aa/example/image.png -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@expo/browser-polyfill": "../expo-browser-polyfill-1.0.1.tgz", 4 | "expo": "^44.0.4", 5 | "expo-2d-context": "^0.0.3", 6 | "expo-file-system": "^13.2.0", 7 | "expo-gl": "^11.1.1", 8 | "firebase": "^9.6.2", 9 | "react": "17.0.1", 10 | "react-native": "0.64.3", 11 | "text-encoding": "^0.7.0", 12 | "xmldom-qsa": "^1.0.3" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.12.9" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /expo-browser-polyfill-1.0.1.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo/browser-polyfill/36d63330cfe04a8502e7608fc65a986cec1c26aa/expo-browser-polyfill-1.0.1.tgz -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@expo/browser-polyfill", 3 | "version": "1.0.1", 4 | "sideEffects": false, 5 | "description": "Browser polyfill for making React Native compatible with web libs like pixi.js, three.js, phaser.js", 6 | "homepage": "https://github.com/expo/browser-polyfill#readme", 7 | "bugs": { 8 | "url": "https://github.com/expo/browser-polyfill/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/expo/browser-polyfill.git" 13 | }, 14 | "keywords": [ 15 | "expo", 16 | "browser", 17 | "polyfill", 18 | "react-native", 19 | "react", 20 | "web", 21 | "dom", 22 | "document", 23 | "shim" 24 | ], 25 | "private": false, 26 | "author": { 27 | "email": "bacon@expo.io", 28 | "name": "Evan Bacon" 29 | }, 30 | "license": "MIT", 31 | "files": [ 32 | "src" 33 | ], 34 | "pre-push": [ 35 | "lint" 36 | ], 37 | "directories": { 38 | "example": "examples", 39 | "lib": "src" 40 | }, 41 | "readmeFilename": "README.md", 42 | "main": "src/index", 43 | "scripts": { 44 | "lint:example": "eslint example/", 45 | "lint": "eslint src/", 46 | "lint:fix": "eslint src/ --fix", 47 | "sync-example": "rsync -rv src example/node_modules/@expo/browser-polyfill && rsync -rv package.json example/node_modules/@expo/browser-polyfill" 48 | }, 49 | "peerDependencies": { 50 | "expo-file-system": "^13.2.0", 51 | "react": "^17.0.1", 52 | "react-native": "^0.64.3" 53 | }, 54 | "devDependencies": { 55 | "babel-preset-expo": "^5.1.1", 56 | "eslint": "^5.16.0", 57 | "eslint-config-universe": "^1.0.7", 58 | "eslint-plugin-jest": "^22.4.1", 59 | "husky": "^1.3.1", 60 | "jest": "^24.7.1", 61 | "jest-expo": "^32.0.0", 62 | "prettier": "^1.17.0" 63 | }, 64 | "dependencies": { 65 | "expo-2d-context": "^0.0.3", 66 | "fbemitter": "^2.1.1", 67 | "text-encoding": "^0.7.0", 68 | "uuid": "^8.3.2", 69 | "xmldom-qsa": "^1.0.3" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/DOM/Document.js: -------------------------------------------------------------------------------- 1 | import Element from './Element'; 2 | import HTMLVideoElement from './HTMLVideoElement'; 3 | import HTMLImageElement from './HTMLImageElement'; 4 | import HTMLCanvasElement from './HTMLCanvasElement'; 5 | 6 | class Document extends Element { 7 | constructor() { 8 | super('#document'); 9 | this.body = new Element('BODY'); 10 | this.documentElement = new Element('HTML'); 11 | this.readyState = 'complete'; 12 | } 13 | 14 | createElement(tagName) { 15 | switch ((tagName || '').toLowerCase()) { 16 | case 'video': 17 | return new HTMLVideoElement(tagName); 18 | case 'img': 19 | return new HTMLImageElement(tagName); 20 | case 'canvas': 21 | return new HTMLCanvasElement(tagName); 22 | case 'iframe': 23 | // Return nothing to keep firebase working. 24 | return null; 25 | default: 26 | return new Element(tagName); 27 | } 28 | } 29 | 30 | createElementNS(tagName) { 31 | const element = this.createElement(tagName); 32 | element.toDataURL = () => ({}); 33 | return element; 34 | } 35 | 36 | getElementById(id) { 37 | return new Element('div'); 38 | } 39 | } 40 | 41 | export default Document; 42 | -------------------------------------------------------------------------------- /src/DOM/Element.js: -------------------------------------------------------------------------------- 1 | import Node from './Node'; 2 | import CanvasRenderingContext2D from 'expo-2d-context'; 3 | 4 | class Element extends Node { 5 | constructor(tagName) { 6 | return super(tagName.toUpperCase()); 7 | 8 | this.doc = { 9 | body: { 10 | innerHTML: '', 11 | }, 12 | }; 13 | } 14 | 15 | get tagName() { 16 | return this.nodeName; 17 | } 18 | 19 | setAttributeNS() {} 20 | 21 | get clientWidth() { 22 | return this.innerWidth; 23 | } 24 | get clientHeight() { 25 | return this.innerHeight; 26 | } 27 | 28 | get offsetWidth() { 29 | return this.innerWidth; 30 | } 31 | get offsetHeight() { 32 | return this.innerHeight; 33 | } 34 | 35 | get innerWidth() { 36 | return window.innerWidth; 37 | } 38 | get innerHeight() { 39 | return window.innerHeight; 40 | } 41 | 42 | getContext(contextType, contextOptions, context) { 43 | const possibleContext = context || global.__context; 44 | if (contextType != '2d' && possibleContext) { 45 | return possibleContext; 46 | } 47 | if (contextType === '2d' && possibleContext) { 48 | return new CanvasRenderingContext2D(possibleContext); 49 | } 50 | 51 | return { 52 | fillText: (text, x, y, maxWidth) => ({}), 53 | measureText: text => ({ 54 | width: (text || '').split('').length * 6, 55 | height: 24, 56 | }), 57 | fillRect: () => ({}), 58 | drawImage: () => ({}), 59 | getImageData: () => ({ data: new Uint8ClampedArray([255, 0, 0, 0]) }), 60 | getContextAttributes: () => ({ 61 | stencil: true, 62 | }), 63 | getExtension: () => ({ 64 | loseContext: () => {}, 65 | }), 66 | putImageData: () => ({}), 67 | createImageData: () => ({}), 68 | }; 69 | } 70 | 71 | get ontouchstart() { 72 | return {}; 73 | } 74 | } 75 | 76 | export default Element; 77 | -------------------------------------------------------------------------------- /src/DOM/HTMLCanvasElement.js: -------------------------------------------------------------------------------- 1 | import Element from './Element'; 2 | class HTMLCanvasElement extends Element {} 3 | export default HTMLCanvasElement; 4 | -------------------------------------------------------------------------------- /src/DOM/HTMLImageElement.js: -------------------------------------------------------------------------------- 1 | import { Image } from 'react-native'; 2 | import * as FileSystem from 'expo-file-system'; 3 | const { writeAsStringAsync, documentDirectory } = FileSystem; 4 | const EncodingType = FileSystem.EncodingType || FileSystem.EncodingTypes; 5 | 6 | import { v1 as uuidv1 } from 'uuid'; 7 | 8 | import Element from './Element'; 9 | 10 | const b64Extensions = { 11 | '/': 'jpg', 12 | i: 'png', 13 | R: 'gif', 14 | U: 'webp', 15 | }; 16 | 17 | function b64WithoutPrefix(b64) { 18 | return b64.split(',')[1]; 19 | } 20 | 21 | function getMIMEforBase64String(b64) { 22 | let input = b64; 23 | if (b64.includes(',')) { 24 | input = b64WithoutPrefix(b64); 25 | } 26 | const first = input.charAt(0); 27 | const mime = b64Extensions[first]; 28 | if (!mime) { 29 | throw new Error('Unknown Base64 MIME type: ', b64); 30 | } 31 | return mime; 32 | } 33 | 34 | class HTMLImageElement extends Element { 35 | get src() { 36 | return this.localUri; 37 | } 38 | 39 | set src(value) { 40 | this.localUri = value; 41 | this._load(); 42 | } 43 | 44 | get onload() { 45 | return this._onload; 46 | } 47 | set onload(value) { 48 | this._onload = value; 49 | } 50 | 51 | get complete() { 52 | return this._complete; 53 | } 54 | set complete(value) { 55 | this._complete = value; 56 | if (value) { 57 | this.emitter.emit('load', this); 58 | this.onload(); 59 | } 60 | } 61 | 62 | constructor(props) { 63 | super('img'); 64 | this._load = this._load.bind(this); 65 | this._onload = () => {}; 66 | if (props !== null && typeof props === 'object') { 67 | const { localUri, width, height } = props || {}; 68 | this.src = localUri; 69 | this.width = width; 70 | this.height = height; 71 | this._load(); 72 | } 73 | } 74 | 75 | _load() { 76 | if (this.src) { 77 | if ( 78 | typeof this.src === 'string' && 79 | this.src.startsWith && 80 | this.src.startsWith('data:') 81 | ) { 82 | // is base64 - convert and try again; 83 | this._base64 = this.src; 84 | const base64result = this.src.split(',')[1]; 85 | (async () => { 86 | try { 87 | const MIME = getMIMEforBase64String(base64result); 88 | this.localUri = `${documentDirectory}${uuidv1()}-b64image.${MIME}`; 89 | await writeAsStringAsync(this.localUri, base64result, { 90 | encoding: EncodingType.Base64, 91 | }); 92 | this._load(); 93 | } catch (error) { 94 | if (global.__debug_browser_polyfill_image) { 95 | console.log(`@expo/browser-polyfill: Error:`, error.message); 96 | } 97 | this.emitter.emit('error', { target: this, error }); 98 | } 99 | })(); 100 | return; 101 | } 102 | if (!this.width || !this.height) { 103 | this.complete = false; 104 | this.emitter.emit('loading', this); 105 | Image.getSize( 106 | this.src, 107 | (width, height) => { 108 | this.width = width; 109 | this.height = height; 110 | this.complete = true; 111 | }, 112 | error => { 113 | this.emitter.emit('error', { target: this }); 114 | }, 115 | ); 116 | } else { 117 | this.complete = true; 118 | } 119 | } 120 | } 121 | } 122 | export default HTMLImageElement; 123 | -------------------------------------------------------------------------------- /src/DOM/HTMLVideoElement.js: -------------------------------------------------------------------------------- 1 | import Element from './Element'; 2 | class HTMLVideoElement extends Element {} 3 | export default HTMLVideoElement; 4 | -------------------------------------------------------------------------------- /src/DOM/Node.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { EventEmitter } from 'fbemitter'; 3 | 4 | class Node { 5 | constructor(nodeName) { 6 | this.emitter = new EventEmitter(); 7 | this.addEventListener = this.addEventListener.bind(this); 8 | this.removeEventListener = this.removeEventListener.bind(this); 9 | this._checkEmitter = this._checkEmitter.bind(this); 10 | 11 | this.style = {}; 12 | this.className = { 13 | baseVal: '', 14 | }; 15 | this.nodeName = nodeName; 16 | } 17 | 18 | get ownerDocument() { 19 | return window.document; 20 | } 21 | 22 | _checkEmitter() { 23 | if ( 24 | !this.emitter || 25 | !(this.emitter.on || this.emitter.addEventListener || this.emitter.addListener) 26 | ) { 27 | this.emitter = new EventEmitter(); 28 | } 29 | } 30 | 31 | addEventListener(eventName, listener) { 32 | this._checkEmitter(); 33 | if (this.emitter.on) { 34 | this.emitter.on(eventName, listener); 35 | } else if (this.emitter.addEventListener) { 36 | this.emitter.addEventListener(eventName, listener); 37 | } else if (this.emitter.addListener) { 38 | this.emitter.addListener(eventName, listener); 39 | } 40 | } 41 | 42 | removeEventListener(eventName, listener) { 43 | this._checkEmitter(); 44 | if (this.emitter.off) { 45 | this.emitter.off(eventName, listener); 46 | } else if (this.emitter.removeEventListener) { 47 | this.emitter.removeEventListener(eventName, listener); 48 | } else if (this.emitter.removeListener) { 49 | this.emitter.removeListener(eventName, listener); 50 | } 51 | } 52 | 53 | appendChild() {} 54 | insertBefore() {} 55 | removeChild() {} 56 | setAttributeNS() {} 57 | 58 | getBoundingClientRect() { 59 | return { 60 | left: 0, 61 | top: 0, 62 | right: window.innerWidth, 63 | bottom: window.innerHeight, 64 | x: 0, 65 | y: 0, 66 | width: window.innerWidth, 67 | height: window.innerHeight, 68 | }; 69 | } 70 | } 71 | 72 | export default Node; 73 | -------------------------------------------------------------------------------- /src/console/count.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | if (!console.count) { 4 | const counts = {}; 5 | 6 | console.count = ((label = '') => { 7 | if (!counts[label]) 8 | counts[label] = 0; 9 | counts[label]++; 10 | console.log(`${label}: ${counts[label]}`); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/console/index.js: -------------------------------------------------------------------------------- 1 | import './count'; 2 | import './time'; -------------------------------------------------------------------------------- /src/console/time.js: -------------------------------------------------------------------------------- 1 | 2 | const startTimes = {}; 3 | 4 | if (!console.time) { 5 | console.time = (label => { 6 | startTimes[label] = window.performance.now(); 7 | }); 8 | } 9 | 10 | if (!console.timeEnd) { 11 | console.timeEnd = (label => { 12 | const endTime = window.performance.now(); 13 | if (startTimes[label]) { 14 | const delta = endTime - startTimes[label]; 15 | console.log(`${label}: ${delta.toFixed(3)}ms`); 16 | delete startTimes[label]; 17 | } else { 18 | console.warn(`Warning: No such label '${label}' for console.timeEnd()`); 19 | } 20 | }); 21 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "./module"; 2 | -------------------------------------------------------------------------------- /src/module.js: -------------------------------------------------------------------------------- 1 | // do nothing :) 2 | -------------------------------------------------------------------------------- /src/module.native.js: -------------------------------------------------------------------------------- 1 | import Document from './DOM/Document'; 2 | 3 | import './window'; 4 | import './resize'; 5 | import './process'; 6 | import './console'; 7 | window.document = window.document || new Document(); 8 | -------------------------------------------------------------------------------- /src/performance.js: -------------------------------------------------------------------------------- 1 | if (!global.nativePerformanceNow) { 2 | global.nativePerformanceNow = global.nativePerformanceNow || global.performanceNow || require('fbjs/lib/performanceNow'); 3 | global.performanceNow = global.performanceNow || global.nativePerformanceNow; 4 | } 5 | 6 | if (!window.performance || !window.performance.now) { 7 | window.performance = { 8 | timeOrigin: -1, 9 | timing: { 10 | connectEnd: -1, 11 | connectStart: -1, 12 | domComplete: -1, 13 | domContentLoadedEventEnd: -1, 14 | domContentLoadedEventStart: -1, 15 | domInteractive: -1, 16 | domLoading: -1, 17 | domainLookupEnd: -1, 18 | domainLookupStart: -1, 19 | fetchStart: -1, 20 | loadEventEnd: -1, 21 | loadEventStart: -1, 22 | navigationStart: -1, 23 | redirectEnd: -1, 24 | redirectStart: -1, 25 | requestStart: -1, 26 | responseEnd: -1, 27 | responseStart: -1, 28 | secureConnectionStart: -1, 29 | unloadEventEnd: -1, 30 | unloadEventStart: -1, 31 | }, 32 | now() { 33 | // TODO: Bacon: Return DOMHighResTimeStamp 34 | return global.nativePerformanceNow(); 35 | }, 36 | toJSON() { 37 | return { 38 | timing: this.timing, 39 | navigation: this.navigation, 40 | timeOrigin: this.timeOrigin 41 | } 42 | }, 43 | navigation: { 44 | redirectCount: -1, 45 | type: -1 46 | }, 47 | memory: { 48 | jsHeapSizeLimit: -1, 49 | totalJSHeapSize: -1, 50 | usedJSHeapSize: -1 51 | }, 52 | measure() { 53 | console.warn('window.performance.measure is not implemented') 54 | }, 55 | mark() { 56 | console.warn('window.performance.mark is not implemented') 57 | }, 58 | clearMeasures() { 59 | console.warn('window.performance.clearMeasures is not implemented') 60 | }, 61 | clearMarks() { 62 | console.warn('window.performance.clearMarks is not implemented') 63 | }, 64 | clearResourceTimings() { 65 | console.warn('window.performance.clearResourceTimings is not implemented') 66 | }, 67 | setResourceTimingBufferSize() { 68 | console.warn('window.performance.setResourceTimingBufferSize is not implemented') 69 | }, 70 | getEntriesByType() { 71 | console.warn('window.performance.getEntriesByType is not implemented') 72 | return []; 73 | }, 74 | getEntriesByName() { 75 | console.warn('window.performance.getEntriesByName is not implemented') 76 | return []; 77 | }, 78 | getEntries() { 79 | console.warn('window.performance.getEntries is not implemented') 80 | } 81 | } 82 | } 83 | 84 | export default window.performance; -------------------------------------------------------------------------------- /src/process.js: -------------------------------------------------------------------------------- 1 | process.browser = true; 2 | -------------------------------------------------------------------------------- /src/resize.js: -------------------------------------------------------------------------------- 1 | import { Dimensions } from 'react-native'; 2 | const { width, height, scale } = Dimensions.get('window'); 3 | /* 4 | Window Resize Stub 5 | */ 6 | window.devicePixelRatio = scale; 7 | window.innerWidth = width; 8 | window.clientWidth = width; 9 | window.innerHeight = height; 10 | window.clientHeight = height; 11 | window.screen = window.screen || {}; 12 | window.screen.orientation = 13 | window.screen.orientation || window.clientWidth < window.clientHeight ? 0 : 90; 14 | 15 | if (!global.__EXPO_BROWSER_POLYFILL_RESIZE) { 16 | global.__EXPO_BROWSER_POLYFILL_RESIZE = true; 17 | Dimensions.addEventListener('change', ({ screen: { width, height, scale } }) => { 18 | window.devicePixelRatio = scale; 19 | window.innerWidth = width; 20 | window.clientWidth = width; 21 | window.innerHeight = height; 22 | window.clientHeight = height; 23 | 24 | window.screen.orientation = width < height ? 0 : 90; 25 | if (window.emitter && window.emitter.emit) { 26 | window.emitter.emit('resize'); 27 | } 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/window.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'fbemitter'; 2 | import { TextDecoder, TextEncoder } from 'text-encoding'; 3 | import Document from './DOM/Document'; 4 | 5 | import './performance'; 6 | 7 | import HTMLImageElement from './DOM/HTMLImageElement'; 8 | import HTMLCanvasElement from './DOM/HTMLCanvasElement'; 9 | import HTMLVideoElement from './DOM/HTMLVideoElement'; 10 | import CanvasRenderingContext2D from 'expo-2d-context'; 11 | 12 | global.HTMLImageElement = global.HTMLImageElement || HTMLImageElement; 13 | global.Image = global.Image || HTMLImageElement; 14 | global.ImageBitmap = global.ImageBitmap || HTMLImageElement; 15 | global.HTMLVideoElement = global.HTMLVideoElement || HTMLVideoElement; 16 | global.Video = global.Video || HTMLVideoElement; 17 | global.HTMLCanvasElement = global.HTMLCanvasElement || HTMLCanvasElement; 18 | global.Canvas = global.Canvas || HTMLCanvasElement; 19 | global.CanvasRenderingContext2D = 20 | global.CanvasRenderingContext2D || CanvasRenderingContext2D; 21 | // This causes the cryptic error: 22 | // `Value is undefined, expected an Object` 23 | // global.WebGLRenderingContext = global.WebGLRenderingContext || function() {}; 24 | 25 | function checkEmitter() { 26 | if ( 27 | !window.emitter || 28 | !( 29 | window.emitter.on || 30 | window.emitter.addEventListener || 31 | window.emitter.addListener 32 | ) 33 | ) { 34 | window.emitter = new EventEmitter(); 35 | } 36 | } 37 | 38 | window.scrollTo = window.scrollTo || (() => ({})); 39 | 40 | window.addEventListener = (eventName, listener) => { 41 | checkEmitter(); 42 | const addListener = () => { 43 | if (window.emitter.on) { 44 | window.emitter.on(eventName, listener); 45 | } else if (window.emitter.addEventListener) { 46 | window.emitter.addEventListener(eventName, listener); 47 | } else if (window.emitter.addListener) { 48 | window.emitter.addListener(eventName, listener); 49 | } 50 | }; 51 | 52 | addListener(); 53 | 54 | if (eventName.toLowerCase() === 'load') { 55 | if (window.emitter && window.emitter.emit) { 56 | setTimeout(() => { 57 | window.emitter.emit('load'); 58 | }, 1); 59 | } 60 | } 61 | }; 62 | 63 | window.removeEventListener = (eventName, listener) => { 64 | checkEmitter(); 65 | if (window.emitter.off) { 66 | window.emitter.off(eventName, listener); 67 | } else if (window.emitter.removeEventListener) { 68 | window.emitter.removeEventListener(eventName, listener); 69 | } else if (window.emitter.removeListener) { 70 | window.emitter.removeListener(eventName, listener); 71 | } 72 | }; 73 | 74 | window.DOMParser = window.DOMParser || require('xmldom-qsa').DOMParser; 75 | global.TextDecoder = global.TextDecoder || TextDecoder; 76 | global.TextEncoder = global.TextEncoder || TextEncoder; 77 | 78 | const agent = 'chrome'; 79 | global.userAgent = global.userAgent || agent; 80 | global.navigator.userAgent = global.navigator.userAgent || agent; 81 | global.navigator.product = 'ReactNative'; 82 | global.navigator.platform = global.navigator.platform || []; 83 | global.navigator.appVersion = global.navigator.appVersion || 'OS10'; 84 | global.navigator.maxTouchPoints = global.navigator.maxTouchPoints || 5; 85 | global.navigator.standalone = 86 | global.navigator.standalone === null ? true : global.navigator.standalone; 87 | 88 | window['chrome'] = window['chrome'] || { 89 | extension: {}, 90 | }; 91 | 92 | ///https://www.w3schools.com/js/js_window_location.asp 93 | // Newer versions of React Native define `window.location` that can't be reassigned. 94 | // When the `location` is defined, we don't have to do anything else. 95 | if ('location' in window === false) { 96 | window.location = { 97 | href: '', // window.location.href returns the href (URL) of the current page 98 | hostname: '', //window.location.hostname returns the domain name of the web host 99 | pathname: '', //window.location.pathname returns the path and filename of the current page 100 | protocol: 'https', //window.location.protocol returns the web protocol used (http: or https:) 101 | assign: null, //window.location.assign loads a new document 102 | }; 103 | } 104 | 105 | if (global.document) { 106 | global.document.readyState = 'complete'; 107 | } 108 | --------------------------------------------------------------------------------