Skip to content

Commit

Permalink
fix(native): restore polyfill conversions, drop networking (pmndrs#3050)
Browse files Browse the repository at this point in the history
  • Loading branch information
CodyJasonBennett authored Oct 21, 2023
1 parent 8a77d2a commit 1e072d6
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 88 deletions.
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"singleQuote": true,
"tabWidth": 2,
"printWidth": 120,
"jsxBracketSameLine": true
"jsxBracketSameLine": true,
"endOfLine": "auto"
}
1 change: 1 addition & 0 deletions packages/fiber/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/fiber/src/native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
219 changes: 135 additions & 84 deletions packages/fiber/src/native/polyfills.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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<Blob | BlobPart | string>, 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<string> {
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<Blob>((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<string>((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,
}
Expand All @@ -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
}
}
15 changes: 14 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -5417,6 +5425,11 @@ [email protected], 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"
Expand Down

0 comments on commit 1e072d6

Please sign in to comment.