diff --git a/.cspell-wordlist.txt b/.cspell-wordlist.txt index 77e4fb593..524109df0 100644 --- a/.cspell-wordlist.txt +++ b/.cspell-wordlist.txt @@ -95,3 +95,4 @@ Português codegen cstdint ocurred +RNFS \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7989ebb5d..e57ce7460 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,11 @@ jobs: run: yarn lint - name: Typecheck files - run: yarn typecheck + run: | + cd packages/react-native-executorch + yarn prepare + cd ../../ + yarn typecheck build-library: runs-on: ubuntu-latest diff --git a/.nvmrc b/.nvmrc index 9a2a0e219..53d1c14db 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20 +v22 diff --git a/apps/computer-vision/app/_layout.tsx b/apps/computer-vision/app/_layout.tsx index 5914d2fe8..e14ba72f0 100644 --- a/apps/computer-vision/app/_layout.tsx +++ b/apps/computer-vision/app/_layout.tsx @@ -1,4 +1,7 @@ import { Drawer } from 'expo-router/drawer'; +import { initExecutorch } from 'react-native-executorch'; +import { ExpoResourceFetcher } from '@rn-executorch/expo-adapter'; + import ColorPalette from '../colors'; import React, { useState } from 'react'; import { Text, StyleSheet, View } from 'react-native'; @@ -10,6 +13,10 @@ import { } from '@react-navigation/drawer'; import { GeneratingContext } from '../context'; +initExecutorch({ + resourceFetcher: ExpoResourceFetcher, +}); + interface CustomDrawerProps extends DrawerContentComponentProps { isGenerating: boolean; } diff --git a/apps/computer-vision/package.json b/apps/computer-vision/package.json index 63885109a..b8f04a286 100644 --- a/apps/computer-vision/package.json +++ b/apps/computer-vision/package.json @@ -11,9 +11,10 @@ "lint": "eslint . --ext .ts,.tsx --fix" }, "dependencies": { - "@react-native/metro-config": "^0.76.3", + "@react-native/metro-config": "^0.81.5", "@react-navigation/drawer": "^7.3.9", "@react-navigation/native": "^7.1.6", + "@rn-executorch/expo-adapter": "workspace:*", "@shopify/react-native-skia": "2.2.12", "expo": "^54.0.27", "expo-constants": "~18.0.11", @@ -21,7 +22,7 @@ "expo-linking": "~8.0.10", "expo-router": "~6.0.17", "expo-status-bar": "~3.0.9", - "metro-config": "^0.81.0", + "metro-config": "^0.81.5", "react": "19.1.0", "react-native": "0.81.5", "react-native-device-info": "^14.0.4", diff --git a/apps/computer-vision/tsconfig.json b/apps/computer-vision/tsconfig.json index 47026ce43..0617caffc 100644 --- a/apps/computer-vision/tsconfig.json +++ b/apps/computer-vision/tsconfig.json @@ -9,7 +9,8 @@ "customConditions": ["react-native"], "noEmit": true, "paths": { - "react-native-executorch": ["../../packages/react-native-executorch/src"] + "react-native-executorch": ["../../packages/react-native-executorch/src"], + "@rn-executorch/expo-adapter": ["../../packages/expo-adapter/src"] } } } diff --git a/apps/llm/app/_layout.tsx b/apps/llm/app/_layout.tsx index 68c715a80..b290bb4ee 100644 --- a/apps/llm/app/_layout.tsx +++ b/apps/llm/app/_layout.tsx @@ -1,8 +1,9 @@ import { Drawer } from 'expo-router/drawer'; +import { initExecutorch } from 'react-native-executorch'; +import { ExpoResourceFetcher } from '@rn-executorch/expo-adapter'; import ColorPalette from '../colors'; import React, { useState } from 'react'; import { Text, StyleSheet, View } from 'react-native'; - import { DrawerContentComponentProps, DrawerContentScrollView, @@ -10,6 +11,10 @@ import { } from '@react-navigation/drawer'; import { GeneratingContext } from '../context'; +initExecutorch({ + resourceFetcher: ExpoResourceFetcher, +}); + interface CustomDrawerProps extends DrawerContentComponentProps { isGenerating: boolean; } diff --git a/apps/llm/package.json b/apps/llm/package.json index de046a299..e2c9f75ee 100644 --- a/apps/llm/package.json +++ b/apps/llm/package.json @@ -11,9 +11,10 @@ "lint": "eslint . --ext .ts,.tsx --fix" }, "dependencies": { - "@react-native/metro-config": "^0.76.3", + "@react-native/metro-config": "^0.81.5", "@react-navigation/drawer": "^7.3.9", "@react-navigation/native": "^7.1.6", + "@rn-executorch/expo-adapter": "workspace:*", "expo": "^54.0.27", "expo-brightness": "~14.0.8", "expo-calendar": "~15.0.8", @@ -22,7 +23,7 @@ "expo-linking": "~8.0.10", "expo-router": "~6.0.17", "expo-status-bar": "~3.0.9", - "metro-config": "^0.81.0", + "metro-config": "^0.81.5", "react": "19.1.0", "react-native": "0.81.5", "react-native-audio-api": "^0.8.2", diff --git a/apps/llm/tsconfig.json b/apps/llm/tsconfig.json index 47026ce43..0617caffc 100644 --- a/apps/llm/tsconfig.json +++ b/apps/llm/tsconfig.json @@ -9,7 +9,8 @@ "customConditions": ["react-native"], "noEmit": true, "paths": { - "react-native-executorch": ["../../packages/react-native-executorch/src"] + "react-native-executorch": ["../../packages/react-native-executorch/src"], + "@rn-executorch/expo-adapter": ["../../packages/expo-adapter/src"] } } } diff --git a/apps/speech/App.tsx b/apps/speech/App.tsx index af0598b59..2f75a0584 100644 --- a/apps/speech/App.tsx +++ b/apps/speech/App.tsx @@ -5,6 +5,12 @@ import { SpeechToTextScreen } from './screens/SpeechToTextScreen'; import ColorPalette from './colors'; import ExecutorchLogo from './assets/executorch.svg'; import { Quiz } from './screens/Quiz'; +import { initExecutorch } from 'react-native-executorch'; +import { ExpoResourceFetcher } from '@rn-executorch/expo-adapter'; + +initExecutorch({ + resourceFetcher: ExpoResourceFetcher, +}); export default function App() { const [currentScreen, setCurrentScreen] = useState< diff --git a/apps/speech/package.json b/apps/speech/package.json index 094fa2b78..339ee6cf9 100644 --- a/apps/speech/package.json +++ b/apps/speech/package.json @@ -11,12 +11,13 @@ "lint": "eslint . --ext .ts,.tsx --fix" }, "dependencies": { - "@react-native/metro-config": "^0.76.3", + "@react-native/metro-config": "^0.81.5", + "@rn-executorch/expo-adapter": "workspace:*", "buffer": "^6.0.3", "expo": "^54.0.27", "expo-font": "~14.0.10", "expo-status-bar": "~3.0.9", - "metro-config": "^0.81.0", + "metro-config": "^0.81.5", "react": "19.1.0", "react-native": "0.81.5", "react-native-audio-api": "0.6.5", diff --git a/apps/speech/tsconfig.json b/apps/speech/tsconfig.json index 47026ce43..0617caffc 100644 --- a/apps/speech/tsconfig.json +++ b/apps/speech/tsconfig.json @@ -9,7 +9,8 @@ "customConditions": ["react-native"], "noEmit": true, "paths": { - "react-native-executorch": ["../../packages/react-native-executorch/src"] + "react-native-executorch": ["../../packages/react-native-executorch/src"], + "@rn-executorch/expo-adapter": ["../../packages/expo-adapter/src"] } } } diff --git a/apps/text-embeddings/app/_layout.tsx b/apps/text-embeddings/app/_layout.tsx index 16bf0e87a..8b710d170 100644 --- a/apps/text-embeddings/app/_layout.tsx +++ b/apps/text-embeddings/app/_layout.tsx @@ -1,4 +1,6 @@ import { Drawer } from 'expo-router/drawer'; +import { initExecutorch } from 'react-native-executorch'; +import { ExpoResourceFetcher } from '@rn-executorch/expo-adapter'; import ColorPalette from '../colors'; import React, { useState } from 'react'; import { Text, StyleSheet, View } from 'react-native'; @@ -10,6 +12,10 @@ import { } from '@react-navigation/drawer'; import { GeneratingContext } from '../context'; +initExecutorch({ + resourceFetcher: ExpoResourceFetcher, +}); + interface CustomDrawerProps extends DrawerContentComponentProps { isGenerating: boolean; } diff --git a/apps/text-embeddings/package.json b/apps/text-embeddings/package.json index cbf0da96c..dd2df7430 100644 --- a/apps/text-embeddings/package.json +++ b/apps/text-embeddings/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@react-navigation/drawer": "^7.3.9", + "@rn-executorch/expo-adapter": "workspace:*", "expo": "^54.0.27", "expo-constants": "~18.0.11", "expo-linking": "~8.0.10", diff --git a/apps/text-embeddings/tsconfig.json b/apps/text-embeddings/tsconfig.json index 47026ce43..0617caffc 100644 --- a/apps/text-embeddings/tsconfig.json +++ b/apps/text-embeddings/tsconfig.json @@ -9,7 +9,8 @@ "customConditions": ["react-native"], "noEmit": true, "paths": { - "react-native-executorch": ["../../packages/react-native-executorch/src"] + "react-native-executorch": ["../../packages/react-native-executorch/src"], + "@rn-executorch/expo-adapter": ["../../packages/expo-adapter/src"] } } } diff --git a/package.json b/package.json index e4ee020b2..cfed3bf58 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,12 @@ "workspaces": { "packages": [ "packages/react-native-executorch", - "apps/*" + "packages/bare-adapter", + "packages/expo-adapter", + "apps/computer-vision", + "apps/llm", + "apps/speech", + "apps/text-embeddings" ] }, "scripts": { diff --git a/packages/bare-adapter/README.md b/packages/bare-adapter/README.md new file mode 100644 index 000000000..e2ec27e3b --- /dev/null +++ b/packages/bare-adapter/README.md @@ -0,0 +1,30 @@ +# @rn-executorch/bare-adapter + +Bare React Native adapter for `react-native-executorch` that provides resource fetching capabilities using native filesystem libraries. + +## Installation + +```bash +yarn add @rn-executorch/bare-adapter +yarn add @dr.pogodin/react-native-fs @kesha-antonov/react-native-background-downloader +``` + +## Usage + +```typescript +import { initExecutorch } from 'react-native-executorch'; +import { BareResourceFetcher } from '@rn-executorch/bare-adapter'; + +initExecutorch({ + resourceFetcher: BareResourceFetcher, +}); +``` + +## When to Use + +Use this adapter if you're working with: +- Bare React Native projects (created with `npx @react-native-community/cli@latest init`) +- Projects that need true background downloads +- Projects requiring direct native filesystem access + +This adapter uses `@dr.pogodin/react-native-fs` and `@kesha-antonov/react-native-background-downloader` for enhanced file operations and background download support. diff --git a/packages/bare-adapter/package.json b/packages/bare-adapter/package.json new file mode 100644 index 000000000..4b809b128 --- /dev/null +++ b/packages/bare-adapter/package.json @@ -0,0 +1,32 @@ +{ + "name": "@rn-executorch/bare-adapter", + "version": "0.1.0", + "description": "Bare React Native adapter for react-native-executorch", + "main": "src/index.ts", + "types": "src/index.ts", + "exports": { + ".": { + "import": "./src/index.ts", + "types": "./src/index.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit", + "lint": "eslint \"**/*.{js,ts,tsx}\"" + }, + "peerDependencies": { + "@dr.pogodin/react-native-fs": "^2.0.0", + "@kesha-antonov/react-native-background-downloader": "^4.0.0", + "react-native": "*", + "react-native-executorch": "*" + }, + "devDependencies": { + "@dr.pogodin/react-native-fs": "^2.36.2", + "@kesha-antonov/react-native-background-downloader": "^4.4.5", + "@types/react": "~19.1.10", + "react": "19.1.0", + "react-native": "0.81.5", + "react-native-executorch": "workspace:*", + "typescript": "~5.9.2" + } +} diff --git a/packages/bare-adapter/src/ResourceFetcher.ts b/packages/bare-adapter/src/ResourceFetcher.ts new file mode 100644 index 000000000..3e45b194f --- /dev/null +++ b/packages/bare-adapter/src/ResourceFetcher.ts @@ -0,0 +1,466 @@ +import { + createDownloadTask, + completeHandler, + DownloadTask, + BeginHandlerParams, + ProgressHandlerParams, +} from '@kesha-antonov/react-native-background-downloader'; +import * as RNFS from '@dr.pogodin/react-native-fs'; +import { Image } from 'react-native'; +import { RNEDirectory } from './constants/directories'; +import { + ResourceSource, + ResourceFetcherAdapter, + RnExecutorchErrorCode, + RnExecutorchError, +} from 'react-native-executorch'; +import { + ResourceFetcherUtils, + DownloadStatus, + SourceType, + ResourceSourceExtended, +} from './ResourceFetcherUtils'; + +interface DownloadResource { + task: DownloadTask; + status: DownloadStatus; + extendedInfo: ResourceSourceExtended; +} + +interface BareResourceFetcherInterface extends ResourceFetcherAdapter { + downloads: Map; + singleFetch(sourceExtended: ResourceSourceExtended): Promise; + returnOrStartNext( + sourceExtended: ResourceSourceExtended, + result: string | string[] + ): string[] | Promise; + pause(source: ResourceSource): Promise; + resume(source: ResourceSource): Promise; + cancel(source: ResourceSource): Promise; + findActive(sources: ResourceSource[]): ResourceSource; + handleObject(source: ResourceSource): Promise; + handleLocalFile(source: ResourceSource): string; + handleReleaseModeFile( + sourceExtended: ResourceSourceExtended + ): Promise; + handleDevModeFile( + sourceExtended: ResourceSourceExtended + ): Promise; + handleRemoteFile( + sourceExtended: ResourceSourceExtended + ): Promise; +} + +export const BareResourceFetcher: BareResourceFetcherInterface = { + downloads: new Map(), //map of currently downloading (or paused) files, if the download was started by .fetch() method. + + async fetch( + callback: (downloadProgress: number) => void = () => {}, + ...sources: ResourceSource[] + ) { + if (sources.length === 0) { + throw new RnExecutorchError( + RnExecutorchErrorCode.InvalidUserInput, + 'Empty list given as an argument' + ); + } + const { results: info, totalLength } = + await ResourceFetcherUtils.getFilesSizes(sources); + const head: ResourceSourceExtended = { + source: info[0]!.source, + sourceType: info[0]!.type, + callback: + info[0]!.type === SourceType.REMOTE_FILE + ? ResourceFetcherUtils.calculateDownloadProgress( + totalLength, + info[0]!.previousFilesTotalLength, + info[0]!.length, + callback + ) + : () => {}, + results: [], + }; + + let node = head; + for (let idx = 1; idx < sources.length; idx++) { + node.next = { + source: info[idx]!.source, + sourceType: info[idx]!.type, + callback: + info[idx]!.type === SourceType.REMOTE_FILE + ? ResourceFetcherUtils.calculateDownloadProgress( + totalLength, + info[idx]!.previousFilesTotalLength, + info[idx]!.length, + callback + ) + : () => {}, + results: [], + }; + node = node.next; + } + return this.singleFetch(head); + }, + + async singleFetch( + sourceExtended: ResourceSourceExtended + ): Promise { + const source = sourceExtended.source; + switch (sourceExtended.sourceType) { + case SourceType.OBJECT: { + return this.returnOrStartNext( + sourceExtended, + await this.handleObject(source) + ); + } + case SourceType.LOCAL_FILE: { + return this.returnOrStartNext( + sourceExtended, + this.handleLocalFile(source) + ); + } + case SourceType.RELEASE_MODE_FILE: { + return this.returnOrStartNext( + sourceExtended, + await this.handleReleaseModeFile(sourceExtended) + ); + } + case SourceType.DEV_MODE_FILE: { + const result = await this.handleDevModeFile(sourceExtended); + if (result !== null) { + return this.returnOrStartNext(sourceExtended, result); + } + return null; + } + default: { + //case SourceType.REMOTE_FILE + const result = await this.handleRemoteFile(sourceExtended); + if (result !== null) { + return this.returnOrStartNext(sourceExtended, result); + } + return null; + } + } + }, + + //if any download ends successfully this function is called - it checks whether it should trigger next download or return list of paths. + returnOrStartNext(sourceExtended: ResourceSourceExtended, result: string) { + sourceExtended.results.push(result); + + if (sourceExtended.next) { + const nextSource = sourceExtended.next; + nextSource.results.push(...sourceExtended.results); + return this.singleFetch(nextSource); + } + sourceExtended.callback!(1); + return sourceExtended.results; + }, + + async pause(source: ResourceSource) { + const resource = this.downloads.get(source)!; + switch (resource.status) { + case DownloadStatus.PAUSED: + throw new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherAlreadyPaused, + "The file download is currently paused. Can't pause the download of the same file twice." + ); + default: { + resource.status = DownloadStatus.PAUSED; + resource.task.pause(); + } + } + }, + + async resume(source: ResourceSource) { + const resource = this.downloads.get(source)!; + if ( + !resource.extendedInfo.fileUri || + !resource.extendedInfo.cacheFileUri || + !resource.extendedInfo.uri + ) { + throw new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherMissingUri, + 'Something went wrong. File uri info is not specified' + ); + } + switch (resource.status) { + case DownloadStatus.ONGOING: + throw new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherAlreadyOngoing, + "The file download is currently ongoing. Can't resume the ongoing download." + ); + default: { + resource.status = DownloadStatus.ONGOING; + resource.task.resume(); + + return new Promise((resolve, reject) => { + resource.task + .done(async () => { + if ( + !this.downloads.has(source) || + this.downloads.get(source)!.status === DownloadStatus.PAUSED + ) { + resolve(null); + return; + } + await RNFS.moveFile( + resource.extendedInfo.cacheFileUri!, + resource.extendedInfo.fileUri! + ); + this.downloads.delete(source); + ResourceFetcherUtils.triggerHuggingFaceDownloadCounter( + resource.extendedInfo.uri! + ); + + // Get the filename from the fileUri + const filename = resource.extendedInfo.fileUri!.split('/').pop(); + if (filename) { + completeHandler(filename); + } + + const result = this.returnOrStartNext( + resource.extendedInfo, + ResourceFetcherUtils.removeFilePrefix( + resource.extendedInfo.fileUri! + ) + ); + resolve(result); + }) + .error((e) => { + reject(e); + }); + }); + } + } + }, + + async cancel(source: ResourceSource) { + const resource = this.downloads.get(source)!; + resource.task.stop(); + this.downloads.delete(source); + }, + + async pauseFetching(...sources: ResourceSource[]) { + const source = this.findActive(sources); + await this.pause(source); + }, + + async resumeFetching(...sources: ResourceSource[]) { + const source = this.findActive(sources); + await this.resume(source); + }, + + async cancelFetching(...sources: ResourceSource[]) { + const source = this.findActive(sources); + await this.cancel(source); + }, + + findActive(sources: ResourceSource[]) { + for (const source of sources) { + if (this.downloads.has(source)) { + return source; + } + } + throw new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherNotActive, + 'None of given sources are currently during downloading process.' + ); + }, + + async listDownloadedFiles() { + const files = await RNFS.readDir(RNEDirectory); + return files.map((file) => file.path); + }, + + async listDownloadedModels() { + const files = await this.listDownloadedFiles(); + return files.filter((file: string) => file.endsWith('.pte')); + }, + + async deleteResources(...sources: ResourceSource[]) { + for (const source of sources) { + const filename = ResourceFetcherUtils.getFilenameFromUri( + source as string + ); + const fileUri = `${RNEDirectory}${filename}`; + if (await ResourceFetcherUtils.checkFileExists(fileUri)) { + await RNFS.unlink(fileUri); + } + } + }, + + async getFilesTotalSize(...sources: ResourceSource[]) { + return (await ResourceFetcherUtils.getFilesSizes(sources)).totalLength; + }, + + async handleObject(source: ResourceSource) { + if (typeof source !== 'object') { + throw new RnExecutorchError( + RnExecutorchErrorCode.InvalidModelSource, + 'Source is expected to be object' + ); + } + const jsonString = JSON.stringify(source); + const digest = ResourceFetcherUtils.hashObject(jsonString); + const filename = `${digest}.json`; + const path = `${RNEDirectory}${filename}`; + + if (await ResourceFetcherUtils.checkFileExists(path)) { + return ResourceFetcherUtils.removeFilePrefix(path); + } + + await ResourceFetcherUtils.createDirectoryIfNoExists(); + await RNFS.writeFile(path, jsonString, 'utf8'); + + return ResourceFetcherUtils.removeFilePrefix(path); + }, + + handleLocalFile(source: ResourceSource) { + if (typeof source !== 'string') { + throw new RnExecutorchError( + RnExecutorchErrorCode.InvalidModelSource, + 'Source is expected to be string' + ); + } + return ResourceFetcherUtils.removeFilePrefix(source); + }, + + async handleReleaseModeFile(sourceExtended: ResourceSourceExtended) { + const source = sourceExtended.source; + if (typeof source !== 'number') { + throw new RnExecutorchError( + RnExecutorchErrorCode.InvalidModelSource, + 'Source is expected to be number' + ); + } + const assetSource = Image.resolveAssetSource(source); + const uri = assetSource.uri; + const filename = ResourceFetcherUtils.getFilenameFromUri(uri); + const fileUri = `${RNEDirectory}${filename}`; + + if (await ResourceFetcherUtils.checkFileExists(fileUri)) { + return ResourceFetcherUtils.removeFilePrefix(fileUri); + } + await ResourceFetcherUtils.createDirectoryIfNoExists(); + + if (uri.startsWith('http') || uri.startsWith('file')) { + await RNFS.copyFile(uri, fileUri); + } + return ResourceFetcherUtils.removeFilePrefix(fileUri); + }, + + async handleDevModeFile(sourceExtended: ResourceSourceExtended) { + const source = sourceExtended.source; + if (typeof source !== 'number') { + throw new RnExecutorchError( + RnExecutorchErrorCode.InvalidModelSource, + 'Source is expected to be a number' + ); + } + sourceExtended.uri = Image.resolveAssetSource(source).uri; + return await this.handleRemoteFile(sourceExtended); + }, + + async handleRemoteFile(sourceExtended: ResourceSourceExtended) { + const source = sourceExtended.source; + if (typeof source === 'object') { + throw new RnExecutorchError( + RnExecutorchErrorCode.InvalidModelSource, + 'Source is expected to be a string or a number' + ); + } + if (this.downloads.has(source)) { + const resource = this.downloads.get(source)!; + if (resource.status === DownloadStatus.PAUSED) { + // if the download is paused, `fetch` is treated like `resume` + return this.resume(source); + } + // if the download is ongoing, throw error. + throw new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherDownloadInProgress, + 'Already downloading this file' + ); + } + if (typeof source === 'number' && !sourceExtended.uri) { + throw new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherMissingUri, + 'Source Uri is expected to be available here' + ); + } + if (typeof source === 'string') { + sourceExtended.uri = source; + } + const uri = sourceExtended.uri!; + const filename = ResourceFetcherUtils.getFilenameFromUri(uri); + sourceExtended.fileUri = `${RNEDirectory}${filename}`; + sourceExtended.cacheFileUri = `${RNFS.CachesDirectoryPath}/${filename}`; + + if (await ResourceFetcherUtils.checkFileExists(sourceExtended.fileUri)) { + return ResourceFetcherUtils.removeFilePrefix(sourceExtended.fileUri); + } + await ResourceFetcherUtils.createDirectoryIfNoExists(); + + return new Promise((resolve, reject) => { + const task = createDownloadTask({ + id: filename, + url: uri, + destination: sourceExtended.cacheFileUri!, + }) + .begin((_: BeginHandlerParams) => { + sourceExtended.callback!(0); + }) + .progress((progress: ProgressHandlerParams) => { + sourceExtended.callback!( + progress.bytesDownloaded / progress.bytesTotal + ); + }) + .done(async () => { + if ( + !this.downloads.has(source) || + this.downloads.get(source)!.status === DownloadStatus.PAUSED + ) { + resolve(null); + return; + } + await RNFS.moveFile( + sourceExtended.cacheFileUri!, + sourceExtended.fileUri! + ); + this.downloads.delete(source); + ResourceFetcherUtils.triggerHuggingFaceDownloadCounter(uri); + + // Complete the download job + completeHandler(filename); + + const nextResult = this.returnOrStartNext( + sourceExtended, + ResourceFetcherUtils.removeFilePrefix(sourceExtended.fileUri!) + ); + resolve(nextResult); + }) + .error((error) => { + this.downloads.delete(source); + reject( + new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherDownloadFailed, + `Failed to fetch resource from '${source}', context: ${error}` + ) + ); + }); + + // Start the download task + task.start(); + + const downloadResource: DownloadResource = { + task: task, + status: DownloadStatus.ONGOING, + extendedInfo: sourceExtended, + }; + this.downloads.set(source, downloadResource); + }); + }, + + async readAsString(path: string) { + return await RNFS.readFile(path, 'utf8'); + }, +}; diff --git a/packages/bare-adapter/src/ResourceFetcherUtils.ts b/packages/bare-adapter/src/ResourceFetcherUtils.ts new file mode 100644 index 000000000..d9d01dd0d --- /dev/null +++ b/packages/bare-adapter/src/ResourceFetcherUtils.ts @@ -0,0 +1,92 @@ +import { RNEDirectory } from './constants/directories'; +import { + ResourceSource, + Logger, + ResourceFetcherUtils as CoreUtils, + HTTP_CODE, + DownloadStatus, + SourceType, + ResourceSourceExtended, +} from 'react-native-executorch'; +import { Image } from 'react-native'; +import * as RNFS from '@dr.pogodin/react-native-fs'; + +export { HTTP_CODE, DownloadStatus, SourceType }; +export type { ResourceSourceExtended }; + +export namespace ResourceFetcherUtils { + export const removeFilePrefix = CoreUtils.removeFilePrefix; + export const hashObject = CoreUtils.hashObject; + export const calculateDownloadProgress = CoreUtils.calculateDownloadProgress; + export const triggerHuggingFaceDownloadCounter = + CoreUtils.triggerHuggingFaceDownloadCounter; + export const getFilenameFromUri = CoreUtils.getFilenameFromUri; + + export function getType(source: ResourceSource): SourceType { + if (typeof source === 'object') { + return SourceType.OBJECT; + } else if (typeof source === 'number') { + const uri = Image.resolveAssetSource(source).uri; + if (uri.startsWith('http')) { + return SourceType.DEV_MODE_FILE; + } + return SourceType.RELEASE_MODE_FILE; + } + // typeof source == 'string' + if (source.startsWith('file://')) { + return SourceType.LOCAL_FILE; + } + return SourceType.REMOTE_FILE; + } + + export async function getFilesSizes(sources: ResourceSource[]) { + const results: Array<{ + source: ResourceSource; + type: SourceType; + length: number; + previousFilesTotalLength: number; + }> = []; + let totalLength = 0; + let previousFilesTotalLength = 0; + for (const source of sources) { + const type = ResourceFetcherUtils.getType(source); + let length = 0; + try { + if (type === SourceType.REMOTE_FILE && typeof source === 'string') { + const response = await fetch(source, { method: 'HEAD' }); + if (!response.ok) { + Logger.warn( + `Failed to fetch HEAD for ${source}: ${response.status}` + ); + continue; + } + + const contentLength = response.headers.get('content-length'); + if (!contentLength) { + Logger.warn(`No content-length header for ${source}`); + } + + length = contentLength ? parseInt(contentLength, 10) : 0; + previousFilesTotalLength = totalLength; + totalLength += length; + } + } catch (error) { + Logger.warn(`Error fetching HEAD for ${source}:`, error); + continue; + } finally { + results.push({ source, type, length, previousFilesTotalLength }); + } + } + return { results, totalLength }; + } + + export async function createDirectoryIfNoExists() { + if (!(await checkFileExists(RNEDirectory))) { + await RNFS.mkdir(RNEDirectory); + } + } + + export async function checkFileExists(fileUri: string) { + return await RNFS.exists(fileUri); + } +} diff --git a/packages/bare-adapter/src/constants/directories.ts b/packages/bare-adapter/src/constants/directories.ts new file mode 100644 index 000000000..4ba8b8401 --- /dev/null +++ b/packages/bare-adapter/src/constants/directories.ts @@ -0,0 +1,3 @@ +import { directories } from '@kesha-antonov/react-native-background-downloader'; + +export const RNEDirectory = `${directories.documents}/react-native-executorch/`; diff --git a/packages/bare-adapter/src/index.ts b/packages/bare-adapter/src/index.ts new file mode 100644 index 000000000..c0ec4024f --- /dev/null +++ b/packages/bare-adapter/src/index.ts @@ -0,0 +1 @@ +export * from './ResourceFetcher'; diff --git a/packages/bare-adapter/tsconfig.json b/packages/bare-adapter/tsconfig.json new file mode 100644 index 000000000..841c9645a --- /dev/null +++ b/packages/bare-adapter/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "tsBuildInfoFile": "./lib/typescript/tsconfig.tsbuildinfo", + "composite": true, + "allowJs": false, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noStrictGenericChecks": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noUncheckedIndexedAccess": true, + "strict": true, + "types": ["react", "node"] + }, + "include": ["src"], + "exclude": ["node_modules", "lib"] +} diff --git a/packages/expo-adapter/README.md b/packages/expo-adapter/README.md new file mode 100644 index 000000000..46d085cff --- /dev/null +++ b/packages/expo-adapter/README.md @@ -0,0 +1,30 @@ +# @rn-executorch/expo-adapter + +Expo adapter for `react-native-executorch` that provides resource fetching capabilities using Expo's filesystem APIs. + +## Installation + +```bash +yarn add @rn-executorch/expo-adapter +yarn add expo-file-system expo-asset +``` + +## Usage + +```typescript +import { initExecutorch } from 'react-native-executorch'; +import { ExpoResourceFetcher } from '@rn-executorch/expo-adapter'; + +initExecutorch({ + resourceFetcher: ExpoResourceFetcher, +}); +``` + +## When to Use + +Use this adapter if you're working with: +- Expo projects +- Expo Router projects +- Projects using Expo managed workflow + +This adapter leverages `expo-file-system` and `expo-asset` to handle file operations and downloads. diff --git a/packages/expo-adapter/package.json b/packages/expo-adapter/package.json new file mode 100644 index 000000000..321c75e55 --- /dev/null +++ b/packages/expo-adapter/package.json @@ -0,0 +1,34 @@ +{ + "name": "@rn-executorch/expo-adapter", + "version": "0.1.0", + "description": "Expo adapter for react-native-executorch", + "main": "src/index.ts", + "types": "src/index.ts", + "exports": { + ".": { + "import": "./src/index.ts", + "types": "./src/index.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit", + "lint": "eslint \"**/*.{js,ts,tsx}\"" + }, + "peerDependencies": { + "expo": ">=54.0.0", + "expo-asset": "^12.0.0", + "expo-file-system": "^19.0.0", + "react-native": "*", + "react-native-executorch": "*" + }, + "devDependencies": { + "@types/react": "~19.1.10", + "expo": "^54.0.0", + "expo-asset": "12.0.11", + "expo-file-system": "^19.0.20", + "react": "19.1.0", + "react-native": "0.81.5", + "react-native-executorch": "workspace:*", + "typescript": "~5.9.2" + } +} diff --git a/packages/expo-adapter/src/ResourceFetcher.ts b/packages/expo-adapter/src/ResourceFetcher.ts new file mode 100644 index 000000000..584712e02 --- /dev/null +++ b/packages/expo-adapter/src/ResourceFetcher.ts @@ -0,0 +1,455 @@ +import { + cacheDirectory, + copyAsync, + createDownloadResumable, + moveAsync, + FileSystemSessionType, + writeAsStringAsync, + EncodingType, + deleteAsync, + readDirectoryAsync, + readAsStringAsync, +} from 'expo-file-system/legacy'; +import { Asset } from 'expo-asset'; +import { Platform } from 'react-native'; +import { RNEDirectory } from './constants/directories'; +import { + ResourceSource, + ResourceFetcherAdapter, + RnExecutorchErrorCode, + RnExecutorchError, +} from 'react-native-executorch'; +import { + ResourceFetcherUtils, + HTTP_CODE, + DownloadStatus, + SourceType, + ResourceSourceExtended, + DownloadResource, +} from './ResourceFetcherUtils'; + +interface ExpoResourceFetcherInterface extends ResourceFetcherAdapter { + downloads: Map; + singleFetch(sourceExtended: ResourceSourceExtended): Promise; + returnOrStartNext( + sourceExtended: ResourceSourceExtended, + result: string | string[] + ): string[] | Promise; + pause(source: ResourceSource): Promise; + resume(source: ResourceSource): Promise; + cancel(source: ResourceSource): Promise; + findActive(sources: ResourceSource[]): ResourceSource; + handleObject(source: ResourceSource): Promise; + handleLocalFile(source: ResourceSource): string; + handleReleaseModeFile( + sourceExtended: ResourceSourceExtended + ): Promise; + handleDevModeFile( + sourceExtended: ResourceSourceExtended + ): Promise; + handleRemoteFile( + sourceExtended: ResourceSourceExtended + ): Promise; +} + +export const ExpoResourceFetcher: ExpoResourceFetcherInterface = { + downloads: new Map(), //map of currently downloading (or paused) files, if the download was started by .fetch() method. + + async fetch( + callback: (downloadProgress: number) => void = () => {}, + ...sources: ResourceSource[] + ) { + if (sources.length === 0) { + throw new RnExecutorchError( + RnExecutorchErrorCode.InvalidUserInput, + 'Empty list given as an argument' + ); + } + const { results: info, totalLength } = + await ResourceFetcherUtils.getFilesSizes(sources); + const head: ResourceSourceExtended = { + source: info[0]!.source, + sourceType: info[0]!.type, + callback: + info[0]!.type === SourceType.REMOTE_FILE + ? ResourceFetcherUtils.calculateDownloadProgress( + totalLength, + info[0]!.previousFilesTotalLength, + info[0]!.length, + callback + ) + : () => {}, + results: [], + }; + + let node = head; + for (let idx = 1; idx < sources.length; idx++) { + node.next = { + source: info[idx]!.source, + sourceType: info[idx]!.type, + callback: + info[idx]!.type === SourceType.REMOTE_FILE + ? ResourceFetcherUtils.calculateDownloadProgress( + totalLength, + info[idx]!.previousFilesTotalLength, + info[idx]!.length, + callback + ) + : () => {}, + results: [], + }; + node = node.next; + } + return this.singleFetch(head); + }, + + async singleFetch( + sourceExtended: ResourceSourceExtended + ): Promise { + const source = sourceExtended.source; + switch (sourceExtended.sourceType) { + case SourceType.OBJECT: { + return this.returnOrStartNext( + sourceExtended, + await this.handleObject(source) + ); + } + case SourceType.LOCAL_FILE: { + return this.returnOrStartNext( + sourceExtended, + this.handleLocalFile(source) + ); + } + case SourceType.RELEASE_MODE_FILE: { + return this.returnOrStartNext( + sourceExtended, + await this.handleReleaseModeFile(sourceExtended) + ); + } + case SourceType.DEV_MODE_FILE: { + const result = await this.handleDevModeFile(sourceExtended); + if (result !== null) { + return this.returnOrStartNext(sourceExtended, result); + } + return null; + } + default: { + //case SourceType.REMOTE_FILE + const result = await this.handleRemoteFile(sourceExtended); + if (result !== null) { + return this.returnOrStartNext(sourceExtended, result); + } + return null; + } + } + }, + + //if any download ends successfully this function is called - it checks whether it should trigger next download or return list of paths. + returnOrStartNext(sourceExtended: ResourceSourceExtended, result: string) { + sourceExtended.results.push(result); + + if (sourceExtended.next) { + const nextSource = sourceExtended.next; + nextSource.results.push(...sourceExtended.results); + return this.singleFetch(nextSource); + } + sourceExtended.callback!(1); + return sourceExtended.results; + }, + + async pause(source: ResourceSource) { + const resource = this.downloads.get(source)!; + switch (resource.status) { + case DownloadStatus.PAUSED: + throw new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherAlreadyPaused, + "The file download is currently paused. Can't pause the download of the same file twice." + ); + default: { + resource.status = DownloadStatus.PAUSED; + await resource.downloadResumable.pauseAsync(); + } + } + }, + + async resume(source: ResourceSource) { + const resource = this.downloads.get(source)!; + if ( + !resource.extendedInfo.fileUri || + !resource.extendedInfo.cacheFileUri || + !resource.extendedInfo.uri + ) { + throw new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherMissingUri, + 'Something went wrong. File uri info is not specified' + ); + } + switch (resource.status) { + case DownloadStatus.ONGOING: + throw new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherAlreadyOngoing, + "The file download is currently ongoing. Can't resume the ongoing download." + ); + default: { + resource.status = DownloadStatus.ONGOING; + const result = await resource.downloadResumable.resumeAsync(); + if ( + !this.downloads.has(source) || + this.downloads.get(source)!.status === DownloadStatus.PAUSED + ) { + //if canceled or paused after earlier resuming. + return null; + } + if ( + !result || + (result.status !== HTTP_CODE.OK && + result.status !== HTTP_CODE.PARTIAL_CONTENT) + ) { + throw new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherDownloadFailed, + `Failed to fetch resource from '${resource.extendedInfo.uri}, context: ${result}'` + ); + } + await moveAsync({ + from: resource.extendedInfo.cacheFileUri, + to: resource.extendedInfo.fileUri, + }); + this.downloads.delete(source); + ResourceFetcherUtils.triggerHuggingFaceDownloadCounter( + resource.extendedInfo.uri + ); + + return this.returnOrStartNext( + resource.extendedInfo, + ResourceFetcherUtils.removeFilePrefix(resource.extendedInfo.fileUri) + ); + } + } + }, + + async cancel(source: ResourceSource) { + const resource = this.downloads.get(source)!; + await resource.downloadResumable.cancelAsync(); + this.downloads.delete(source); + }, + + async pauseFetching(...sources: ResourceSource[]) { + const source = this.findActive(sources); + await this.pause(source); + }, + + async resumeFetching(...sources: ResourceSource[]) { + const source = this.findActive(sources); + await this.resume(source); + }, + + async cancelFetching(...sources: ResourceSource[]) { + const source = this.findActive(sources); + await this.cancel(source); + }, + + findActive(sources: ResourceSource[]) { + for (const source of sources) { + if (this.downloads.has(source)) { + return source; + } + } + throw new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherNotActive, + 'None of given sources are currently during downloading process.' + ); + }, + + async listDownloadedFiles() { + const files = await readDirectoryAsync(RNEDirectory); + return files.map((file: string) => `${RNEDirectory}${file}`); + }, + + async listDownloadedModels() { + const files = await this.listDownloadedFiles(); + return files.filter((file: string) => file.endsWith('.pte')); + }, + + async deleteResources(...sources: ResourceSource[]) { + for (const source of sources) { + const filename = ResourceFetcherUtils.getFilenameFromUri( + source as string + ); + const fileUri = `${RNEDirectory}${filename}`; + if (await ResourceFetcherUtils.checkFileExists(fileUri)) { + await deleteAsync(fileUri); + } + } + }, + + async getFilesTotalSize(...sources: ResourceSource[]) { + return (await ResourceFetcherUtils.getFilesSizes(sources)).totalLength; + }, + + async handleObject(source: ResourceSource) { + if (typeof source !== 'object') { + throw new RnExecutorchError( + RnExecutorchErrorCode.InvalidModelSource, + 'Source is expected to be number' + ); + } + const jsonString = JSON.stringify(source); + const digest = ResourceFetcherUtils.hashObject(jsonString); + const filename = `${digest}.json`; + const path = `${RNEDirectory}${filename}`; + + if (await ResourceFetcherUtils.checkFileExists(path)) { + return ResourceFetcherUtils.removeFilePrefix(path); + } + + await ResourceFetcherUtils.createDirectoryIfNoExists(); + await writeAsStringAsync(path, jsonString, { + encoding: EncodingType.UTF8, + }); + + return ResourceFetcherUtils.removeFilePrefix(path); + }, + + handleLocalFile(source: ResourceSource) { + if (typeof source !== 'string') { + throw new RnExecutorchError( + RnExecutorchErrorCode.InvalidModelSource, + 'Source is expected to be string' + ); + } + return ResourceFetcherUtils.removeFilePrefix(source); + }, + + async handleReleaseModeFile(sourceExtended: ResourceSourceExtended) { + const source = sourceExtended.source; + if (typeof source !== 'number') { + throw new RnExecutorchError( + RnExecutorchErrorCode.InvalidModelSource, + 'Source is expected to be number' + ); + } + const asset = Asset.fromModule(source); + const uri = asset.uri; + const filename = ResourceFetcherUtils.getFilenameFromUri(uri); + const fileUri = `${RNEDirectory}${filename}`; + // On Android, file uri does not contain file extension, so we add it manually + const fileUriWithType = + Platform.OS === 'android' ? `${fileUri}.${asset.type}` : fileUri; + if (await ResourceFetcherUtils.checkFileExists(fileUri)) { + return ResourceFetcherUtils.removeFilePrefix(fileUri); + } + await ResourceFetcherUtils.createDirectoryIfNoExists(); + await copyAsync({ + from: asset.uri, + to: fileUriWithType, + }); + return ResourceFetcherUtils.removeFilePrefix(fileUriWithType); + }, + + async handleDevModeFile(sourceExtended: ResourceSourceExtended) { + const source = sourceExtended.source; + if (typeof source !== 'number') { + throw new RnExecutorchError( + RnExecutorchErrorCode.InvalidModelSource, + 'Source is expected to be a number' + ); + } + sourceExtended.uri = Asset.fromModule(source).uri; + return await this.handleRemoteFile(sourceExtended); + }, + + async handleRemoteFile(sourceExtended: ResourceSourceExtended) { + const source = sourceExtended.source; + if (typeof source === 'object') { + throw new RnExecutorchError( + RnExecutorchErrorCode.InvalidModelSource, + 'Source is expected to be a string or a number' + ); + } + if (this.downloads.has(source)) { + const resource = this.downloads.get(source)!; + if (resource.status === DownloadStatus.PAUSED) { + // if the download is paused, `fetch` is treated like `resume` + this.resume(source); + } + // if the download is ongoing, throw error. + throw new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherDownloadInProgress, + 'Already downloading this file' + ); + } + if (typeof source === 'number' && !sourceExtended.uri) { + throw new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherMissingUri, + 'Source Uri is expected to be available here' + ); + } + if (typeof source === 'string') { + sourceExtended.uri = source; + } + const uri = sourceExtended.uri!; + const filename = ResourceFetcherUtils.getFilenameFromUri(uri); + sourceExtended.fileUri = `${RNEDirectory}${filename}`; + sourceExtended.cacheFileUri = `${cacheDirectory}${filename}`; + + if (await ResourceFetcherUtils.checkFileExists(sourceExtended.fileUri)) { + return ResourceFetcherUtils.removeFilePrefix(sourceExtended.fileUri); + } + await ResourceFetcherUtils.createDirectoryIfNoExists(); + + const downloadResumable = createDownloadResumable( + uri, + sourceExtended.cacheFileUri, + { sessionType: FileSystemSessionType.BACKGROUND }, + ({ + totalBytesWritten, + totalBytesExpectedToWrite, + }: { + totalBytesWritten: number; + totalBytesExpectedToWrite: number; + }) => { + if (totalBytesExpectedToWrite === -1) { + // If totalBytesExpectedToWrite is -1, it means the server does not provide content length. + sourceExtended.callback!(0); + return; + } + sourceExtended.callback!(totalBytesWritten / totalBytesExpectedToWrite); + } + ); + //create value for the this.download Map + const downloadResource: DownloadResource = { + downloadResumable: downloadResumable, + status: DownloadStatus.ONGOING, + extendedInfo: sourceExtended, + }; + //add key-value pair to map + this.downloads.set(source, downloadResource); + const result = await downloadResumable.downloadAsync(); + if ( + !this.downloads.has(source) || + this.downloads.get(source)!.status === DownloadStatus.PAUSED + ) { + // if canceled or paused during the download + return null; + } + if (!result || result.status !== HTTP_CODE.OK) { + throw new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherDownloadFailed, + `Failed to fetch resource from '${source}, context: ${result}'` + ); + } + await moveAsync({ + from: sourceExtended.cacheFileUri, + to: sourceExtended.fileUri, + }); + this.downloads.delete(source); + ResourceFetcherUtils.triggerHuggingFaceDownloadCounter(uri); + return ResourceFetcherUtils.removeFilePrefix(sourceExtended.fileUri); + }, + + async readAsString(path: string) { + // Expo needs URI + const uri = path.startsWith('file://') ? path : `file://${path}`; + return await readAsStringAsync(uri); + }, +}; diff --git a/packages/expo-adapter/src/ResourceFetcherUtils.ts b/packages/expo-adapter/src/ResourceFetcherUtils.ts new file mode 100644 index 000000000..0b89cc4ac --- /dev/null +++ b/packages/expo-adapter/src/ResourceFetcherUtils.ts @@ -0,0 +1,106 @@ +import { RNEDirectory } from './constants/directories'; +import { + ResourceSource, + Logger, + ResourceFetcherUtils as CoreUtils, + HTTP_CODE, + DownloadStatus, + SourceType, + ResourceSourceExtended, +} from 'react-native-executorch'; +import { Asset } from 'expo-asset'; + +/** + * @internal + */ +import { + getInfoAsync, + makeDirectoryAsync, + type DownloadResumable, +} from 'expo-file-system/legacy'; + +export { HTTP_CODE, DownloadStatus, SourceType, ResourceSourceExtended }; + +export interface DownloadResource { + downloadResumable: DownloadResumable; + status: DownloadStatus; + extendedInfo: ResourceSourceExtended; +} + +export namespace ResourceFetcherUtils { + export const removeFilePrefix = CoreUtils.removeFilePrefix; + export const hashObject = CoreUtils.hashObject; + export const calculateDownloadProgress = CoreUtils.calculateDownloadProgress; + export const triggerHuggingFaceDownloadCounter = + CoreUtils.triggerHuggingFaceDownloadCounter; + export const getFilenameFromUri = CoreUtils.getFilenameFromUri; + + export function getType(source: ResourceSource): SourceType { + if (typeof source === 'object') { + return SourceType.OBJECT; + } else if (typeof source === 'number') { + const uri = Asset.fromModule(source).uri; + if (uri.startsWith('http')) { + return SourceType.DEV_MODE_FILE; + } + return SourceType.RELEASE_MODE_FILE; + } + // typeof source == 'string' + if (source.startsWith('file://')) { + return SourceType.LOCAL_FILE; + } + return SourceType.REMOTE_FILE; + } + + export async function getFilesSizes(sources: ResourceSource[]) { + const results: Array<{ + source: ResourceSource; + type: SourceType; + length: number; + previousFilesTotalLength: number; + }> = []; + let totalLength = 0; + let previousFilesTotalLength = 0; + for (const source of sources) { + const type = ResourceFetcherUtils.getType(source); + let length = 0; + try { + if (type === SourceType.REMOTE_FILE && typeof source === 'string') { + const response = await fetch(source, { method: 'HEAD' }); + if (!response.ok) { + Logger.warn( + `Failed to fetch HEAD for ${source}: ${response.status}` + ); + continue; + } + + const contentLength = response.headers.get('content-length'); + if (!contentLength) { + Logger.warn(`No content-length header for ${source}`); + } + + length = contentLength ? parseInt(contentLength, 10) : 0; + previousFilesTotalLength = totalLength; + totalLength += length; + } + } catch (error) { + Logger.warn(`Error fetching HEAD for ${source}:`, error); + continue; + } finally { + results.push({ source, type, length, previousFilesTotalLength }); + } + } + return { results, totalLength }; + } + + export async function createDirectoryIfNoExists() { + if (!(await checkFileExists(RNEDirectory))) { + await makeDirectoryAsync(RNEDirectory, { intermediates: true }); + } + } + + export async function checkFileExists(fileUri: string) { + const fileInfo = await getInfoAsync(fileUri); + return fileInfo.exists; + } +} diff --git a/packages/react-native-executorch/src/constants/directories.ts b/packages/expo-adapter/src/constants/directories.ts similarity index 100% rename from packages/react-native-executorch/src/constants/directories.ts rename to packages/expo-adapter/src/constants/directories.ts diff --git a/packages/expo-adapter/src/index.ts b/packages/expo-adapter/src/index.ts new file mode 100644 index 000000000..c0ec4024f --- /dev/null +++ b/packages/expo-adapter/src/index.ts @@ -0,0 +1 @@ +export * from './ResourceFetcher'; diff --git a/packages/expo-adapter/tsconfig.json b/packages/expo-adapter/tsconfig.json new file mode 100644 index 000000000..841c9645a --- /dev/null +++ b/packages/expo-adapter/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "tsBuildInfoFile": "./lib/typescript/tsconfig.tsbuildinfo", + "composite": true, + "allowJs": false, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noStrictGenericChecks": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noUncheckedIndexedAccess": true, + "strict": true, + "types": ["react", "node"] + }, + "include": ["src"], + "exclude": ["node_modules", "lib"] +} diff --git a/packages/react-native-executorch/package.json b/packages/react-native-executorch/package.json index 341f30273..9ccff9608 100644 --- a/packages/react-native-executorch/package.json +++ b/packages/react-native-executorch/package.json @@ -1,6 +1,6 @@ { "name": "react-native-executorch", - "version": "0.7.0", + "version": "0.8.0", "description": "An easy way to run AI models in React Native with ExecuTorch", "source": "./src/index.ts", "main": "./lib/module/index.js", @@ -65,9 +65,6 @@ "registry": "https://registry.npmjs.org/" }, "peerDependencies": { - "expo": ">=54.0.0", - "expo-asset": "^12.0.0", - "expo-file-system": "^19.0.0", "react": "*", "react-native": "*" }, @@ -75,9 +72,6 @@ "@react-native-community/cli": "latest", "@types/jest": "^29.5.5", "@types/react": "~19.1.10", - "expo": "^54.0.0", - "expo-asset": "12.0.11", - "expo-file-system": "^19.0.20", "jest": "^29.7.0", "metro-react-native-babel-preset": "^0.77.0", "react": "19.1.0", diff --git a/packages/react-native-executorch/src/controllers/LLMController.ts b/packages/react-native-executorch/src/controllers/LLMController.ts index 6be8d6fc9..aaee449e7 100644 --- a/packages/react-native-executorch/src/controllers/LLMController.ts +++ b/packages/react-native-executorch/src/controllers/LLMController.ts @@ -12,7 +12,6 @@ import { } from '../types/llm'; import { parseToolCall } from '../utils/llm'; import { Logger } from '../common/Logger'; -import { readAsStringAsync } from 'expo-file-system/legacy'; import { RnExecutorchError, parseUnknownError } from '../errors/errorUtils'; import { RnExecutorchErrorCode } from '../errors/ErrorCodes'; @@ -132,7 +131,7 @@ export class LLMController { } this.tokenizerConfig = JSON.parse( - await readAsStringAsync('file://' + tokenizerConfigPath!) + await ResourceFetcher.fs.readAsString(tokenizerConfigPath!) ); this.nativeModule = global.loadLLM(modelPath, tokenizerPath); this.isReadyCallback(true); diff --git a/packages/react-native-executorch/src/index.ts b/packages/react-native-executorch/src/index.ts index 3b8b3e8e4..9d438f275 100644 --- a/packages/react-native-executorch/src/index.ts +++ b/packages/react-native-executorch/src/index.ts @@ -1,4 +1,20 @@ import { ETInstallerNativeModule } from './native/RnExecutorchModules'; +import { + ResourceFetcher, + ResourceFetcherAdapter, +} from './utils/ResourceFetcher'; + +export interface ExecutorchConfig { + resourceFetcher: ResourceFetcherAdapter; +} + +export function initExecutorch(config: ExecutorchConfig) { + ResourceFetcher.setAdapter(config.resourceFetcher); +} + +export function cleanupExecutorch() { + ResourceFetcher.setAdapter(null as unknown as ResourceFetcherAdapter); +} // eslint-disable no-var declare global { @@ -112,7 +128,9 @@ export * from './modules/general/ExecutorchModule'; // utils export * from './utils/ResourceFetcher'; +export * from './utils/ResourceFetcherUtils'; export * from './utils/llm'; +export * from './common/Logger'; // types export * from './types/objectDetection'; diff --git a/packages/react-native-executorch/src/utils/ResourceFetcher.ts b/packages/react-native-executorch/src/utils/ResourceFetcher.ts index 8e733e1ce..15fce7b4e 100644 --- a/packages/react-native-executorch/src/utils/ResourceFetcher.ts +++ b/packages/react-native-executorch/src/utils/ResourceFetcher.ts @@ -28,427 +28,80 @@ * - Automatically processes subsequent downloads when `.next` contains a valid resource */ -import { - cacheDirectory, - copyAsync, - createDownloadResumable, - moveAsync, - FileSystemSessionType, - writeAsStringAsync, - EncodingType, - deleteAsync, - readDirectoryAsync, -} from 'expo-file-system/legacy'; -import { Asset } from 'expo-asset'; -import { Platform } from 'react-native'; -import { RNEDirectory } from '../constants/directories'; import { ResourceSource } from '../types/common'; -import { - ResourceFetcherUtils, - HTTP_CODE, - DownloadStatus, - SourceType, - ResourceSourceExtended, - DownloadResource, -} from './ResourceFetcherUtils'; -import { RnExecutorchErrorCode } from '../errors/ErrorCodes'; import { RnExecutorchError } from '../errors/errorUtils'; +import { RnExecutorchErrorCode } from '../errors/ErrorCodes'; -export class ResourceFetcher { - static downloads = new Map(); //map of currently downloading (or paused) files, if the download was started by .fetch() method. - - static async fetch( - callback: (downloadProgress: number) => void = () => {}, +export interface ResourceFetcherAdapter { + fetch( + callback: (downloadProgress: number) => void, ...sources: ResourceSource[] - ) { - if (sources.length === 0) { - throw new RnExecutorchError( - RnExecutorchErrorCode.InvalidUserInput, - 'Empty list given as an argument' - ); - } - const { results: info, totalLength } = - await ResourceFetcherUtils.getFilesSizes(sources); - const head: ResourceSourceExtended = { - source: info[0]!.source, - sourceType: info[0]!.type, - callback: - info[0]!.type === SourceType.REMOTE_FILE - ? ResourceFetcherUtils.calculateDownloadProgress( - totalLength, - info[0]!.previousFilesTotalLength, - info[0]!.length, - callback - ) - : () => {}, - results: [], - }; - - let node = head; - for (let idx = 1; idx < sources.length; idx++) { - node.next = { - source: info[idx]!.source, - sourceType: info[idx]!.type, - callback: - info[idx]!.type === SourceType.REMOTE_FILE - ? ResourceFetcherUtils.calculateDownloadProgress( - totalLength, - info[idx]!.previousFilesTotalLength, - info[idx]!.length, - callback - ) - : () => {}, - results: [], - }; - node = node.next; - } - return this.singleFetch(head); - } - - private static async singleFetch( - sourceExtended: ResourceSourceExtended - ): Promise { - const source = sourceExtended.source; - switch (sourceExtended.sourceType) { - case SourceType.OBJECT: { - return this.returnOrStartNext( - sourceExtended, - await this.handleObject(source) - ); - } - case SourceType.LOCAL_FILE: { - return this.returnOrStartNext( - sourceExtended, - this.handleLocalFile(source) - ); - } - case SourceType.RELEASE_MODE_FILE: { - return this.returnOrStartNext( - sourceExtended, - await this.handleReleaseModeFile(sourceExtended) - ); - } - case SourceType.DEV_MODE_FILE: { - const result = await this.handleDevModeFile(sourceExtended); - if (result !== null) { - return this.returnOrStartNext(sourceExtended, result); - } - return null; - } - default: { - //case SourceType.REMOTE_FILE - const result = await this.handleRemoteFile(sourceExtended); - if (result !== null) { - return this.returnOrStartNext(sourceExtended, result); - } - return null; - } - } - } - - //if any download ends successfully this function is called - it checks whether it should trigger next download or return list of paths. - private static returnOrStartNext( - sourceExtended: ResourceSourceExtended, - result: string - ) { - sourceExtended.results.push(result); + ): Promise; + pauseFetching(...sources: ResourceSource[]): Promise; + resumeFetching(...sources: ResourceSource[]): Promise; + cancelFetching(...sources: ResourceSource[]): Promise; + listDownloadedFiles(): Promise; + listDownloadedModels(): Promise; + deleteResources(...sources: ResourceSource[]): Promise; + getFilesTotalSize(...sources: ResourceSource[]): Promise; + readAsString(path: string): Promise; +} - if (sourceExtended.next) { - const nextSource = sourceExtended.next; - nextSource.results.push(...sourceExtended.results); - return this.singleFetch(nextSource); - } - sourceExtended.callback!(1); - return sourceExtended.results; - } +export class ResourceFetcher { + private static adapter: ResourceFetcherAdapter | null = null; - private static async pause(source: ResourceSource) { - const resource = this.downloads.get(source)!; - switch (resource.status) { - case DownloadStatus.PAUSED: - throw new RnExecutorchError( - RnExecutorchErrorCode.ResourceFetcherAlreadyPaused, - "The file download is currently paused. Can't pause the download of the same file twice." - ); - default: { - resource.status = DownloadStatus.PAUSED; - await resource.downloadResumable.pauseAsync(); - } - } + static setAdapter(adapter: ResourceFetcherAdapter) { + this.adapter = adapter; } - private static async resume(source: ResourceSource) { - const resource = this.downloads.get(source)!; - if ( - !resource.extendedInfo.fileUri || - !resource.extendedInfo.cacheFileUri || - !resource.extendedInfo.uri - ) { + static getAdapter(): ResourceFetcherAdapter { + if (!this.adapter) { throw new RnExecutorchError( - RnExecutorchErrorCode.ResourceFetcherMissingUri, - 'Something went wrong. File uri info is not specified' + RnExecutorchErrorCode.NotImplemented, + 'ResourceFetcher adapter is not initialized. Please call initExecutorch({ resourceFetcher: ... }) with a valid adapter, e.g., from @rn-executorch/expo-adapter or @rn-executorch/bare-adapter.' ); } - switch (resource.status) { - case DownloadStatus.ONGOING: - throw new RnExecutorchError( - RnExecutorchErrorCode.ResourceFetcherAlreadyOngoing, - "The file download is currently ongoing. Can't resume the ongoing download." - ); - default: { - resource.status = DownloadStatus.ONGOING; - const result = await resource.downloadResumable.resumeAsync(); - if ( - !this.downloads.has(source) || - this.downloads.get(source)!.status === DownloadStatus.PAUSED - ) { - //if canceled or paused after earlier resuming. - return null; - } - if ( - !result || - (result.status !== HTTP_CODE.OK && - result.status !== HTTP_CODE.PARTIAL_CONTENT) - ) { - throw new RnExecutorchError( - RnExecutorchErrorCode.ResourceFetcherDownloadFailed, - `Failed to fetch resource from '${resource.extendedInfo.uri}, context: ${result}'` - ); - } - await moveAsync({ - from: resource.extendedInfo.cacheFileUri, - to: resource.extendedInfo.fileUri, - }); - this.downloads.delete(source); - ResourceFetcherUtils.triggerHuggingFaceDownloadCounter( - resource.extendedInfo.uri - ); - - return this.returnOrStartNext( - resource.extendedInfo, - ResourceFetcherUtils.removeFilePrefix(resource.extendedInfo.fileUri) - ); - } - } + return this.adapter; } - private static async cancel(source: ResourceSource) { - const resource = this.downloads.get(source)!; - await resource.downloadResumable.cancelAsync(); - this.downloads.delete(source); + static async fetch( + callback: (downloadProgress: number) => void = () => {}, + ...sources: ResourceSource[] + ) { + return this.getAdapter().fetch(callback, ...sources); } static async pauseFetching(...sources: ResourceSource[]) { - const source = this.findActive(sources); - await this.pause(source); + return this.getAdapter().pauseFetching(...sources); } static async resumeFetching(...sources: ResourceSource[]) { - const source = this.findActive(sources); - await this.resume(source); + return this.getAdapter().resumeFetching(...sources); } static async cancelFetching(...sources: ResourceSource[]) { - const source = this.findActive(sources); - await this.cancel(source); - } - - private static findActive(sources: ResourceSource[]) { - for (const source of sources) { - if (this.downloads.has(source)) { - return source; - } - } - throw new RnExecutorchError( - RnExecutorchErrorCode.ResourceFetcherNotActive, - 'None of given sources are currently during downloading process.' - ); + return this.getAdapter().cancelFetching(...sources); } static async listDownloadedFiles() { - const files = await readDirectoryAsync(RNEDirectory); - return files.map((file) => `${RNEDirectory}${file}`); + return this.getAdapter().listDownloadedFiles(); } static async listDownloadedModels() { - const files = await this.listDownloadedFiles(); - return files.filter((file) => file.endsWith('.pte')); + return this.getAdapter().listDownloadedModels(); } static async deleteResources(...sources: ResourceSource[]) { - for (const source of sources) { - const filename = ResourceFetcherUtils.getFilenameFromUri( - source as string - ); - const fileUri = `${RNEDirectory}${filename}`; - if (await ResourceFetcherUtils.checkFileExists(fileUri)) { - await deleteAsync(fileUri); - } - } + return this.getAdapter().deleteResources(...sources); } static async getFilesTotalSize(...sources: ResourceSource[]) { - return (await ResourceFetcherUtils.getFilesSizes(sources)).totalLength; - } - - private static async handleObject(source: ResourceSource) { - if (typeof source !== 'object') { - throw new RnExecutorchError( - RnExecutorchErrorCode.InvalidModelSource, - 'Source is expected to be object' - ); - } - const jsonString = JSON.stringify(source); - const digest = ResourceFetcherUtils.hashObject(jsonString); - const filename = `${digest}.json`; - const path = `${RNEDirectory}${filename}`; - - if (await ResourceFetcherUtils.checkFileExists(path)) { - return ResourceFetcherUtils.removeFilePrefix(path); - } - - await ResourceFetcherUtils.createDirectoryIfNoExists(); - await writeAsStringAsync(path, jsonString, { - encoding: EncodingType.UTF8, - }); - - return ResourceFetcherUtils.removeFilePrefix(path); - } - - private static handleLocalFile(source: ResourceSource) { - if (typeof source !== 'string') { - throw new RnExecutorchError( - RnExecutorchErrorCode.InvalidModelSource, - 'Source is expected to be string' - ); - } - return ResourceFetcherUtils.removeFilePrefix(source); - } - - private static async handleReleaseModeFile( - sourceExtended: ResourceSourceExtended - ) { - const source = sourceExtended.source; - if (typeof source !== 'number') { - throw new RnExecutorchError( - RnExecutorchErrorCode.InvalidModelSource, - 'Source is expected to be number' - ); - } - const asset = Asset.fromModule(source); - const uri = asset.uri; - const filename = ResourceFetcherUtils.getFilenameFromUri(uri); - const fileUri = `${RNEDirectory}${filename}`; - // On Android, file uri does not contain file extension, so we add it manually - const fileUriWithType = - Platform.OS === 'android' ? `${fileUri}.${asset.type}` : fileUri; - if (await ResourceFetcherUtils.checkFileExists(fileUri)) { - return ResourceFetcherUtils.removeFilePrefix(fileUri); - } - await ResourceFetcherUtils.createDirectoryIfNoExists(); - await copyAsync({ - from: asset.uri, - to: fileUriWithType, - }); - return ResourceFetcherUtils.removeFilePrefix(fileUriWithType); - } - - private static async handleDevModeFile( - sourceExtended: ResourceSourceExtended - ) { - const source = sourceExtended.source; - if (typeof source !== 'number') { - throw new RnExecutorchError( - RnExecutorchErrorCode.InvalidModelSource, - 'Source is expected to be a number' - ); - } - sourceExtended.uri = Asset.fromModule(source).uri; - return await this.handleRemoteFile(sourceExtended); + return this.getAdapter().getFilesTotalSize(...sources); } - private static async handleRemoteFile( - sourceExtended: ResourceSourceExtended - ) { - const source = sourceExtended.source; - if (typeof source === 'object') { - throw new RnExecutorchError( - RnExecutorchErrorCode.InvalidModelSource, - 'Source is expected to be a string or a number' - ); - } - if (this.downloads.has(source)) { - const resource = this.downloads.get(source)!; - if (resource.status === DownloadStatus.PAUSED) { - // if the download is paused, `fetch` is treated like `resume` - this.resume(source); - } - // if the download is ongoing, throw error. - throw new RnExecutorchError( - RnExecutorchErrorCode.ResourceFetcherDownloadInProgress, - 'Already downloading this file' - ); - } - if (typeof source === 'number' && !sourceExtended.uri) { - throw new RnExecutorchError( - RnExecutorchErrorCode.ResourceFetcherMissingUri, - 'Source Uri is expected to be available here' - ); - } - if (typeof source === 'string') { - sourceExtended.uri = source; - } - const uri = sourceExtended.uri!; - const filename = ResourceFetcherUtils.getFilenameFromUri(uri); - sourceExtended.fileUri = `${RNEDirectory}${filename}`; - sourceExtended.cacheFileUri = `${cacheDirectory}${filename}`; - - if (await ResourceFetcherUtils.checkFileExists(sourceExtended.fileUri)) { - return ResourceFetcherUtils.removeFilePrefix(sourceExtended.fileUri); - } - await ResourceFetcherUtils.createDirectoryIfNoExists(); - - const downloadResumable = createDownloadResumable( - uri, - sourceExtended.cacheFileUri, - { sessionType: FileSystemSessionType.BACKGROUND }, - ({ totalBytesWritten, totalBytesExpectedToWrite }) => { - if (totalBytesExpectedToWrite === -1) { - // If totalBytesExpectedToWrite is -1, it means the server does not provide content length. - sourceExtended.callback!(0); - return; - } - sourceExtended.callback!(totalBytesWritten / totalBytesExpectedToWrite); - } - ); - //create value for the this.download Map - const downloadResource: DownloadResource = { - downloadResumable: downloadResumable, - status: DownloadStatus.ONGOING, - extendedInfo: sourceExtended, - }; - //add key-value pair to map - this.downloads.set(source, downloadResource); - const result = await downloadResumable.downloadAsync(); - if ( - !this.downloads.has(source) || - this.downloads.get(source)!.status === DownloadStatus.PAUSED - ) { - // if canceled or paused during the download - return null; - } - if (!result || result.status !== HTTP_CODE.OK) { - throw new RnExecutorchError( - RnExecutorchErrorCode.ResourceFetcherDownloadFailed, - `Failed to fetch resource from '${source}, context: ${result}'` - ); - } - await moveAsync({ - from: sourceExtended.cacheFileUri, - to: sourceExtended.fileUri, - }); - this.downloads.delete(source); - ResourceFetcherUtils.triggerHuggingFaceDownloadCounter(uri); - return ResourceFetcherUtils.removeFilePrefix(sourceExtended.fileUri); - } + static fs = { + readAsString: async (path: string) => { + return this.getAdapter().readAsString(path); + }, + }; } diff --git a/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts b/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts index 207adce9e..ae60b4ab0 100644 --- a/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts +++ b/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts @@ -1,28 +1,16 @@ -import { RNEDirectory } from '../constants/directories'; -import { ResourceSource } from '../types/common'; -import { Asset } from 'expo-asset'; -import { Logger } from '../common/Logger'; +import { ResourceSource } from '..'; -/** - * @internal - */ -import { - getInfoAsync, - makeDirectoryAsync, - type DownloadResumable, -} from 'expo-file-system/legacy'; - -export const enum HTTP_CODE { +export enum HTTP_CODE { OK = 200, PARTIAL_CONTENT = 206, } -export const enum DownloadStatus { +export enum DownloadStatus { ONGOING, PAUSED, } -export const enum SourceType { +export enum SourceType { OBJECT, LOCAL_FILE, RELEASE_MODE_FILE, @@ -41,71 +29,7 @@ export interface ResourceSourceExtended { next?: ResourceSourceExtended; } -export interface DownloadResource { - downloadResumable: DownloadResumable; - status: DownloadStatus; - extendedInfo: ResourceSourceExtended; -} - export namespace ResourceFetcherUtils { - export function getType(source: ResourceSource): SourceType { - if (typeof source === 'object') { - return SourceType.OBJECT; - } else if (typeof source === 'number') { - const uri = Asset.fromModule(source).uri; - if (uri.startsWith('http')) { - return SourceType.DEV_MODE_FILE; - } - return SourceType.RELEASE_MODE_FILE; - } - // typeof source == 'string' - if (source.startsWith('file://')) { - return SourceType.LOCAL_FILE; - } - return SourceType.REMOTE_FILE; - } - - export async function getFilesSizes(sources: ResourceSource[]) { - const results: Array<{ - source: ResourceSource; - type: SourceType; - length: number; - previousFilesTotalLength: number; - }> = []; - let totalLength = 0; - let previousFilesTotalLength = 0; - for (const source of sources) { - const type = ResourceFetcherUtils.getType(source); - let length = 0; - try { - if (type === SourceType.REMOTE_FILE && typeof source === 'string') { - const response = await fetch(source, { method: 'HEAD' }); - if (!response.ok) { - Logger.warn( - `Failed to fetch HEAD for ${source}: ${response.status}` - ); - continue; - } - - const contentLength = response.headers.get('content-length'); - if (!contentLength) { - Logger.warn(`No content-length header for ${source}`); - } - - length = contentLength ? parseInt(contentLength, 10) : 0; - previousFilesTotalLength = totalLength; - totalLength += length; - } - } catch (error) { - Logger.warn(`Error fetching HEAD for ${source}:`, error); - continue; - } finally { - results.push({ source, type, length, previousFilesTotalLength }); - } - } - return { results, totalLength }; - } - export function removeFilePrefix(uri: string) { return uri.startsWith('file://') ? uri.slice(7) : uri; } @@ -165,17 +89,6 @@ export namespace ResourceFetcherUtils { } } - export async function createDirectoryIfNoExists() { - if (!(await checkFileExists(RNEDirectory))) { - await makeDirectoryAsync(RNEDirectory, { intermediates: true }); - } - } - - export async function checkFileExists(fileUri: string) { - const fileInfo = await getInfoAsync(fileUri); - return fileInfo.exists; - } - export function getFilenameFromUri(uri: string) { let cleanUri = uri.replace(/^https?:\/\//, ''); cleanUri = cleanUri.split('#')?.[0] ?? cleanUri; diff --git a/yarn.lock b/yarn.lock index 1ca8d5d29..ac2641d98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -60,7 +60,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.20.0, @babel/core@npm:^7.21.3, @babel/core@npm:^7.23.9, @babel/core@npm:^7.25.2": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.20.0, @babel/core@npm:^7.21.3, @babel/core@npm:^7.23.9, @babel/core@npm:^7.25.2": version: 7.28.5 resolution: "@babel/core@npm:7.28.5" dependencies: @@ -332,7 +332,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.25.3, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.5": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.25.3, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.5": version: 7.28.5 resolution: "@babel/parser@npm:7.28.5" dependencies: @@ -416,7 +416,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-proposal-class-properties@npm:^7.13.0, @babel/plugin-proposal-class-properties@npm:^7.18.0": +"@babel/plugin-proposal-class-properties@npm:^7.18.0": version: 7.18.6 resolution: "@babel/plugin-proposal-class-properties@npm:7.18.6" dependencies: @@ -452,7 +452,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-proposal-nullish-coalescing-operator@npm:^7.13.8, @babel/plugin-proposal-nullish-coalescing-operator@npm:^7.18.0": +"@babel/plugin-proposal-nullish-coalescing-operator@npm:^7.18.0": version: 7.18.6 resolution: "@babel/plugin-proposal-nullish-coalescing-operator@npm:7.18.6" dependencies: @@ -503,7 +503,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-proposal-optional-chaining@npm:^7.13.12, @babel/plugin-proposal-optional-chaining@npm:^7.20.0": +"@babel/plugin-proposal-optional-chaining@npm:^7.20.0": version: 7.21.0 resolution: "@babel/plugin-proposal-optional-chaining@npm:7.21.0" dependencies: @@ -982,7 +982,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-flow-strip-types@npm:^7.20.0, @babel/plugin-transform-flow-strip-types@npm:^7.25.2, @babel/plugin-transform-flow-strip-types@npm:^7.26.5, @babel/plugin-transform-flow-strip-types@npm:^7.27.1": +"@babel/plugin-transform-flow-strip-types@npm:^7.20.0, @babel/plugin-transform-flow-strip-types@npm:^7.25.2, @babel/plugin-transform-flow-strip-types@npm:^7.26.5": version: 7.27.1 resolution: "@babel/plugin-transform-flow-strip-types@npm:7.27.1" dependencies: @@ -1075,7 +1075,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.0.0, @babel/plugin-transform-modules-commonjs@npm:^7.13.8, @babel/plugin-transform-modules-commonjs@npm:^7.24.8, @babel/plugin-transform-modules-commonjs@npm:^7.27.1": +"@babel/plugin-transform-modules-commonjs@npm:^7.0.0, @babel/plugin-transform-modules-commonjs@npm:^7.24.8, @babel/plugin-transform-modules-commonjs@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-modules-commonjs@npm:7.27.1" dependencies: @@ -1585,19 +1585,6 @@ __metadata: languageName: node linkType: hard -"@babel/preset-flow@npm:^7.13.13": - version: 7.27.1 - resolution: "@babel/preset-flow@npm:7.27.1" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/helper-validator-option": "npm:^7.27.1" - "@babel/plugin-transform-flow-strip-types": "npm:^7.27.1" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10/f3f25b390debf72a6ff0170a2d5198aea344ba96f05eaca0bae2c7072119706fd46321604d89646bda1842527cfc6eab8828a983ec90149218d2120b9cd26596 - languageName: node - linkType: hard - "@babel/preset-modules@npm:0.1.6-no-external-plugins": version: 0.1.6-no-external-plugins resolution: "@babel/preset-modules@npm:0.1.6-no-external-plugins" @@ -1627,7 +1614,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-typescript@npm:^7.13.0, @babel/preset-typescript@npm:^7.16.7, @babel/preset-typescript@npm:^7.23.0, @babel/preset-typescript@npm:^7.24.7": +"@babel/preset-typescript@npm:^7.16.7, @babel/preset-typescript@npm:^7.23.0, @babel/preset-typescript@npm:^7.24.7": version: 7.28.5 resolution: "@babel/preset-typescript@npm:7.28.5" dependencies: @@ -1642,21 +1629,6 @@ __metadata: languageName: node linkType: hard -"@babel/register@npm:^7.13.16": - version: 7.28.3 - resolution: "@babel/register@npm:7.28.3" - dependencies: - clone-deep: "npm:^4.0.1" - find-cache-dir: "npm:^2.0.0" - make-dir: "npm:^2.1.0" - pirates: "npm:^4.0.6" - source-map-support: "npm:^0.5.16" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10/9475696152579933dbb0ffa7f47e0a3d130064101fc6ee471ec4cd8f20c70438798f3165708cb2ad29f585d81af0a26a4c233d732fbe63c7abec6a63b7d509d8 - languageName: node - linkType: hard - "@babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.25.0": version: 7.28.4 resolution: "@babel/runtime@npm:7.28.4" @@ -2272,6 +2244,19 @@ __metadata: languageName: node linkType: hard +"@dr.pogodin/react-native-fs@npm:^2.36.2": + version: 2.36.2 + resolution: "@dr.pogodin/react-native-fs@npm:2.36.2" + dependencies: + buffer: "npm:^6.0.3" + http-status-codes: "npm:^2.3.0" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/6a4b1c324386c455e01ccf45daf80474364079bc6495f0a65c1468e4e15607951754b81d9d720fb635a8caba5ba94b34719d0140357ce8e168172583e09156d4 + languageName: node + linkType: hard + "@egjs/hammerjs@npm:^2.0.17": version: 2.0.17 resolution: "@egjs/hammerjs@npm:2.0.17" @@ -2415,6 +2400,88 @@ __metadata: languageName: node linkType: hard +"@expo/cli@npm:54.0.22": + version: 54.0.22 + resolution: "@expo/cli@npm:54.0.22" + dependencies: + "@0no-co/graphql.web": "npm:^1.0.8" + "@expo/code-signing-certificates": "npm:^0.0.6" + "@expo/config": "npm:~12.0.13" + "@expo/config-plugins": "npm:~54.0.4" + "@expo/devcert": "npm:^1.2.1" + "@expo/env": "npm:~2.0.8" + "@expo/image-utils": "npm:^0.8.8" + "@expo/json-file": "npm:^10.0.8" + "@expo/metro": "npm:~54.2.0" + "@expo/metro-config": "npm:~54.0.14" + "@expo/osascript": "npm:^2.3.8" + "@expo/package-manager": "npm:^1.9.10" + "@expo/plist": "npm:^0.4.8" + "@expo/prebuild-config": "npm:^54.0.8" + "@expo/schema-utils": "npm:^0.1.8" + "@expo/spawn-async": "npm:^1.7.2" + "@expo/ws-tunnel": "npm:^1.0.1" + "@expo/xcpretty": "npm:^4.3.0" + "@react-native/dev-middleware": "npm:0.81.5" + "@urql/core": "npm:^5.0.6" + "@urql/exchange-retry": "npm:^1.3.0" + accepts: "npm:^1.3.8" + arg: "npm:^5.0.2" + better-opn: "npm:~3.0.2" + bplist-creator: "npm:0.1.0" + bplist-parser: "npm:^0.3.1" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.3.0" + compression: "npm:^1.7.4" + connect: "npm:^3.7.0" + debug: "npm:^4.3.4" + env-editor: "npm:^0.4.1" + expo-server: "npm:^1.0.5" + freeport-async: "npm:^2.0.0" + getenv: "npm:^2.0.0" + glob: "npm:^13.0.0" + lan-network: "npm:^0.1.6" + minimatch: "npm:^9.0.0" + node-forge: "npm:^1.3.3" + npm-package-arg: "npm:^11.0.0" + ora: "npm:^3.4.0" + picomatch: "npm:^3.0.1" + pretty-bytes: "npm:^5.6.0" + pretty-format: "npm:^29.7.0" + progress: "npm:^2.0.3" + prompts: "npm:^2.3.2" + qrcode-terminal: "npm:0.11.0" + require-from-string: "npm:^2.0.2" + requireg: "npm:^0.2.2" + resolve: "npm:^1.22.2" + resolve-from: "npm:^5.0.0" + resolve.exports: "npm:^2.0.3" + semver: "npm:^7.6.0" + send: "npm:^0.19.0" + slugify: "npm:^1.3.4" + source-map-support: "npm:~0.5.21" + stacktrace-parser: "npm:^0.1.10" + structured-headers: "npm:^0.4.1" + tar: "npm:^7.5.2" + terminal-link: "npm:^2.1.1" + undici: "npm:^6.18.2" + wrap-ansi: "npm:^7.0.0" + ws: "npm:^8.12.1" + peerDependencies: + expo: "*" + expo-router: "*" + react-native: "*" + peerDependenciesMeta: + expo-router: + optional: true + react-native: + optional: true + bin: + expo-internal: build/bin/cli + checksum: 10/c20c89f5737e1f5b055567d6b560a920ea52ed17cab73fd5ab59d8f1640c62f7a9915d9d69eb0c5ebc85bd04505e3ac32ba329ab8f1e15da8a9fcd6c6dd8d808 + languageName: node + linkType: hard + "@expo/code-signing-certificates@npm:^0.0.5": version: 0.0.5 resolution: "@expo/code-signing-certificates@npm:0.0.5" @@ -2425,6 +2492,15 @@ __metadata: languageName: node linkType: hard +"@expo/code-signing-certificates@npm:^0.0.6": + version: 0.0.6 + resolution: "@expo/code-signing-certificates@npm:0.0.6" + dependencies: + node-forge: "npm:^1.3.3" + checksum: 10/4446cca45e8b48b90ba728e39aab6b1195ede730d7aba7d9830f635aa16a52634e6eba9dc510f83cc6ff6fb6b0e3077bc6021098f0157f6dba96f8494685c388 + languageName: node + linkType: hard + "@expo/config-plugins@npm:~54.0.3": version: 54.0.3 resolution: "@expo/config-plugins@npm:54.0.3" @@ -2447,6 +2523,35 @@ __metadata: languageName: node linkType: hard +"@expo/config-plugins@npm:~54.0.4": + version: 54.0.4 + resolution: "@expo/config-plugins@npm:54.0.4" + dependencies: + "@expo/config-types": "npm:^54.0.10" + "@expo/json-file": "npm:~10.0.8" + "@expo/plist": "npm:^0.4.8" + "@expo/sdk-runtime-versions": "npm:^1.0.0" + chalk: "npm:^4.1.2" + debug: "npm:^4.3.5" + getenv: "npm:^2.0.0" + glob: "npm:^13.0.0" + resolve-from: "npm:^5.0.0" + semver: "npm:^7.5.4" + slash: "npm:^3.0.0" + slugify: "npm:^1.6.6" + xcode: "npm:^3.0.1" + xml2js: "npm:0.6.0" + checksum: 10/55dab3f5f29b6dfb58bc32a9b0a681766f6b260ee94b1c295f67ac3c5e8f372afc512bb416f2e50901e387d4012e3a4a8fd3b461e5aa8c20e16fdcde64a07327 + languageName: node + linkType: hard + +"@expo/config-types@npm:^54.0.10": + version: 54.0.10 + resolution: "@expo/config-types@npm:54.0.10" + checksum: 10/7e4d598d2d1905dc53f2b30d5a1e0817dd486b13c89a24575deb4e25ec441b0de009d156f041a3c9a1f2121dfba28f2a24fd4fb5a056cac90502ca67c639bb8a + languageName: node + linkType: hard + "@expo/config-types@npm:^54.0.9": version: 54.0.9 resolution: "@expo/config-types@npm:54.0.9" @@ -2475,6 +2580,27 @@ __metadata: languageName: node linkType: hard +"@expo/config@npm:~12.0.13": + version: 12.0.13 + resolution: "@expo/config@npm:12.0.13" + dependencies: + "@babel/code-frame": "npm:~7.10.4" + "@expo/config-plugins": "npm:~54.0.4" + "@expo/config-types": "npm:^54.0.10" + "@expo/json-file": "npm:^10.0.8" + deepmerge: "npm:^4.3.1" + getenv: "npm:^2.0.0" + glob: "npm:^13.0.0" + require-from-string: "npm:^2.0.2" + resolve-from: "npm:^5.0.0" + resolve-workspace-root: "npm:^2.0.0" + semver: "npm:^7.6.0" + slugify: "npm:^1.3.4" + sucrase: "npm:~3.35.1" + checksum: 10/2caac758fb706a75fc6d07df31c24c22d633f522091148e615d9c28475ae35cfaed29458cfd08f13d40d71d33715e5ac618af78591c11886529157b8519fe4ea + languageName: node + linkType: hard + "@expo/devcert@npm:^1.2.1": version: 1.2.1 resolution: "@expo/devcert@npm:1.2.1" @@ -2564,6 +2690,16 @@ __metadata: languageName: node linkType: hard +"@expo/json-file@npm:^10.0.9, @expo/json-file@npm:~10.0.8": + version: 10.0.9 + resolution: "@expo/json-file@npm:10.0.9" + dependencies: + "@babel/code-frame": "npm:~7.10.4" + json5: "npm:^2.2.3" + checksum: 10/40c758fcdb37b35aa5ca43acd2c882763f073672ccaf53038bf94100a5c511ff90e0d1f900a4c49394ae968bdbabbedfc39f961b7d60a47abecb8ec55c6863da + languageName: node + linkType: hard + "@expo/metro-config@npm:54.0.10, @expo/metro-config@npm:~54.0.10": version: 54.0.10 resolution: "@expo/metro-config@npm:54.0.10" @@ -2598,6 +2734,40 @@ __metadata: languageName: node linkType: hard +"@expo/metro-config@npm:54.0.14, @expo/metro-config@npm:~54.0.14": + version: 54.0.14 + resolution: "@expo/metro-config@npm:54.0.14" + dependencies: + "@babel/code-frame": "npm:^7.20.0" + "@babel/core": "npm:^7.20.0" + "@babel/generator": "npm:^7.20.5" + "@expo/config": "npm:~12.0.13" + "@expo/env": "npm:~2.0.8" + "@expo/json-file": "npm:~10.0.8" + "@expo/metro": "npm:~54.2.0" + "@expo/spawn-async": "npm:^1.7.2" + browserslist: "npm:^4.25.0" + chalk: "npm:^4.1.0" + debug: "npm:^4.3.2" + dotenv: "npm:~16.4.5" + dotenv-expand: "npm:~11.0.6" + getenv: "npm:^2.0.0" + glob: "npm:^13.0.0" + hermes-parser: "npm:^0.29.1" + jsc-safe-url: "npm:^0.2.4" + lightningcss: "npm:^1.30.1" + minimatch: "npm:^9.0.0" + postcss: "npm:~8.4.32" + resolve-from: "npm:^5.0.0" + peerDependencies: + expo: "*" + peerDependenciesMeta: + expo: + optional: true + checksum: 10/c1a67c187fcd9f3dd43cd1b33a500644715768ab55939d5e2ff354311709ea5fed2bb3c103610b0ddac961d7ab2f94f7a1d1f25d033af98690ed6b9cec9ac787 + languageName: node + linkType: hard + "@expo/metro-runtime@npm:^6.1.2": version: 6.1.2 resolution: "@expo/metro-runtime@npm:6.1.2" @@ -2638,6 +2808,28 @@ __metadata: languageName: node linkType: hard +"@expo/metro@npm:~54.2.0": + version: 54.2.0 + resolution: "@expo/metro@npm:54.2.0" + dependencies: + metro: "npm:0.83.3" + metro-babel-transformer: "npm:0.83.3" + metro-cache: "npm:0.83.3" + metro-cache-key: "npm:0.83.3" + metro-config: "npm:0.83.3" + metro-core: "npm:0.83.3" + metro-file-map: "npm:0.83.3" + metro-minify-terser: "npm:0.83.3" + metro-resolver: "npm:0.83.3" + metro-runtime: "npm:0.83.3" + metro-source-map: "npm:0.83.3" + metro-symbolicate: "npm:0.83.3" + metro-transform-plugins: "npm:0.83.3" + metro-transform-worker: "npm:0.83.3" + checksum: 10/36087cec4cb1788f6c8f6148f9dcd30e8d3693fbf8a14f8b0a3c9575895bd6b1847690c958181d7e92718d49ab66df285a79d64ff3c13e4168bbfee26b670d7f + languageName: node + linkType: hard + "@expo/osascript@npm:^2.3.8": version: 2.3.8 resolution: "@expo/osascript@npm:2.3.8" @@ -2648,6 +2840,20 @@ __metadata: languageName: node linkType: hard +"@expo/package-manager@npm:^1.9.10": + version: 1.10.0 + resolution: "@expo/package-manager@npm:1.10.0" + dependencies: + "@expo/json-file": "npm:^10.0.9" + "@expo/spawn-async": "npm:^1.7.2" + chalk: "npm:^4.0.0" + npm-package-arg: "npm:^11.0.0" + ora: "npm:^3.4.0" + resolve-workspace-root: "npm:^2.0.0" + checksum: 10/61dc892764f886fc9bf4b957fbc8b686fc941f2ceb008428752070f61cd44a40bd4d48bef594b3169eacb389d9e44f86c6b0003da8ef1c8ab3948dfd88875328 + languageName: node + linkType: hard + "@expo/package-manager@npm:^1.9.9": version: 1.9.9 resolution: "@expo/package-manager@npm:1.9.9" @@ -2693,6 +2899,26 @@ __metadata: languageName: node linkType: hard +"@expo/prebuild-config@npm:^54.0.8": + version: 54.0.8 + resolution: "@expo/prebuild-config@npm:54.0.8" + dependencies: + "@expo/config": "npm:~12.0.13" + "@expo/config-plugins": "npm:~54.0.4" + "@expo/config-types": "npm:^54.0.10" + "@expo/image-utils": "npm:^0.8.8" + "@expo/json-file": "npm:^10.0.8" + "@react-native/normalize-colors": "npm:0.81.5" + debug: "npm:^4.3.1" + resolve-from: "npm:^5.0.0" + semver: "npm:^7.6.0" + xml2js: "npm:0.6.0" + peerDependencies: + expo: "*" + checksum: 10/67f0fd1ad9332ff10c554e4b31602656daf222f2c51cebde9c024cb47b7ea13653ee1b01a00b6ea7cdf8fe8c99e20955788de9dec578c394e6b2357ef5919ab9 + languageName: node + linkType: hard + "@expo/schema-utils@npm:^0.1.8": version: 0.1.8 resolution: "@expo/schema-utils@npm:0.1.8" @@ -3162,6 +3388,15 @@ __metadata: languageName: node linkType: hard +"@kesha-antonov/react-native-background-downloader@npm:^4.4.5": + version: 4.4.5 + resolution: "@kesha-antonov/react-native-background-downloader@npm:4.4.5" + peerDependencies: + react-native: ">=0.57.0" + checksum: 10/56c90b08f5efdbc017a373dd3358470b5879f1ccb28b9a25df6b725fb6cc534d7d60c989167e07774f4c8bca530b22ed383322f5f105f8bd58bac2d85914c548 + languageName: node + linkType: hard + "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1": version: 5.1.1-v1 resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1" @@ -3805,15 +4040,6 @@ __metadata: languageName: node linkType: hard -"@react-native/babel-plugin-codegen@npm:0.76.9": - version: 0.76.9 - resolution: "@react-native/babel-plugin-codegen@npm:0.76.9" - dependencies: - "@react-native/codegen": "npm:0.76.9" - checksum: 10/f70b341954c8a83de7c9ee0d261f55b326204d2c02ff7680e091a999fa9137b654aa8fe13769ab76daac5d12b47532833cf49b9bdc7a00011d260c8871b5b4cf - languageName: node - linkType: hard - "@react-native/babel-plugin-codegen@npm:0.81.5": version: 0.81.5 resolution: "@react-native/babel-plugin-codegen@npm:0.81.5" @@ -3824,61 +4050,6 @@ __metadata: languageName: node linkType: hard -"@react-native/babel-preset@npm:0.76.9": - version: 0.76.9 - resolution: "@react-native/babel-preset@npm:0.76.9" - dependencies: - "@babel/core": "npm:^7.25.2" - "@babel/plugin-proposal-export-default-from": "npm:^7.24.7" - "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" - "@babel/plugin-syntax-export-default-from": "npm:^7.24.7" - "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" - "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" - "@babel/plugin-transform-arrow-functions": "npm:^7.24.7" - "@babel/plugin-transform-async-generator-functions": "npm:^7.25.4" - "@babel/plugin-transform-async-to-generator": "npm:^7.24.7" - "@babel/plugin-transform-block-scoping": "npm:^7.25.0" - "@babel/plugin-transform-class-properties": "npm:^7.25.4" - "@babel/plugin-transform-classes": "npm:^7.25.4" - "@babel/plugin-transform-computed-properties": "npm:^7.24.7" - "@babel/plugin-transform-destructuring": "npm:^7.24.8" - "@babel/plugin-transform-flow-strip-types": "npm:^7.25.2" - "@babel/plugin-transform-for-of": "npm:^7.24.7" - "@babel/plugin-transform-function-name": "npm:^7.25.1" - "@babel/plugin-transform-literals": "npm:^7.25.2" - "@babel/plugin-transform-logical-assignment-operators": "npm:^7.24.7" - "@babel/plugin-transform-modules-commonjs": "npm:^7.24.8" - "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.24.7" - "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.24.7" - "@babel/plugin-transform-numeric-separator": "npm:^7.24.7" - "@babel/plugin-transform-object-rest-spread": "npm:^7.24.7" - "@babel/plugin-transform-optional-catch-binding": "npm:^7.24.7" - "@babel/plugin-transform-optional-chaining": "npm:^7.24.8" - "@babel/plugin-transform-parameters": "npm:^7.24.7" - "@babel/plugin-transform-private-methods": "npm:^7.24.7" - "@babel/plugin-transform-private-property-in-object": "npm:^7.24.7" - "@babel/plugin-transform-react-display-name": "npm:^7.24.7" - "@babel/plugin-transform-react-jsx": "npm:^7.25.2" - "@babel/plugin-transform-react-jsx-self": "npm:^7.24.7" - "@babel/plugin-transform-react-jsx-source": "npm:^7.24.7" - "@babel/plugin-transform-regenerator": "npm:^7.24.7" - "@babel/plugin-transform-runtime": "npm:^7.24.7" - "@babel/plugin-transform-shorthand-properties": "npm:^7.24.7" - "@babel/plugin-transform-spread": "npm:^7.24.7" - "@babel/plugin-transform-sticky-regex": "npm:^7.24.7" - "@babel/plugin-transform-typescript": "npm:^7.25.2" - "@babel/plugin-transform-unicode-regex": "npm:^7.24.7" - "@babel/template": "npm:^7.25.0" - "@react-native/babel-plugin-codegen": "npm:0.76.9" - babel-plugin-syntax-hermes-parser: "npm:^0.25.1" - babel-plugin-transform-flow-enums: "npm:^0.0.2" - react-refresh: "npm:^0.14.0" - peerDependencies: - "@babel/core": "*" - checksum: 10/3f4810482ea40b0f48add41320e440daabcae1c62ab9c344d0d426b81b2196a6c9b02882b594cfeb039e398fc238980c35f73c4b0182bfd15298de0faed13f0f - languageName: node - linkType: hard - "@react-native/babel-preset@npm:0.81.5": version: 0.81.5 resolution: "@react-native/babel-preset@npm:0.81.5" @@ -3934,24 +4105,6 @@ __metadata: languageName: node linkType: hard -"@react-native/codegen@npm:0.76.9": - version: 0.76.9 - resolution: "@react-native/codegen@npm:0.76.9" - dependencies: - "@babel/parser": "npm:^7.25.3" - glob: "npm:^7.1.1" - hermes-parser: "npm:0.23.1" - invariant: "npm:^2.2.4" - jscodeshift: "npm:^0.14.0" - mkdirp: "npm:^0.5.1" - nullthrows: "npm:^1.1.1" - yargs: "npm:^17.6.2" - peerDependencies: - "@babel/preset-env": ^7.1.6 - checksum: 10/4a4c97f8d7569fb1917e2dad71b4be66558be5d47993d666d14e886e65f19c7b1ebfd5d2205d382e7c3724b2d58bcbc8e23e5a64cb2a281d8869e0419153bab5 - languageName: node - linkType: hard - "@react-native/codegen@npm:0.81.5": version: 0.81.5 resolution: "@react-native/codegen@npm:0.81.5" @@ -4055,13 +4208,6 @@ __metadata: languageName: node linkType: hard -"@react-native/js-polyfills@npm:0.76.9": - version: 0.76.9 - resolution: "@react-native/js-polyfills@npm:0.76.9" - checksum: 10/3e1b64b9143a5ad69d7d56537792b1adba4f3b94aaf04f8a67f5ff3b851fd85df8d1cd79c9247a1811072dff93958d9142c7d41f125e7806138a03d6da105b03 - languageName: node - linkType: hard - "@react-native/js-polyfills@npm:0.81.5": version: 0.81.5 resolution: "@react-native/js-polyfills@npm:0.81.5" @@ -4069,29 +4215,29 @@ __metadata: languageName: node linkType: hard -"@react-native/metro-babel-transformer@npm:0.76.9": - version: 0.76.9 - resolution: "@react-native/metro-babel-transformer@npm:0.76.9" +"@react-native/metro-babel-transformer@npm:0.81.5": + version: 0.81.5 + resolution: "@react-native/metro-babel-transformer@npm:0.81.5" dependencies: "@babel/core": "npm:^7.25.2" - "@react-native/babel-preset": "npm:0.76.9" - hermes-parser: "npm:0.23.1" + "@react-native/babel-preset": "npm:0.81.5" + hermes-parser: "npm:0.29.1" nullthrows: "npm:^1.1.1" peerDependencies: "@babel/core": "*" - checksum: 10/c9cd4100142b634ecc61981bde71cbd044e4f5fa5f459284ed141599d46be4d43d1520319265ee02a4d474a98de6cc7f60a2ec4597e2b1bc76579c8a11d3619e + checksum: 10/401cd5e396a0c04865164c8321c29c17b9cdfbfef5efdf771befb77f830fd28c0bafe116f6d51930e684372f37b4a47f143a404341780187ae9e9fab0da39af4 languageName: node linkType: hard -"@react-native/metro-config@npm:^0.76.3": - version: 0.76.9 - resolution: "@react-native/metro-config@npm:0.76.9" +"@react-native/metro-config@npm:^0.81.5": + version: 0.81.5 + resolution: "@react-native/metro-config@npm:0.81.5" dependencies: - "@react-native/js-polyfills": "npm:0.76.9" - "@react-native/metro-babel-transformer": "npm:0.76.9" - metro-config: "npm:^0.81.0" - metro-runtime: "npm:^0.81.0" - checksum: 10/c52dd64967e6ead75d735702def2e29767f56321d888eae48b683e65118852c567c066755fa0f18c554773a8a0cb44493b436f516bf2c96bb6625f86e7439fec + "@react-native/js-polyfills": "npm:0.81.5" + "@react-native/metro-babel-transformer": "npm:0.81.5" + metro-config: "npm:^0.83.1" + metro-runtime: "npm:^0.83.1" + checksum: 10/13af9cb8f743e8ae51fe0c77db4c61070ef31074b985911ad03b53ec79985f3ba261f1b0026bc62b1b070a3954c8928b73d2d956fc13bad6ece3699b3f5d7254 languageName: node linkType: hard @@ -4237,6 +4383,46 @@ __metadata: languageName: node linkType: hard +"@rn-executorch/bare-adapter@workspace:packages/bare-adapter": + version: 0.0.0-use.local + resolution: "@rn-executorch/bare-adapter@workspace:packages/bare-adapter" + dependencies: + "@dr.pogodin/react-native-fs": "npm:^2.36.2" + "@kesha-antonov/react-native-background-downloader": "npm:^4.4.5" + "@types/react": "npm:~19.1.10" + react: "npm:19.1.0" + react-native: "npm:0.81.5" + react-native-executorch: "workspace:*" + typescript: "npm:~5.9.2" + peerDependencies: + "@dr.pogodin/react-native-fs": ^2.0.0 + "@kesha-antonov/react-native-background-downloader": ^4.0.0 + react-native: "*" + react-native-executorch: "*" + languageName: unknown + linkType: soft + +"@rn-executorch/expo-adapter@workspace:*, @rn-executorch/expo-adapter@workspace:packages/expo-adapter": + version: 0.0.0-use.local + resolution: "@rn-executorch/expo-adapter@workspace:packages/expo-adapter" + dependencies: + "@types/react": "npm:~19.1.10" + expo: "npm:^54.0.0" + expo-asset: "npm:12.0.11" + expo-file-system: "npm:^19.0.20" + react: "npm:19.1.0" + react-native: "npm:0.81.5" + react-native-executorch: "workspace:*" + typescript: "npm:~5.9.2" + peerDependencies: + expo: ">=54.0.0" + expo-asset: ^12.0.0 + expo-file-system: ^19.0.0 + react-native: "*" + react-native-executorch: "*" + languageName: unknown + linkType: soft + "@shopify/react-native-skia@npm:2.2.12": version: 2.2.12 resolution: "@shopify/react-native-skia@npm:2.2.12" @@ -5230,15 +5416,6 @@ __metadata: languageName: node linkType: hard -"ast-types@npm:0.15.2": - version: 0.15.2 - resolution: "ast-types@npm:0.15.2" - dependencies: - tslib: "npm:^2.0.1" - checksum: 10/81680bd5829cdec33524e9aa3434e23f3919c0c388927068a0ff2e8466f55b0f34eae53e0007b3668742910c289481ab4e1d486a5318f618ae2fc93b5e7e863b - languageName: node - linkType: hard - "astral-regex@npm:^1.0.0": version: 1.0.0 resolution: "astral-regex@npm:1.0.0" @@ -5272,16 +5449,7 @@ __metadata: resolution: "available-typed-arrays@npm:1.0.7" dependencies: possible-typed-array-names: "npm:^1.0.0" - checksum: 10/6c9da3a66caddd83c875010a1ca8ef11eac02ba15fb592dc9418b2b5e7b77b645fa7729380a92d9835c2f05f2ca1b6251f39b993e0feb3f1517c74fa1af02cab - languageName: node - linkType: hard - -"babel-core@npm:^7.0.0-bridge.0": - version: 7.0.0-bridge.0 - resolution: "babel-core@npm:7.0.0-bridge.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10/2a1cb879019dffb08d17bec36e13c3a6d74c94773f41c1fd8b14de13f149cc34b705b0a1e07b42fcf35917b49d78db6ff0c5c3b00b202a5235013d517b5c6bbb + checksum: 10/6c9da3a66caddd83c875010a1ca8ef11eac02ba15fb592dc9418b2b5e7b77b645fa7729380a92d9835c2f05f2ca1b6251f39b993e0feb3f1517c74fa1af02cab languageName: node linkType: hard @@ -5388,15 +5556,6 @@ __metadata: languageName: node linkType: hard -"babel-plugin-syntax-hermes-parser@npm:^0.25.1": - version: 0.25.1 - resolution: "babel-plugin-syntax-hermes-parser@npm:0.25.1" - dependencies: - hermes-parser: "npm:0.25.1" - checksum: 10/dc80fafde1aed8e60cf86ecd2e9920e7f35ffe02b33bd4e772daaa786167bcf508aac3fc1aea425ff4c7a0be94d82528f3fe8619b7f41dac853264272d640c04 - languageName: node - linkType: hard - "babel-plugin-syntax-hermes-parser@npm:^0.28.0": version: 0.28.1 resolution: "babel-plugin-syntax-hermes-parser@npm:0.28.1" @@ -5440,6 +5599,45 @@ __metadata: languageName: node linkType: hard +"babel-preset-expo@npm:~54.0.10": + version: 54.0.10 + resolution: "babel-preset-expo@npm:54.0.10" + dependencies: + "@babel/helper-module-imports": "npm:^7.25.9" + "@babel/plugin-proposal-decorators": "npm:^7.12.9" + "@babel/plugin-proposal-export-default-from": "npm:^7.24.7" + "@babel/plugin-syntax-export-default-from": "npm:^7.24.7" + "@babel/plugin-transform-class-static-block": "npm:^7.27.1" + "@babel/plugin-transform-export-namespace-from": "npm:^7.25.9" + "@babel/plugin-transform-flow-strip-types": "npm:^7.25.2" + "@babel/plugin-transform-modules-commonjs": "npm:^7.24.8" + "@babel/plugin-transform-object-rest-spread": "npm:^7.24.7" + "@babel/plugin-transform-parameters": "npm:^7.24.7" + "@babel/plugin-transform-private-methods": "npm:^7.24.7" + "@babel/plugin-transform-private-property-in-object": "npm:^7.24.7" + "@babel/plugin-transform-runtime": "npm:^7.24.7" + "@babel/preset-react": "npm:^7.22.15" + "@babel/preset-typescript": "npm:^7.23.0" + "@react-native/babel-preset": "npm:0.81.5" + babel-plugin-react-compiler: "npm:^1.0.0" + babel-plugin-react-native-web: "npm:~0.21.0" + babel-plugin-syntax-hermes-parser: "npm:^0.29.1" + babel-plugin-transform-flow-enums: "npm:^0.0.2" + debug: "npm:^4.3.4" + resolve-from: "npm:^5.0.0" + peerDependencies: + "@babel/runtime": ^7.20.0 + expo: "*" + react-refresh: ">=0.14.0 <1.0.0" + peerDependenciesMeta: + "@babel/runtime": + optional: true + expo: + optional: true + checksum: 10/210493e87fb2566fbf774a2bf20a0cfd552eb83f7d3fb71aa4b576ebeed6d367a1d7eda64cec8d166859efde6594789946676bae0d26176a45e4be9fac2fd6a4 + languageName: node + linkType: hard + "babel-preset-expo@npm:~54.0.8": version: 54.0.8 resolution: "babel-preset-expo@npm:54.0.8" @@ -6007,17 +6205,6 @@ __metadata: languageName: node linkType: hard -"clone-deep@npm:^4.0.1": - version: 4.0.1 - resolution: "clone-deep@npm:4.0.1" - dependencies: - is-plain-object: "npm:^2.0.4" - kind-of: "npm:^6.0.2" - shallow-clone: "npm:^3.0.0" - checksum: 10/770f912fe4e6f21873c8e8fbb1e99134db3b93da32df271d00589ea4a29dbe83a9808a322c93f3bcaf8584b8b4fa6fc269fc8032efbaa6728e0c9886c74467d2 - languageName: node - linkType: hard - "clone@npm:^1.0.2": version: 1.0.4 resolution: "clone@npm:1.0.4" @@ -6165,13 +6352,6 @@ __metadata: languageName: node linkType: hard -"commondir@npm:^1.0.1": - version: 1.0.1 - resolution: "commondir@npm:1.0.1" - checksum: 10/4620bc4936a4ef12ce7dfcd272bb23a99f2ad68889a4e4ad766c9f8ad21af982511934d6f7050d4a8bde90011b1c15d56e61a1b4576d9913efbf697a20172d6c - languageName: node - linkType: hard - "compressible@npm:~2.0.18": version: 2.0.18 resolution: "compressible@npm:2.0.18" @@ -6201,9 +6381,10 @@ __metadata: resolution: "computer-vision@workspace:apps/computer-vision" dependencies: "@babel/core": "npm:^7.25.2" - "@react-native/metro-config": "npm:^0.76.3" + "@react-native/metro-config": "npm:^0.81.5" "@react-navigation/drawer": "npm:^7.3.9" "@react-navigation/native": "npm:^7.1.6" + "@rn-executorch/expo-adapter": "workspace:*" "@shopify/react-native-skia": "npm:2.2.12" "@types/pngjs": "npm:^6.0.5" "@types/react": "npm:~19.1.10" @@ -6213,7 +6394,7 @@ __metadata: expo-linking: "npm:~8.0.10" expo-router: "npm:~6.0.17" expo-status-bar: "npm:~3.0.9" - metro-config: "npm:^0.81.0" + metro-config: "npm:^0.81.5" react: "npm:19.1.0" react-native: "npm:0.81.5" react-native-device-info: "npm:^14.0.4" @@ -7506,7 +7687,7 @@ __metadata: languageName: node linkType: hard -"esprima@npm:^4.0.0, esprima@npm:^4.0.1, esprima@npm:~4.0.0": +"esprima@npm:^4.0.0, esprima@npm:^4.0.1": version: 4.0.1 resolution: "esprima@npm:4.0.1" bin: @@ -7644,6 +7825,20 @@ __metadata: languageName: node linkType: hard +"expo-asset@npm:~12.0.12": + version: 12.0.12 + resolution: "expo-asset@npm:12.0.12" + dependencies: + "@expo/image-utils": "npm:^0.8.8" + expo-constants: "npm:~18.0.12" + peerDependencies: + expo: "*" + react: "*" + react-native: "*" + checksum: 10/7034316d820837c92ac70274be56a8e59181f1513805f8a4c85e16f12e1dd75ac6d6ae0b231bd8a76adbb71be6163c05b31b1d437f15b14745c70cc1f255c8a1 + languageName: node + linkType: hard + "expo-brightness@npm:~14.0.8": version: 14.0.8 resolution: "expo-brightness@npm:14.0.8" @@ -7677,7 +7872,30 @@ __metadata: languageName: node linkType: hard -"expo-file-system@npm:^19.0.20, expo-file-system@npm:~19.0.20": +"expo-constants@npm:~18.0.12, expo-constants@npm:~18.0.13": + version: 18.0.13 + resolution: "expo-constants@npm:18.0.13" + dependencies: + "@expo/config": "npm:~12.0.13" + "@expo/env": "npm:~2.0.8" + peerDependencies: + expo: "*" + react-native: "*" + checksum: 10/f29c72b6f5798bd37550aafcc89c3f1a630c4910a5b69c1e19d03544f6ebf0cb65adf39db600ccbeb6e60545b2b231d244373ef3139e3c75991b380940065c6b + languageName: node + linkType: hard + +"expo-file-system@npm:^19.0.20, expo-file-system@npm:~19.0.21": + version: 19.0.21 + resolution: "expo-file-system@npm:19.0.21" + peerDependencies: + expo: "*" + react-native: "*" + checksum: 10/00a2f13f8139724016f8b811303dd4a4070a315f80ee9e1877bcfd00773b38caafe4f1d3d7d4a87777e4ff53ba645aae0b4430e875f9ee5f277b88372b507811 + languageName: node + linkType: hard + +"expo-file-system@npm:~19.0.20": version: 19.0.20 resolution: "expo-file-system@npm:19.0.20" peerDependencies: @@ -7700,6 +7918,19 @@ __metadata: languageName: node linkType: hard +"expo-font@npm:~14.0.11": + version: 14.0.11 + resolution: "expo-font@npm:14.0.11" + dependencies: + fontfaceobserver: "npm:^2.1.0" + peerDependencies: + expo: "*" + react: "*" + react-native: "*" + checksum: 10/80acffecdbd49a2ba1d7ecd8727f355bf47c39873d92f5959ff3bf7fd1de3e6ac10ebe2a77b8238287c3f2b7d033df40b562505fec370f82d9444400e19d7518 + languageName: node + linkType: hard + "expo-keep-awake@npm:~15.0.8": version: 15.0.8 resolution: "expo-keep-awake@npm:15.0.8" @@ -7738,6 +7969,21 @@ __metadata: languageName: node linkType: hard +"expo-modules-autolinking@npm:3.0.24": + version: 3.0.24 + resolution: "expo-modules-autolinking@npm:3.0.24" + dependencies: + "@expo/spawn-async": "npm:^1.7.2" + chalk: "npm:^4.1.0" + commander: "npm:^7.2.0" + require-from-string: "npm:^2.0.2" + resolve-from: "npm:^5.0.0" + bin: + expo-modules-autolinking: bin/expo-modules-autolinking.js + checksum: 10/e3b77d2fa84b77e53dca2ef608b48c4db196957c76ac7cc1aba4eb2cca44b5082a16f7af8a3549a342c7a1362f069a76fb9ebdab4be6b467e3791ad48387e15a + languageName: node + linkType: hard + "expo-modules-core@npm:3.0.28": version: 3.0.28 resolution: "expo-modules-core@npm:3.0.28" @@ -7750,6 +7996,18 @@ __metadata: languageName: node linkType: hard +"expo-modules-core@npm:3.0.29": + version: 3.0.29 + resolution: "expo-modules-core@npm:3.0.29" + dependencies: + invariant: "npm:^2.2.4" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/db23a1c7321db54f40f0bcb9c18e7239d798fb7fb5d8ceedf09879f7ff4d90a85e375851796008006441326ed61c00ba00950b06bc7ea74f6ba648a9dac9d053 + languageName: node + linkType: hard + "expo-router@npm:~6.0.17": version: 6.0.17 resolution: "expo-router@npm:6.0.17" @@ -7831,7 +8089,53 @@ __metadata: languageName: node linkType: hard -"expo@npm:^54.0.0, expo@npm:^54.0.27": +"expo@npm:^54.0.0": + version: 54.0.32 + resolution: "expo@npm:54.0.32" + dependencies: + "@babel/runtime": "npm:^7.20.0" + "@expo/cli": "npm:54.0.22" + "@expo/config": "npm:~12.0.13" + "@expo/config-plugins": "npm:~54.0.4" + "@expo/devtools": "npm:0.1.8" + "@expo/fingerprint": "npm:0.15.4" + "@expo/metro": "npm:~54.2.0" + "@expo/metro-config": "npm:54.0.14" + "@expo/vector-icons": "npm:^15.0.3" + "@ungap/structured-clone": "npm:^1.3.0" + babel-preset-expo: "npm:~54.0.10" + expo-asset: "npm:~12.0.12" + expo-constants: "npm:~18.0.13" + expo-file-system: "npm:~19.0.21" + expo-font: "npm:~14.0.11" + expo-keep-awake: "npm:~15.0.8" + expo-modules-autolinking: "npm:3.0.24" + expo-modules-core: "npm:3.0.29" + pretty-format: "npm:^29.7.0" + react-refresh: "npm:^0.14.2" + whatwg-url-without-unicode: "npm:8.0.0-3" + peerDependencies: + "@expo/dom-webview": "*" + "@expo/metro-runtime": "*" + react: "*" + react-native: "*" + react-native-webview: "*" + peerDependenciesMeta: + "@expo/dom-webview": + optional: true + "@expo/metro-runtime": + optional: true + react-native-webview: + optional: true + bin: + expo: bin/cli + expo-modules-autolinking: bin/autolinking + fingerprint: bin/fingerprint + checksum: 10/78c9b88b98bcf424c00ffdd399b77e6fbf34f2c26a3d420e731bfccd9cdfb54e95f27b539d071155c4144ae7157e2700f9d22d4b2816974b20f8fcf0675bbb05 + languageName: node + linkType: hard + +"expo@npm:^54.0.27": version: 54.0.27 resolution: "expo@npm:54.0.27" dependencies: @@ -8022,26 +8326,6 @@ __metadata: languageName: node linkType: hard -"find-cache-dir@npm:^2.0.0": - version: 2.1.0 - resolution: "find-cache-dir@npm:2.1.0" - dependencies: - commondir: "npm:^1.0.1" - make-dir: "npm:^2.0.0" - pkg-dir: "npm:^3.0.0" - checksum: 10/60ad475a6da9f257df4e81900f78986ab367d4f65d33cf802c5b91e969c28a8762f098693d7a571b6e4dd4c15166c2da32ae2d18b6766a18e2071079448fdce4 - languageName: node - linkType: hard - -"find-up@npm:^3.0.0": - version: 3.0.0 - resolution: "find-up@npm:3.0.0" - dependencies: - locate-path: "npm:^3.0.0" - checksum: 10/38eba3fe7a66e4bc7f0f5a1366dc25508b7cfc349f852640e3678d26ad9a6d7e2c43eff0a472287de4a9753ef58f066a0ea892a256fa3636ad51b3fe1e17fae9 - languageName: node - linkType: hard - "find-up@npm:^4.0.0, find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" @@ -8097,13 +8381,6 @@ __metadata: languageName: node linkType: hard -"flow-parser@npm:0.*": - version: 0.292.0 - resolution: "flow-parser@npm:0.292.0" - checksum: 10/d5f9de995cdf6035bff4086a590b93ebcf94d759d94738ec83e7a98716553009caaa998bf9c19a97e2ed297c6ebcc4322060f6cff32937b2bcdff07f1ca90545 - languageName: node - linkType: hard - "fontfaceobserver@npm:^2.1.0": version: 2.3.0 resolution: "fontfaceobserver@npm:2.3.0" @@ -8453,7 +8730,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.3, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.3, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 @@ -8531,13 +8808,6 @@ __metadata: languageName: node linkType: hard -"hermes-estree@npm:0.23.1": - version: 0.23.1 - resolution: "hermes-estree@npm:0.23.1" - checksum: 10/b7ad78f53044d53ec1c77e93036c16e34f6f0985c895540876301e4791d4db08da828870977140f5cf1ae34532bbb9d9d013a0a1a4a5a0da05177225648d5295 - languageName: node - linkType: hard - "hermes-estree@npm:0.25.1": version: 0.25.1 resolution: "hermes-estree@npm:0.25.1" @@ -8566,15 +8836,6 @@ __metadata: languageName: node linkType: hard -"hermes-parser@npm:0.23.1": - version: 0.23.1 - resolution: "hermes-parser@npm:0.23.1" - dependencies: - hermes-estree: "npm:0.23.1" - checksum: 10/de88df4f23bd8dc2ffa89c8a317445320af8c7705a2aeeb05c4dd171f037a747982be153a0a237b1c9c7337b79bceaeb5052934cb8a25fe2e2473294a5343334 - languageName: node - linkType: hard - "hermes-parser@npm:0.25.1": version: 0.25.1 resolution: "hermes-parser@npm:0.25.1" @@ -8679,6 +8940,13 @@ __metadata: languageName: node linkType: hard +"http-status-codes@npm:^2.3.0": + version: 2.3.0 + resolution: "http-status-codes@npm:2.3.0" + checksum: 10/1b8a01940b5e14d3c5b2f842313f4531469b41ce4fa40ca3aae5c82a3101828db2cc9406bfb2d50a46e6d521d106577b6656c2b065c76125b99ee54b2cbbac09 + languageName: node + linkType: hard + "https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.5": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" @@ -9139,15 +9407,6 @@ __metadata: languageName: node linkType: hard -"is-plain-object@npm:^2.0.4": - version: 2.0.4 - resolution: "is-plain-object@npm:2.0.4" - dependencies: - isobject: "npm:^3.0.1" - checksum: 10/2a401140cfd86cabe25214956ae2cfee6fbd8186809555cd0e84574f88de7b17abacb2e477a6a658fa54c6083ecbda1e6ae404c7720244cd198903848fca70ca - languageName: node - linkType: hard - "is-regex@npm:^1.2.1": version: 1.2.1 resolution: "is-regex@npm:1.2.1" @@ -9308,13 +9567,6 @@ __metadata: languageName: node linkType: hard -"isobject@npm:^3.0.1": - version: 3.0.1 - resolution: "isobject@npm:3.0.1" - checksum: 10/db85c4c970ce30693676487cca0e61da2ca34e8d4967c2e1309143ff910c207133a969f9e4ddb2dc6aba670aabce4e0e307146c310350b298e74a31f7d464703 - languageName: node - linkType: hard - "istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": version: 3.2.2 resolution: "istanbul-lib-coverage@npm:3.2.2" @@ -9903,37 +10155,6 @@ __metadata: languageName: node linkType: hard -"jscodeshift@npm:^0.14.0": - version: 0.14.0 - resolution: "jscodeshift@npm:0.14.0" - dependencies: - "@babel/core": "npm:^7.13.16" - "@babel/parser": "npm:^7.13.16" - "@babel/plugin-proposal-class-properties": "npm:^7.13.0" - "@babel/plugin-proposal-nullish-coalescing-operator": "npm:^7.13.8" - "@babel/plugin-proposal-optional-chaining": "npm:^7.13.12" - "@babel/plugin-transform-modules-commonjs": "npm:^7.13.8" - "@babel/preset-flow": "npm:^7.13.13" - "@babel/preset-typescript": "npm:^7.13.0" - "@babel/register": "npm:^7.13.16" - babel-core: "npm:^7.0.0-bridge.0" - chalk: "npm:^4.1.2" - flow-parser: "npm:0.*" - graceful-fs: "npm:^4.2.4" - micromatch: "npm:^4.0.4" - neo-async: "npm:^2.5.0" - node-dir: "npm:^0.1.17" - recast: "npm:^0.21.0" - temp: "npm:^0.8.4" - write-file-atomic: "npm:^2.3.0" - peerDependencies: - "@babel/preset-env": ^7.1.6 - bin: - jscodeshift: bin/jscodeshift.js - checksum: 10/fc355dde2287c026a682e8b38df5d8d1ff5c9ca044dfd558f2b6d17bb28f9257063bd0e47690814612e572804caa5383733c9d8ca8bc18e70bcee43e0458df59 - languageName: node - linkType: hard - "jsesc@npm:^3.0.2, jsesc@npm:~3.1.0": version: 3.1.0 resolution: "jsesc@npm:3.1.0" @@ -10049,13 +10270,6 @@ __metadata: languageName: node linkType: hard -"kind-of@npm:^6.0.2": - version: 6.0.3 - resolution: "kind-of@npm:6.0.3" - checksum: 10/5873d303fb36aad875b7538798867da2ae5c9e328d67194b0162a3659a627d22f742fc9c4ae95cd1704132a24b00cae5041fc00c0f6ef937dc17080dc4dbb962 - languageName: node - linkType: hard - "kleur@npm:^3.0.3": version: 3.0.3 resolution: "kleur@npm:3.0.3" @@ -10257,9 +10471,10 @@ __metadata: resolution: "llm@workspace:apps/llm" dependencies: "@babel/core": "npm:^7.25.2" - "@react-native/metro-config": "npm:^0.76.3" + "@react-native/metro-config": "npm:^0.81.5" "@react-navigation/drawer": "npm:^7.3.9" "@react-navigation/native": "npm:^7.1.6" + "@rn-executorch/expo-adapter": "workspace:*" "@types/react": "npm:~19.1.10" expo: "npm:^54.0.27" expo-brightness: "npm:~14.0.8" @@ -10269,7 +10484,7 @@ __metadata: expo-linking: "npm:~8.0.10" expo-router: "npm:~6.0.17" expo-status-bar: "npm:~3.0.9" - metro-config: "npm:^0.81.0" + metro-config: "npm:^0.81.5" react: "npm:19.1.0" react-native: "npm:0.81.5" react-native-audio-api: "npm:^0.8.2" @@ -10287,16 +10502,6 @@ __metadata: languageName: unknown linkType: soft -"locate-path@npm:^3.0.0": - version: 3.0.0 - resolution: "locate-path@npm:3.0.0" - dependencies: - p-locate: "npm:^3.0.0" - path-exists: "npm:^3.0.0" - checksum: 10/53db3996672f21f8b0bf2a2c645ae2c13ffdae1eeecfcd399a583bce8516c0b88dcb4222ca6efbbbeb6949df7e46860895be2c02e8d3219abd373ace3bfb4e11 - languageName: node - linkType: hard - "locate-path@npm:^5.0.0": version: 5.0.0 resolution: "locate-path@npm:5.0.0" @@ -10418,16 +10623,6 @@ __metadata: languageName: node linkType: hard -"make-dir@npm:^2.0.0, make-dir@npm:^2.1.0": - version: 2.1.0 - resolution: "make-dir@npm:2.1.0" - dependencies: - pify: "npm:^4.0.1" - semver: "npm:^5.6.0" - checksum: 10/043548886bfaf1820323c6a2997e6d2fa51ccc2586ac14e6f14634f7458b4db2daf15f8c310e2a0abd3e0cddc64df1890d8fc7263033602c47bb12cbfcf86aab - languageName: node - linkType: hard - "make-dir@npm:^4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" @@ -10697,7 +10892,7 @@ __metadata: languageName: node linkType: hard -"metro-config@npm:0.81.5, metro-config@npm:^0.81.0": +"metro-config@npm:0.81.5, metro-config@npm:^0.81.5": version: 0.81.5 resolution: "metro-config@npm:0.81.5" dependencies: @@ -10935,7 +11130,7 @@ __metadata: languageName: node linkType: hard -"metro-runtime@npm:0.81.5, metro-runtime@npm:^0.81.0": +"metro-runtime@npm:0.81.5": version: 0.81.5 resolution: "metro-runtime@npm:0.81.5" dependencies: @@ -11642,7 +11837,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -11660,7 +11855,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.6": +"minimist@npm:^1.2.0": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 10/908491b6cc15a6c440ba5b22780a0ba89b9810e1aea684e253e43c4e3b8d56ec1dcdd7ea96dde119c29df59c936cde16062159eae4225c691e19c70b432b6e6f @@ -11743,17 +11938,6 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^0.5.1": - version: 0.5.6 - resolution: "mkdirp@npm:0.5.6" - dependencies: - minimist: "npm:^1.2.6" - bin: - mkdirp: bin/cmd.js - checksum: 10/0c91b721bb12c3f9af4b77ebf73604baf350e64d80df91754dc509491ae93bf238581e59c7188360cec7cb62fc4100959245a42cfe01834efedc5e9d068376c2 - languageName: node - linkType: hard - "mkdirp@npm:^1.0.4": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" @@ -11825,13 +12009,6 @@ __metadata: languageName: node linkType: hard -"neo-async@npm:^2.5.0": - version: 2.6.2 - resolution: "neo-async@npm:2.6.2" - checksum: 10/1a7948fea86f2b33ec766bc899c88796a51ba76a4afc9026764aedc6e7cde692a09067031e4a1bf6db4f978ccd99e7f5b6c03fe47ad9865c3d4f99050d67e002 - languageName: node - linkType: hard - "nested-error-stacks@npm:~2.0.1": version: 2.0.1 resolution: "nested-error-stacks@npm:2.0.1" @@ -11856,16 +12033,7 @@ __metadata: languageName: node linkType: hard -"node-dir@npm:^0.1.17": - version: 0.1.17 - resolution: "node-dir@npm:0.1.17" - dependencies: - minimatch: "npm:^3.0.2" - checksum: 10/281fdea12d9c080a7250e5b5afefa3ab39426d40753ec8126a2d1e67f189b8824723abfed74f5d8549c5d78352d8c489fe08d0b067d7684c87c07283d38374a5 - languageName: node - linkType: hard - -"node-forge@npm:^1.2.1, node-forge@npm:^1.3.1": +"node-forge@npm:^1.2.1, node-forge@npm:^1.3.1, node-forge@npm:^1.3.3": version: 1.3.3 resolution: "node-forge@npm:1.3.3" checksum: 10/f41c31b9296771a4b8c955d58417471712f54f324603a35f8e6cbac19d5e6eaaf5fd5fd14584dfedecbf46a05438ded6eee60a5f2f0822fc5061aaa073cfc75d @@ -12204,7 +12372,7 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^2.0.0, p-limit@npm:^2.2.0": +"p-limit@npm:^2.2.0": version: 2.3.0 resolution: "p-limit@npm:2.3.0" dependencies: @@ -12222,15 +12390,6 @@ __metadata: languageName: node linkType: hard -"p-locate@npm:^3.0.0": - version: 3.0.0 - resolution: "p-locate@npm:3.0.0" - dependencies: - p-limit: "npm:^2.0.0" - checksum: 10/83991734a9854a05fe9dbb29f707ea8a0599391f52daac32b86f08e21415e857ffa60f0e120bfe7ce0cc4faf9274a50239c7895fc0d0579d08411e513b83a4ae - languageName: node - linkType: hard - "p-locate@npm:^4.1.0": version: 4.1.0 resolution: "p-locate@npm:4.1.0" @@ -12356,13 +12515,6 @@ __metadata: languageName: node linkType: hard -"path-exists@npm:^3.0.0": - version: 3.0.0 - resolution: "path-exists@npm:3.0.0" - checksum: 10/96e92643aa34b4b28d0de1cd2eba52a1c5313a90c6542d03f62750d82480e20bfa62bc865d5cfc6165f5fcd5aeb0851043c40a39be5989646f223300021bae0a - languageName: node - linkType: hard - "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -12446,29 +12598,13 @@ __metadata: languageName: node linkType: hard -"pify@npm:^4.0.1": - version: 4.0.1 - resolution: "pify@npm:4.0.1" - checksum: 10/8b97cbf9dc6d4c1320cc238a2db0fc67547f9dc77011729ff353faf34f1936ea1a4d7f3c63b2f4980b253be77bcc72ea1e9e76ee3fd53cce2aafb6a8854d07ec - languageName: node - linkType: hard - -"pirates@npm:^4.0.1, pirates@npm:^4.0.4, pirates@npm:^4.0.6": +"pirates@npm:^4.0.1, pirates@npm:^4.0.4": version: 4.0.7 resolution: "pirates@npm:4.0.7" checksum: 10/2427f371366081ae42feb58214f04805d6b41d6b84d74480ebcc9e0ddbd7105a139f7c653daeaf83ad8a1a77214cf07f64178e76de048128fec501eab3305a96 languageName: node linkType: hard -"pkg-dir@npm:^3.0.0": - version: 3.0.0 - resolution: "pkg-dir@npm:3.0.0" - dependencies: - find-up: "npm:^3.0.0" - checksum: 10/70c9476ffefc77552cc6b1880176b71ad70bfac4f367604b2b04efd19337309a4eec985e94823271c7c0e83946fa5aeb18cd360d15d10a5d7533e19344bfa808 - languageName: node - linkType: hard - "pkg-dir@npm:^4.2.0": version: 4.2.0 resolution: "pkg-dir@npm:4.2.0" @@ -12952,9 +13088,6 @@ __metadata: "@react-native-community/cli": "npm:latest" "@types/jest": "npm:^29.5.5" "@types/react": "npm:~19.1.10" - expo: "npm:^54.0.0" - expo-asset: "npm:12.0.11" - expo-file-system: "npm:^19.0.20" jest: "npm:^29.7.0" jsonrepair: "npm:^3.12.0" jsonschema: "npm:^1.5.0" @@ -12965,9 +13098,6 @@ __metadata: typescript: "npm:~5.9.2" zod: "npm:^3.25.0" peerDependencies: - expo: ">=54.0.0" - expo-asset: ^12.0.0 - expo-file-system: ^19.0.0 react: "*" react-native: "*" languageName: unknown @@ -13286,18 +13416,6 @@ __metadata: languageName: node linkType: hard -"recast@npm:^0.21.0": - version: 0.21.5 - resolution: "recast@npm:0.21.5" - dependencies: - ast-types: "npm:0.15.2" - esprima: "npm:~4.0.0" - source-map: "npm:~0.6.1" - tslib: "npm:^2.0.1" - checksum: 10/b41da2bcf7e705511db2f27d17420ace027de8dd167de9f19190d4988a1f80d112f60c095101ac2f145c8657ddde0c5133eb71df20504efaf3fd9d76ad07e15d - languageName: node - linkType: hard - "reflect.getprototypeof@npm:^1.0.6, reflect.getprototypeof@npm:^1.0.9": version: 1.0.10 resolution: "reflect.getprototypeof@npm:1.0.10" @@ -13583,17 +13701,6 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:~2.6.2": - version: 2.6.3 - resolution: "rimraf@npm:2.6.3" - dependencies: - glob: "npm:^7.1.3" - bin: - rimraf: ./bin.js - checksum: 10/756419f2fa99aa119c46a9fc03e09d84ecf5421a80a72d1944c5088c9e4671e77128527a900a313ed9d3fdbdd37e2ae05486cd7e9116d5812d8c31f2399d7c86 - languageName: node - linkType: hard - "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -13681,15 +13788,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:^5.6.0": - version: 5.7.2 - resolution: "semver@npm:5.7.2" - bin: - semver: bin/semver - checksum: 10/fca14418a174d4b4ef1fecb32c5941e3412d52a4d3d85165924ce3a47fbc7073372c26faf7484ceb4bbc2bde25880c6b97e492473dc7e9708fdfb1c6a02d546e - languageName: node - linkType: hard - "semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" @@ -13843,15 +13941,6 @@ __metadata: languageName: node linkType: hard -"shallow-clone@npm:^3.0.0": - version: 3.0.1 - resolution: "shallow-clone@npm:3.0.1" - dependencies: - kind-of: "npm:^6.0.2" - checksum: 10/e066bd540cfec5e1b0f78134853e0d892d1c8945fb9a926a579946052e7cb0c70ca4fc34f875a8083aa7910d751805d36ae64af250a6de6f3d28f9fa7be6c21b - languageName: node - linkType: hard - "shallowequal@npm:^1.1.0": version: 1.1.0 resolution: "shallowequal@npm:1.1.0" @@ -14051,7 +14140,7 @@ __metadata: languageName: node linkType: hard -"source-map-support@npm:^0.5.16, source-map-support@npm:~0.5.20, source-map-support@npm:~0.5.21": +"source-map-support@npm:~0.5.20, source-map-support@npm:~0.5.21": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" dependencies: @@ -14068,7 +14157,7 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.1": +"source-map@npm:^0.6.0, source-map@npm:^0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" checksum: 10/59ef7462f1c29d502b3057e822cdbdae0b0e565302c4dd1a95e11e793d8d9d62006cdc10e0fd99163ca33ff2071360cf50ee13f90440806e7ed57d81cba2f7ff @@ -14080,13 +14169,14 @@ __metadata: resolution: "speech@workspace:apps/speech" dependencies: "@babel/core": "npm:^7.25.2" - "@react-native/metro-config": "npm:^0.76.3" + "@react-native/metro-config": "npm:^0.81.5" + "@rn-executorch/expo-adapter": "workspace:*" "@types/react": "npm:~19.1.10" buffer: "npm:^6.0.3" expo: "npm:^54.0.27" expo-font: "npm:~14.0.10" expo-status-bar: "npm:~3.0.9" - metro-config: "npm:^0.81.0" + metro-config: "npm:^0.81.5" react: "npm:19.1.0" react-native: "npm:0.81.5" react-native-audio-api: "npm:0.6.5" @@ -14494,15 +14584,6 @@ __metadata: languageName: node linkType: hard -"temp@npm:^0.8.4": - version: 0.8.4 - resolution: "temp@npm:0.8.4" - dependencies: - rimraf: "npm:~2.6.2" - checksum: 10/0a7f76b49637415bc391c3f6e69377cc4c38afac95132b4158fa711e77b70b082fe56fd886f9d11ffab9d148df181a105a93c8b618fb72266eeaa5e5ddbfe37f - languageName: node - linkType: hard - "terminal-link@npm:^2.1.1": version: 2.1.1 resolution: "terminal-link@npm:2.1.1" @@ -14544,6 +14625,7 @@ __metadata: dependencies: "@babel/core": "npm:^7.25.2" "@react-navigation/drawer": "npm:^7.3.9" + "@rn-executorch/expo-adapter": "workspace:*" "@types/react": "npm:~19.1.10" expo: "npm:^54.0.27" expo-constants: "npm:~18.0.11" @@ -14653,7 +14735,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0": +"tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 @@ -15275,17 +15357,6 @@ __metadata: languageName: node linkType: hard -"write-file-atomic@npm:^2.3.0": - version: 2.4.3 - resolution: "write-file-atomic@npm:2.4.3" - dependencies: - graceful-fs: "npm:^4.1.11" - imurmurhash: "npm:^0.1.4" - signal-exit: "npm:^3.0.2" - checksum: 10/15ce863dce07075d0decedd7c9094f4461e46139d28a758c53162f24c0791c16cd2e7a76baa5b47b1a851fbb51e16f2fab739afb156929b22628f3225437135c - languageName: node - linkType: hard - "write-file-atomic@npm:^4.0.2": version: 4.0.2 resolution: "write-file-atomic@npm:4.0.2"