diff --git a/.prettierrc b/.prettierrc index 5553e1cf25..754aefb5df 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,5 +4,6 @@ "singleQuote": true, "tabWidth": 2, "printWidth": 120, - "jsxBracketSameLine": true + "jsxBracketSameLine": true, + "endOfLine": "auto" } diff --git a/packages/fiber/package.json b/packages/fiber/package.json index 6379f80057..41212ab83e 100644 --- a/packages/fiber/package.json +++ b/packages/fiber/package.json @@ -45,6 +45,7 @@ "@babel/runtime": "^7.17.8", "@types/react-reconciler": "^0.26.7", "base64-js": "^1.5.1", + "buffer": "^6.0.3", "its-fine": "^1.0.6", "react-reconciler": "^0.27.0", "react-use-measure": "^2.1.1", diff --git a/packages/fiber/src/native.tsx b/packages/fiber/src/native.tsx index b7b796eef0..c37809041f 100644 --- a/packages/fiber/src/native.tsx +++ b/packages/fiber/src/native.tsx @@ -21,6 +21,6 @@ export type { GlobalRenderCallback, GlobalEffectType } from './core/loop' export * from './core' import { Platform } from 'react-native' -import { _polyfills } from './native/polyfills' +import { polyfills } from './native/polyfills' -if (Platform.OS !== 'web') _polyfills() +if (Platform.OS !== 'web') polyfills() diff --git a/packages/fiber/src/native/polyfills.ts b/packages/fiber/src/native/polyfills.ts index 3e07eef93a..3adfa5b198 100644 --- a/packages/fiber/src/native/polyfills.ts +++ b/packages/fiber/src/native/polyfills.ts @@ -1,42 +1,139 @@ -import { Image } from 'react-native' -import { Asset } from 'expo-asset' -import { cacheDirectory, copyAsync } from 'expo-file-system' import * as THREE from 'three' +import { Image, NativeModules } from 'react-native' +import { Asset } from 'expo-asset' +import * as fs from 'expo-file-system' +import { fromByteArray } from 'base64-js' +import { Buffer } from 'buffer' + +export function polyfills() { + function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0, + v = c == 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) + } -async function getAsset(input: string | number): Promise { - const asset = typeof input === 'string' ? Asset.fromURI(input) : Asset.fromModule(input) + // Patch Blob for ArrayBuffer if unsupported + try { + new Blob([new ArrayBuffer(4) as any]) + } catch (_) { + const BlobManager = require('react-native/Libraries/Blob/BlobManager.js') + + BlobManager.createFromParts = function createFromParts(parts: Array, options: any) { + const blobId = uuidv4() + + const items = parts.map((part) => { + if (part instanceof ArrayBuffer || ArrayBuffer.isView(part)) { + const data = fromByteArray(new Uint8Array(part as ArrayBuffer)) + return { + data, + type: 'string', + } + } else if (part instanceof Blob) { + return { + data: (part as any).data, + type: 'blob', + } + } else { + return { + data: String(part), + type: 'string', + } + } + }) + const size = items.reduce((acc, curr) => { + if (curr.type === 'string') { + return acc + global.unescape(encodeURI(curr.data)).length + } else { + return acc + curr.data.size + } + }, 0) - await asset.downloadAsync() - let localUri = asset.localUri || asset.uri + NativeModules.BlobModule.createFromParts(items, blobId) - // Unpack assets in Android Release Mode - if (!localUri.includes('://')) { - localUri = `${cacheDirectory}ExponentAsset-${asset.hash}.${asset.type}` - await copyAsync({ from: localUri, to: localUri }) + return BlobManager.createFromOptions({ + blobId, + offset: 0, + size, + type: options ? options.type : '', + lastModified: options ? options.lastModified : Date.now(), + }) + } } - return localUri -} + async function getAsset(input: string | number): Promise { + if (typeof input === 'string') { + // Don't process storage + if (input.startsWith('file:')) return input + + // Unpack Blobs from react-native BlobManager + if (input.startsWith('blob:')) { + const blob = await new Promise((res, rej) => { + const xhr = new XMLHttpRequest() + xhr.open('GET', input as string) + xhr.responseType = 'blob' + xhr.onload = () => res(xhr.response) + xhr.onerror = rej + xhr.send() + }) + + const data = await new Promise((res, rej) => { + const reader = new FileReader() + reader.onload = () => res(reader.result as string) + reader.onerror = rej + reader.readAsText(blob) + }) + + input = `data:${blob.type};base64,${data}` + } + + // Create safe URI for JSI + if (input.startsWith('data:')) { + const [header, data] = input.split(',') + const [, type] = header.split('/') + + const uri = fs.cacheDirectory + uuidv4() + `.${type}` + await fs.writeAsStringAsync(uri, data, { encoding: fs.EncodingType.Base64 }) + + return uri + } + } + + // Download bundler module or external URL + const asset = await Asset.fromModule(input).downloadAsync() + let uri = asset.localUri || asset.uri + + // Unpack assets in Android Release Mode + if (!uri.includes(':')) { + const file = `${fs.cacheDirectory}ExponentAsset-${asset.hash}.${asset.type}` + await fs.copyAsync({ from: uri, to: file }) + uri = file + } + + return uri + } -export function _polyfills() { // Don't pre-process urls, let expo-asset generate an absolute URL const extractUrlBase = THREE.LoaderUtils.extractUrlBase.bind(THREE.LoaderUtils) THREE.LoaderUtils.extractUrlBase = (url: string) => (typeof url === 'string' ? extractUrlBase(url) : './') // There's no Image in native, so create a data texture instead THREE.TextureLoader.prototype.load = function load(url, onLoad, onProgress, onError) { - if (this.path) url = this.path + url + if (this.path && typeof url === 'string') url = this.path + url + + this.manager.itemStart(url) const texture = new THREE.Texture() getAsset(url) - .then(async (localUri) => { + .then(async (uri) => { const { width, height } = await new Promise<{ width: number; height: number }>((res, rej) => - Image.getSize(localUri, (width, height) => res({ width, height }), rej), + Image.getSize(uri, (width, height) => res({ width, height }), rej), ) texture.image = { - data: { localUri }, + data: { localUri: uri }, width, height, } @@ -50,81 +147,35 @@ export function _polyfills() { onLoad?.(texture) }) - .catch(onError) + .catch((error) => { + onError?.(error) + this.manager.itemError(url) + }) + .finally(() => { + this.manager.itemEnd(url) + }) return texture } // Fetches assets via XMLHttpRequest THREE.FileLoader.prototype.load = function load(url, onLoad, onProgress, onError) { - if (this.path) url = this.path + url + if (this.path && typeof url === 'string') url = this.path + url - const request = new XMLHttpRequest() + this.manager.itemStart(url) getAsset(url) - .then((localUri) => { - request.open('GET', localUri, true) - - request.addEventListener( - 'load', - (event) => { - if (request.status === 200) { - onLoad?.(request.response) - - this.manager.itemEnd(url) - } else { - onError?.(event as unknown as ErrorEvent) - - this.manager.itemError(url) - this.manager.itemEnd(url) - } - }, - false, - ) - - request.addEventListener( - 'progress', - (event) => { - onProgress?.(event) - }, - false, - ) - - request.addEventListener( - 'error', - (event) => { - onError?.(event as unknown as ErrorEvent) - - this.manager.itemError(url) - this.manager.itemEnd(url) - }, - false, - ) - - request.addEventListener( - 'abort', - (event) => { - onError?.(event as unknown as ErrorEvent) - - this.manager.itemError(url) - this.manager.itemEnd(url) - }, - false, - ) - - if (this.responseType) request.responseType = this.responseType - if (this.withCredentials) request.withCredentials = this.withCredentials - - for (const header in this.requestHeader) { - request.setRequestHeader(header, this.requestHeader[header]) - } - - request.send(null) - - this.manager.itemStart(url) + .then(async (uri) => { + const base64 = await fs.readAsStringAsync(uri, { encoding: fs.EncodingType.Base64 }) + const data = Buffer.from(base64, 'base64') + onLoad?.(data.buffer) + }) + .catch((error) => { + onError?.(error) + this.manager.itemError(url) + }) + .finally(() => { + this.manager.itemEnd(url) }) - .catch(onError) - - return request } } diff --git a/yarn.lock b/yarn.lock index 16f4f7ea20..d90433eb09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3413,7 +3413,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.1.2, base64-js@^1.5.1: +base64-js@^1.1.2, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -3559,6 +3559,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + builtin-modules@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" @@ -5417,6 +5425,11 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore-walk@^3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335"