diff --git a/.cspell-wordlist.txt b/.cspell-wordlist.txt index 2a6a720bd..7428cd147 100644 --- a/.cspell-wordlist.txt +++ b/.cspell-wordlist.txt @@ -108,3 +108,7 @@ sublist TTFT timestamping logprob +RNFS +pogodin +kesha +antonov \ No newline at end of file diff --git a/.cspell.json b/.cspell.json index c1332b118..372de0118 100644 --- a/.cspell.json +++ b/.cspell.json @@ -1,7 +1,7 @@ { "version": "0.2", "language": "en", - "ignorePaths": ["**/node_modules", "**/Pods"], + "ignorePaths": ["**/node_modules", "**/Pods", "**/readmes/**"], "dictionaryDefinitions": [ { "name": "project-words", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7989ebb5d..dd9585bc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: run: yarn lint - name: Typecheck files - run: yarn typecheck + run: yarn workspaces foreach --all --topological-dev run prepare && yarn typecheck build-library: runs-on: ubuntu-latest @@ -35,7 +35,5 @@ jobs: - name: Setup uses: ./.github/actions/setup - - name: Build package - run: | - cd packages/react-native-executorch - yarn prepare + - name: Build all packages + run: yarn workspaces foreach --all --topological-dev run prepare diff --git a/.nvmrc b/.nvmrc index 9a2a0e219..53d1c14db 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20 +v22 diff --git a/README.md b/README.md index 43ffe6d7f..8497adc5d 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,15 @@ React Native ExecuTorch is powering [Private Mind](https://privatemind.swmansion ```bash # Install the package yarn add react-native-executorch + +# If you use expo, please add these packages for resource fetching: +yarn add @react-native-executorch/expo-adapter +yarn add expo-file-system expo-asset + +#if you use bare React Native project use these packages: +yarn add @react-native-executorch/bare-adapter +yarn add @dr.pogodin/react-native-fs @kesha-antonov/react-native-background-downloader + # Depending on the platform, choose either iOS or Android yarn expo run:< ios | android > ``` @@ -88,8 +97,14 @@ Add this to your component file: import { useLLM, LLAMA3_2_1B, - Message + Message, + initExecutorch, } from 'react-native-executorch'; +import { ExpoResourceFetcher } from '@react-native-executorch/expo-resource-fetcher'; + +initExecutorch({ + resourceFetcher: ExpoResourceFetcher, +}); function MyComponent() { // Initialize the model 🚀 diff --git a/apps/computer-vision/app/_layout.tsx b/apps/computer-vision/app/_layout.tsx index 5914d2fe8..35fba7fb1 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 '@react-native-executorch/expo-resource-fetcher'; + 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..cce918197 100644 --- a/apps/computer-vision/package.json +++ b/apps/computer-vision/package.json @@ -11,7 +11,8 @@ "lint": "eslint . --ext .ts,.tsx --fix" }, "dependencies": { - "@react-native/metro-config": "^0.76.3", + "@react-native-executorch/expo-resource-fetcher": "workspace:*", + "@react-native/metro-config": "^0.81.5", "@react-navigation/drawer": "^7.3.9", "@react-navigation/native": "^7.1.6", "@shopify/react-native-skia": "2.2.12", @@ -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..a08f2140a 100644 --- a/apps/computer-vision/tsconfig.json +++ b/apps/computer-vision/tsconfig.json @@ -9,7 +9,10 @@ "customConditions": ["react-native"], "noEmit": true, "paths": { - "react-native-executorch": ["../../packages/react-native-executorch/src"] + "react-native-executorch": ["../../packages/react-native-executorch/src"], + "@react-native-executorch/expo-resource-fetcher": [ + "../../packages/expo-resource-fetcher/src" + ] } } } diff --git a/apps/llm/app/_layout.tsx b/apps/llm/app/_layout.tsx index 68c715a80..5ece80f1f 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 '@react-native-executorch/expo-resource-fetcher'; 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/app/llm/index.tsx b/apps/llm/app/llm/index.tsx index b46e43c13..c159e745c 100644 --- a/apps/llm/app/llm/index.tsx +++ b/apps/llm/app/llm/index.tsx @@ -34,7 +34,7 @@ function LLMScreen() { useEffect(() => { if (llm.error) { - console.log('LLM error:', llm.error); + console.error('LLM error:', llm.error); } }, [llm.error]); diff --git a/apps/llm/app/llm_structured_output/index.tsx b/apps/llm/app/llm_structured_output/index.tsx index 3fa230600..e77900ab2 100644 --- a/apps/llm/app/llm_structured_output/index.tsx +++ b/apps/llm/app/llm_structured_output/index.tsx @@ -119,7 +119,7 @@ function LLMScreen() { useEffect(() => { if (llm.error) { - console.log('LLM error:', llm.error); + console.error('LLM error:', llm.error); } }, [llm.error]); diff --git a/apps/llm/app/llm_tool_calling/index.tsx b/apps/llm/app/llm_tool_calling/index.tsx index 6fbf49f19..191410548 100644 --- a/apps/llm/app/llm_tool_calling/index.tsx +++ b/apps/llm/app/llm_tool_calling/index.tsx @@ -61,7 +61,7 @@ function LLMToolCallingScreen() { useEffect(() => { if (llm.error) { - console.log('LLM error:', llm.error); + console.error('LLM error:', llm.error); } }, [llm.error]); diff --git a/apps/llm/app/voice_chat/index.tsx b/apps/llm/app/voice_chat/index.tsx index 230d0b8e9..abf101b1f 100644 --- a/apps/llm/app/voice_chat/index.tsx +++ b/apps/llm/app/voice_chat/index.tsx @@ -98,6 +98,18 @@ function VoiceChatScreen() { } }; + useEffect(() => { + if (llm.error) { + console.error('LLM error:', llm.error); + } + }, [llm.error]); + + useEffect(() => { + if (speechToText.error) { + console.error('speechToText error:', speechToText.error); + } + }, [speechToText.error]); + return !llm.isReady || !speechToText.isReady ? ( -If you're using bare React Native (instead of a managed Expo project), you also need to install Expo Modules because the underlying implementation relies on expo-file-system. Since expo-file-system is an Expo package, bare React Native projects need **Expo Modules** to properly integrate and use it. The link provided (https://docs.expo.dev/bare/installing-expo-modules/) offers guidance on setting up Expo Modules in a bare React Native environment. +Our library offers support for both bare React Native and Expo projects. Please follow the instructions from [Loading models section](./02-loading-models.md) to make sure you setup your project correctly. We encourage you to use Expo project if possible. If you are planning to migrate from bare React Native to Expo project, the link (https://docs.expo.dev/bare/installing-expo-modules/) offers a guidance on setting up Expo Modules in a bare React Native environment. If you plan on using your models via require() instead of fetching them from a url, you also need to add following lines to your `metro.config.js`: diff --git a/docs/docs/01-fundamentals/02-loading-models.md b/docs/docs/01-fundamentals/02-loading-models.md index 96be9784f..6e84f9666 100644 --- a/docs/docs/01-fundamentals/02-loading-models.md +++ b/docs/docs/01-fundamentals/02-loading-models.md @@ -4,6 +4,46 @@ title: Loading Models There are three different methods available for loading model files, depending on their size and location. +## Prerequisites + +In our library, you can use two different resource fetching mechanisms. One is implemented using Expo FileSystem, the other one uses external library. We encourage you to use implementation utilizing Expo if possible. + +To use the Expo adapter, please add these libraries: + +```bash +yarn add @react-native-executorch/expo-adapter +yarn add expo-file-system expo-asset +``` + +and then add the following code in your React Native app: + +```typescript +import { initExecutorch } from 'react-native-executorch'; +import { ExpoResourceFetcher } from '@react-native-executorch/expo-resource-fetcher'; + +initExecutorch({ + resourceFetcher: ExpoResourceFetcher, +}); +``` + +If you cannot use Expo in your project, proceed with the following steps: + +```bash +yarn add @react-native-executorch/bare-adapter +yarn add @dr.pogodin/react-native-fs @kesha-antonov/react-native-background-downloader +``` + +and + +```typescript +import { initExecutorch } from 'react-native-executorch'; +import { BareResourceFetcher } from '@react-native-executorch/bare-adapter'; + +initExecutorch({ + resourceFetcher: BareResourceFetcher, +}); +``` + **1. Load from React Native assets folder (For Files < 512MB)** ```typescript diff --git a/docs/docs/01-fundamentals/03-frequently-asked-questions.md b/docs/docs/01-fundamentals/03-frequently-asked-questions.md index 9216c615f..69e3792d4 100644 --- a/docs/docs/01-fundamentals/03-frequently-asked-questions.md +++ b/docs/docs/01-fundamentals/03-frequently-asked-questions.md @@ -31,7 +31,7 @@ If your model doesn't support it, you can still work around it using context. Fo ### Can I use React Native ExecuTorch in bare React Native apps? -To use the library, you need to install Expo Modules first. For a setup guide, refer to [this tutorial](https://docs.expo.dev/bare/installing-expo-modules/). This is because we use Expo File System under the hood to download and manage the model binaries. +Yes, staring from version `0.8.x` you can use React Native ExecuTorch in bare React Native apps. You just need to use bare React Native resource fetcher instead of Expo one, see: [Loading models section](./02-loading-models.md) for more details. ### Do you support the old architecture? diff --git a/docs/docs/06-api-reference/classes/LLMModule.md b/docs/docs/06-api-reference/classes/LLMModule.md index c78bd1109..fc5a0b874 100644 --- a/docs/docs/06-api-reference/classes/LLMModule.md +++ b/docs/docs/06-api-reference/classes/LLMModule.md @@ -1,6 +1,6 @@ # Class: LLMModule -Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:10](https://github.com/software-mansion/react-native-executorch/blob/a9d9b826d75623e7b7d41c2da95ed0c60fbb6424/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L10) +Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:10](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L10) Module for managing a Large Language Model (LLM) instance. @@ -10,7 +10,7 @@ Module for managing a Large Language Model (LLM) instance. > **new LLMModule**(`optionalCallbacks`): `LLMModule` -Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:20](https://github.com/software-mansion/react-native-executorch/blob/a9d9b826d75623e7b7d41c2da95ed0c60fbb6424/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L20) +Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:20](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L20) Creates a new instance of `LLMModule` with optional callbacks. @@ -45,7 +45,7 @@ A new LLMModule instance. > **configure**(`config`): `void` -Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:87](https://github.com/software-mansion/react-native-executorch/blob/a9d9b826d75623e7b7d41c2da95ed0c60fbb6424/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L87) +Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:87](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L87) Configures chat and tool calling and generation settings. See [Configuring the model](https://docs.swmansion.com/react-native-executorch/docs/hooks/natural-language-processing/useLLM#configuring-the-model) for details. @@ -68,7 +68,7 @@ Configuration object containing `chatConfig`, `toolsConfig`, and `generationConf > **delete**(): `void` -Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:184](https://github.com/software-mansion/react-native-executorch/blob/a9d9b826d75623e7b7d41c2da95ed0c60fbb6424/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L184) +Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:184](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L184) Method to delete the model from memory. Note you cannot delete model while it's generating. @@ -84,7 +84,7 @@ You need to interrupt it first and make sure model stopped generation. > **deleteMessage**(`index`): [`Message`](../interfaces/Message.md)[] -Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:140](https://github.com/software-mansion/react-native-executorch/blob/a9d9b826d75623e7b7d41c2da95ed0c60fbb6424/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L140) +Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:140](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L140) Deletes all messages starting with message on `index` position. After deletion it will call `messageHistoryCallback()` containing new history. @@ -110,7 +110,7 @@ The index of the message to delete from history. > **forward**(`input`): `Promise`\<`string`\> -Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:104](https://github.com/software-mansion/react-native-executorch/blob/a9d9b826d75623e7b7d41c2da95ed0c60fbb6424/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L104) +Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:104](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L104) Runs model inference with raw input string. You need to provide entire conversation and prompt (in correct format and with special tokens!) in input string to this method. @@ -137,7 +137,7 @@ The generated response as a string. > **generate**(`messages`, `tools?`): `Promise`\<`string`\> -Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:115](https://github.com/software-mansion/react-native-executorch/blob/a9d9b826d75623e7b7d41c2da95ed0c60fbb6424/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L115) +Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:115](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L115) Runs model to complete chat passed in `messages` argument. It doesn't manage conversation context. @@ -167,7 +167,7 @@ The generated response as a string. > **getGeneratedTokenCount**(): `number` -Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:157](https://github.com/software-mansion/react-native-executorch/blob/a9d9b826d75623e7b7d41c2da95ed0c60fbb6424/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L157) +Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:157](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L157) Returns the number of tokens generated in the last response. @@ -183,7 +183,7 @@ The count of generated tokens. > **getPromptTokensCount**(): `number` -Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:166](https://github.com/software-mansion/react-native-executorch/blob/a9d9b826d75623e7b7d41c2da95ed0c60fbb6424/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L166) +Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:166](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L166) Returns the number of prompt tokens in the last message. @@ -199,7 +199,7 @@ The count of prompt token. > **getTotalTokensCount**(): `number` -Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:175](https://github.com/software-mansion/react-native-executorch/blob/a9d9b826d75623e7b7d41c2da95ed0c60fbb6424/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L175) +Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:175](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L175) Returns the number of total tokens from the previous generation. This is a sum of prompt tokens and generated tokens. @@ -215,7 +215,7 @@ The count of prompt and generated tokens. > **interrupt**(): `void` -Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:148](https://github.com/software-mansion/react-native-executorch/blob/a9d9b826d75623e7b7d41c2da95ed0c60fbb6424/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L148) +Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:148](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L148) Interrupts model generation. It may return one more token after interrupt. @@ -229,7 +229,7 @@ Interrupts model generation. It may return one more token after interrupt. > **load**(`model`, `onDownloadProgressCallback`): `Promise`\<`void`\> -Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:49](https://github.com/software-mansion/react-native-executorch/blob/a9d9b826d75623e7b7d41c2da95ed0c60fbb6424/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L49) +Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:49](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L49) Loads the LLM model and tokenizer. @@ -273,7 +273,7 @@ Optional callback to track download progress (value between 0 and 1). > **sendMessage**(`message`): `Promise`\<[`Message`](../interfaces/Message.md)[]\> -Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:127](https://github.com/software-mansion/react-native-executorch/blob/a9d9b826d75623e7b7d41c2da95ed0c60fbb6424/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L127) +Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:127](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L127) Method to add user message to conversation. After model responds it will call `messageHistoryCallback()` containing both user message and model response. @@ -299,7 +299,7 @@ The message string to send. > **setTokenCallback**(`tokenCallback`): `void` -Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:73](https://github.com/software-mansion/react-native-executorch/blob/a9d9b826d75623e7b7d41c2da95ed0c60fbb6424/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L73) +Defined in: [packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts:73](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts#L73) Sets new token callback invoked on every token batch. diff --git a/docs/docs/06-api-reference/classes/Logger.md b/docs/docs/06-api-reference/classes/Logger.md new file mode 100644 index 000000000..f340d6c85 --- /dev/null +++ b/docs/docs/06-api-reference/classes/Logger.md @@ -0,0 +1,105 @@ +# Class: Logger + +Defined in: [packages/react-native-executorch/src/common/Logger.ts:5](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/common/Logger.ts#L5) + +High level wrapper that prefixes `console.` with [React Native ExecuTorch] tag. + +## Constructors + +### Constructor + +> **new Logger**(): `Logger` + +#### Returns + +`Logger` + +## Methods + +### debug() + +> `static` **debug**(...`data`): `void` + +Defined in: [packages/react-native-executorch/src/common/Logger.ts:12](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/common/Logger.ts#L12) + +#### Parameters + +##### data + +...`any`[] + +#### Returns + +`void` + +--- + +### error() + +> `static` **error**(...`data`): `void` + +Defined in: [packages/react-native-executorch/src/common/Logger.ts:24](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/common/Logger.ts#L24) + +#### Parameters + +##### data + +...`any`[] + +#### Returns + +`void` + +--- + +### info() + +> `static` **info**(...`data`): `void` + +Defined in: [packages/react-native-executorch/src/common/Logger.ts:16](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/common/Logger.ts#L16) + +#### Parameters + +##### data + +...`any`[] + +#### Returns + +`void` + +--- + +### log() + +> `static` **log**(...`data`): `void` + +Defined in: [packages/react-native-executorch/src/common/Logger.ts:8](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/common/Logger.ts#L8) + +#### Parameters + +##### data + +...`any`[] + +#### Returns + +`void` + +--- + +### warn() + +> `static` **warn**(...`data`): `void` + +Defined in: [packages/react-native-executorch/src/common/Logger.ts:20](https://github.com/software-mansion/react-native-executorch/blob/a5440c5efceab4377accbd22e9409b019538907f/packages/react-native-executorch/src/common/Logger.ts#L20) + +#### Parameters + +##### data + +...`any`[] + +#### Returns + +`void` diff --git a/docs/docs/06-api-reference/classes/ResourceFetcher.md b/docs/docs/06-api-reference/classes/ResourceFetcher.md index b157d268e..7af39afbb 100644 --- a/docs/docs/06-api-reference/classes/ResourceFetcher.md +++ b/docs/docs/06-api-reference/classes/ResourceFetcher.md @@ -1,6 +1,6 @@ # Class: ResourceFetcher -Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:63](https://github.com/software-mansion/react-native-executorch/blob/326d6344894d75625c600d5988666e215a32d466/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L63) +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:52](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L52) This module provides functions to download and work with downloaded files stored in the application's document directory inside the `react-native-executorch/` directory. These utilities can help you manage your storage and clean up the downloaded files when they are no longer needed. @@ -17,67 +17,50 @@ These utilities can help you manage your storage and clean up the downloaded fil ## Properties -### downloads +### fs -> `static` **downloads**: `Map`\<[`ResourceSource`](../type-aliases/ResourceSource.md), `DownloadResource`\> +> `static` **fs**: `object` -Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:64](https://github.com/software-mansion/react-native-executorch/blob/326d6344894d75625c600d5988666e215a32d466/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L64) +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:118](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L118) -## Methods - -### cancelFetching() - -> `static` **cancelFetching**(...`sources`): `Promise`\<`void`\> - -Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:284](https://github.com/software-mansion/react-native-executorch/blob/326d6344894d75625c600d5988666e215a32d466/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L284) - -Cancels an ongoing/paused download of files. - -#### Parameters +Filesystem utilities for reading downloaded resources. -##### sources +#### readAsString() -...[`ResourceSource`](../type-aliases/ResourceSource.md)[] +> **readAsString**: (`path`) => `Promise`\<`string`\> -The resource identifiers used when calling `fetch()`. +Reads the contents of a file as a string. -#### Returns +##### Parameters -`Promise`\<`void`\> +###### path -A promise that resolves once the download is canceled. +`string` ---- +Absolute file path to read. -### deleteResources() +##### Returns -> `static` **deleteResources**(...`sources`): `Promise`\<`void`\> +`Promise`\<`string`\> -Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:327](https://github.com/software-mansion/react-native-executorch/blob/326d6344894d75625c600d5988666e215a32d466/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L327) +A promise that resolves to the file contents as a string. -Deletes downloaded resources from the local filesystem. +##### Remarks -#### Parameters +**REQUIRED**: Used internally for reading configuration files (e.g., tokenizer configs). -##### sources +#### Remarks -...[`ResourceSource`](../type-aliases/ResourceSource.md)[] +Provides access to filesystem operations through the configured adapter. +Currently supports reading file contents as strings for configuration files. -The resource identifiers used when calling `fetch`. - -#### Returns - -`Promise`\<`void`\> - -A promise that resolves once all specified resources have been removed. - ---- +## Methods ### fetch() > `static` **fetch**(`callback`, ...`sources`): `Promise`\<`string`[] \| `null`\> -Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:74](https://github.com/software-mansion/react-native-executorch/blob/326d6344894d75625c600d5988666e215a32d466/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L74) +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:104](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L104) Fetches resources (remote URLs, local files or embedded assets), downloads or stores them locally for use by React Native ExecuTorch. @@ -100,109 +83,72 @@ Multiple resources that can be strings, asset references, or objects. `Promise`\<`string`[] \| `null`\> If the fetch was successful, it returns a promise which resolves to an array of local file paths for the downloaded/stored resources (without file:// prefix). -If the fetch was interrupted by `pauseFetching` or `cancelFetching`, it returns a promise which resolves to `null`. +If the fetch was interrupted, it returns a promise which resolves to `null`. --- -### getFilesTotalSize() +### getAdapter() -> `static` **getFilesTotalSize**(...`sources`): `Promise`\<`number`\> +> `static` **getAdapter**(): [`ResourceFetcherAdapter`](../interfaces/ResourceFetcherAdapter.md) -Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:345](https://github.com/software-mansion/react-native-executorch/blob/326d6344894d75625c600d5988666e215a32d466/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L345) +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:86](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L86) -Fetches the info about files size. Works only for remote files. - -#### Parameters - -##### sources - -...[`ResourceSource`](../type-aliases/ResourceSource.md)[] - -The resource identifiers (URLs). +Gets the current resource fetcher adapter instance. #### Returns -`Promise`\<`number`\> - -A promise that resolves to combined size of files in bytes. - ---- +[`ResourceFetcherAdapter`](../interfaces/ResourceFetcherAdapter.md) -### listDownloadedFiles() +The configured ResourceFetcherAdapter instance. -> `static` **listDownloadedFiles**(): `Promise`\<`string`[]\> +#### Throws -Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:306](https://github.com/software-mansion/react-native-executorch/blob/326d6344894d75625c600d5988666e215a32d466/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L306) - -Lists all the downloaded files used by React Native ExecuTorch. - -#### Returns +If no adapter has been set via [setAdapter](#setadapter). -`Promise`\<`string`[]\> +#### Remarks -A promise, which resolves to an array of URIs for all the downloaded files. +**INTERNAL**: Used internally by all resource fetching operations. --- -### listDownloadedModels() +### resetAdapter() -> `static` **listDownloadedModels**(): `Promise`\<`string`[]\> +> `static` **resetAdapter**(): `void` -Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:316](https://github.com/software-mansion/react-native-executorch/blob/326d6344894d75625c600d5988666e215a32d466/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L316) +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:73](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L73) -Lists all the downloaded models used by React Native ExecuTorch. +Resets the resource fetcher adapter to null. #### Returns -`Promise`\<`string`[]\> - -A promise, which resolves to an array of URIs for all the downloaded models. - ---- - -### pauseFetching() - -> `static` **pauseFetching**(...`sources`): `Promise`\<`void`\> - -Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:261](https://github.com/software-mansion/react-native-executorch/blob/326d6344894d75625c600d5988666e215a32d466/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L261) +`void` -Pauses an ongoing download of files. +#### Remarks -#### Parameters - -##### sources - -...[`ResourceSource`](../type-aliases/ResourceSource.md)[] - -The resource identifiers used when calling `fetch`. - -#### Returns - -`Promise`\<`void`\> - -A promise that resolves once the download is paused. +**INTERNAL**: Used primarily for testing purposes to clear the adapter state. --- -### resumeFetching() +### setAdapter() -> `static` **resumeFetching**(...`sources`): `Promise`\<`void`\> +> `static` **setAdapter**(`adapter`): `void` -Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:273](https://github.com/software-mansion/react-native-executorch/blob/326d6344894d75625c600d5988666e215a32d466/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L273) +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:63](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L63) -Resumes a paused download of files. +Sets a custom resource fetcher adapter for resource operations. #### Parameters -##### sources +##### adapter -...[`ResourceSource`](../type-aliases/ResourceSource.md)[] +[`ResourceFetcherAdapter`](../interfaces/ResourceFetcherAdapter.md) -The resource identifiers used when calling fetch. +The adapter instance to use for fetching resources. #### Returns -`Promise`\<`void`\> +`void` + +#### Remarks -If the fetch was successful, it returns a promise which resolves to an array of local file paths for the downloaded resources (without file:// prefix). -If the fetch was again interrupted by `pauseFetching` or `cancelFetching`, it returns a promise which resolves to `null`. +**INTERNAL**: Used by platform-specific init functions (expo/bare) to inject their fetcher implementation. diff --git a/docs/docs/06-api-reference/enumerations/DownloadStatus.md b/docs/docs/06-api-reference/enumerations/DownloadStatus.md new file mode 100644 index 000000000..41f8aeec6 --- /dev/null +++ b/docs/docs/06-api-reference/enumerations/DownloadStatus.md @@ -0,0 +1,25 @@ +# Enumeration: DownloadStatus + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:23](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L23) + +Download status of the file. + +## Enumeration Members + +### ONGOING + +> **ONGOING**: `0` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:27](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L27) + +Download is still in progress. + +--- + +### PAUSED + +> **PAUSED**: `1` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:32](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L32) + +Download is paused. diff --git a/docs/docs/06-api-reference/enumerations/HTTP_CODE.md b/docs/docs/06-api-reference/enumerations/HTTP_CODE.md new file mode 100644 index 000000000..3fb6a317e --- /dev/null +++ b/docs/docs/06-api-reference/enumerations/HTTP_CODE.md @@ -0,0 +1,25 @@ +# Enumeration: HTTP_CODE + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:8](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L8) + +Http status codes + +## Enumeration Members + +### OK + +> **OK**: `200` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:11](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L11) + +- Everything is ok. + +--- + +### PARTIAL_CONTENT + +> **PARTIAL_CONTENT**: `206` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:15](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L15) + +- Server has fulfilled a client request for a specific part of a resource, instead of sending the entire file. diff --git a/docs/docs/06-api-reference/enumerations/SourceType.md b/docs/docs/06-api-reference/enumerations/SourceType.md new file mode 100644 index 000000000..e9bfbb71b --- /dev/null +++ b/docs/docs/06-api-reference/enumerations/SourceType.md @@ -0,0 +1,55 @@ +# Enumeration: SourceType + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:40](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L40) + +Types of sources that can be downloaded + +## Enumeration Members + +### DEV_MODE_FILE + +> **DEV_MODE_FILE**: `3` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:59](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L59) + +Represents a file served via the metro bundler during development. + +--- + +### LOCAL_FILE + +> **LOCAL_FILE**: `1` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:49](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L49) + +Represents a file stored locally on the filesystem. + +--- + +### OBJECT + +> **OBJECT**: `0` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:44](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L44) + +Represents a raw object or data structure. + +--- + +### RELEASE_MODE_FILE + +> **RELEASE_MODE_FILE**: `2` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:54](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L54) + +Represents a file bundled with the application in release mode. + +--- + +### REMOTE_FILE + +> **REMOTE_FILE**: `4` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:64](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L64) + +Represents a file located at a remote URL. diff --git a/docs/docs/06-api-reference/functions/cleanupExecutorch.md b/docs/docs/06-api-reference/functions/cleanupExecutorch.md new file mode 100644 index 000000000..092ad8cb9 --- /dev/null +++ b/docs/docs/06-api-reference/functions/cleanupExecutorch.md @@ -0,0 +1,11 @@ +# Function: cleanupExecutorch() + +> **cleanupExecutorch**(): `void` + +Defined in: [packages/react-native-executorch/src/index.ts:32](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/index.ts#L32) + +Function that cleans current setup of fetching resources. + +## Returns + +`void` diff --git a/docs/docs/06-api-reference/functions/initExecutorch.md b/docs/docs/06-api-reference/functions/initExecutorch.md new file mode 100644 index 000000000..1a8fc8cc6 --- /dev/null +++ b/docs/docs/06-api-reference/functions/initExecutorch.md @@ -0,0 +1,19 @@ +# Function: initExecutorch() + +> **initExecutorch**(`config`): `void` + +Defined in: [packages/react-native-executorch/src/index.ts:23](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/index.ts#L23) + +Function that setups the provided resource fetcher. + +## Parameters + +### config + +[`ExecutorchConfig`](../interfaces/ExecutorchConfig.md) + +Configuration that you want to use in resource fetching. + +## Returns + +`void` diff --git a/docs/docs/06-api-reference/index.md b/docs/docs/06-api-reference/index.md index 1415421de..28c65d0cd 100644 --- a/docs/docs/06-api-reference/index.md +++ b/docs/docs/06-api-reference/index.md @@ -18,6 +18,10 @@ - [useVAD](functions/useVAD.md) - [useVerticalOCR](functions/useVerticalOCR.md) +## Interfaces + +- [ResourceSourceExtended](interfaces/ResourceSourceExtended.md) + ## Models - Classification - [EFFICIENTNET\_V2\_S](variables/EFFICIENTNET_V2_S.md) @@ -194,7 +198,10 @@ - [CocoLabel](enumerations/CocoLabel.md) - [DeeplabLabel](enumerations/DeeplabLabel.md) +- [DownloadStatus](enumerations/DownloadStatus.md) +- [HTTP_CODE](enumerations/HTTP_CODE.md) - [ScalarType](enumerations/ScalarType.md) +- [SourceType](enumerations/SourceType.md) - [Bbox](interfaces/Bbox.md) - [ChatConfig](interfaces/ChatConfig.md) - [ClassificationProps](interfaces/ClassificationProps.md) @@ -240,10 +247,13 @@ - [TokenizerType](interfaces/TokenizerType.md) - [ToolCall](interfaces/ToolCall.md) - [ToolsConfig](interfaces/ToolsConfig.md) +- [TranscriptionResult](interfaces/TranscriptionResult.md) +- [TranscriptionSegment](interfaces/TranscriptionSegment.md) - [VADProps](interfaces/VADProps.md) - [VADType](interfaces/VADType.md) - [VerticalOCRProps](interfaces/VerticalOCRProps.md) - [VoiceConfig](interfaces/VoiceConfig.md) +- [Word](interfaces/Word.md) - [LLMTool](type-aliases/LLMTool.md) - [MessageRole](type-aliases/MessageRole.md) - [OCRLanguage](type-aliases/OCRLanguage.md) @@ -273,7 +283,12 @@ ## Utilities - General +- [ResourceFetcherUtils](react-native-executorch/namespaces/ResourceFetcherUtils/index.md) - [ResourceFetcher](classes/ResourceFetcher.md) +- [ExecutorchConfig](interfaces/ExecutorchConfig.md) +- [ResourceFetcherAdapter](interfaces/ResourceFetcherAdapter.md) +- [cleanupExecutorch](functions/cleanupExecutorch.md) +- [initExecutorch](functions/initExecutorch.md) ## Utilities - LLM diff --git a/docs/docs/06-api-reference/interfaces/ExecutorchConfig.md b/docs/docs/06-api-reference/interfaces/ExecutorchConfig.md new file mode 100644 index 000000000..2fd75477b --- /dev/null +++ b/docs/docs/06-api-reference/interfaces/ExecutorchConfig.md @@ -0,0 +1,14 @@ +# Interface: ExecutorchConfig + +Defined in: [packages/react-native-executorch/src/index.ts:13](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/index.ts#L13) + +Configuration that goes to the `initExecutorch`. +You can pass either bare React Native or Expo configuration. + +## Properties + +### resourceFetcher + +> **resourceFetcher**: [`ResourceFetcherAdapter`](ResourceFetcherAdapter.md) + +Defined in: [packages/react-native-executorch/src/index.ts:14](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/index.ts#L14) diff --git a/docs/docs/06-api-reference/interfaces/ResourceFetcherAdapter.md b/docs/docs/06-api-reference/interfaces/ResourceFetcherAdapter.md new file mode 100644 index 000000000..bc87be0c7 --- /dev/null +++ b/docs/docs/06-api-reference/interfaces/ResourceFetcherAdapter.md @@ -0,0 +1,77 @@ +# Interface: ResourceFetcherAdapter + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:17](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L17) + +Adapter interface for resource fetching operations. +**Required Methods:** + +- `fetch`: Download resources to local storage (used by all modules) +- `readAsString`: Read file contents as string (used for config files) + +## Remarks + +This interface is intentionally minimal. Custom fetchers only need to implement +these two methods for the library to function correctly. + +## Methods + +### fetch() + +> **fetch**(`callback`, ...`sources`): `Promise`\<`string`[] \| `null`\> + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:29](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L29) + +Fetches resources (remote URLs, local files or embedded assets), downloads or stores them locally for use by React Native ExecuTorch. + +#### Parameters + +##### callback + +(`downloadProgress`) => `void` + +Optional callback to track progress of all downloads, reported between 0 and 1. + +##### sources + +...[`ResourceSource`](../type-aliases/ResourceSource.md)[] + +Multiple resources that can be strings, asset references, or objects. + +#### Returns + +`Promise`\<`string`[] \| `null`\> + +If the fetch was successful, it returns a promise which resolves to an array of local file paths for the downloaded/stored resources (without file:// prefix). +If the fetch was interrupted, it returns a promise which resolves to `null`. + +#### Remarks + +**REQUIRED**: Used by all library modules for downloading models and resources. + +--- + +### readAsString() + +> **readAsString**(`path`): `Promise`\<`string`\> + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcher.ts:43](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcher.ts#L43) + +Read file contents as a string. + +#### Parameters + +##### path + +`string` + +Absolute file path + +#### Returns + +`Promise`\<`string`\> + +File contents as string + +#### Remarks + +**REQUIRED**: Used internally for reading configuration files (e.g., tokenizer configs). diff --git a/docs/docs/06-api-reference/interfaces/ResourceSourceExtended.md b/docs/docs/06-api-reference/interfaces/ResourceSourceExtended.md new file mode 100644 index 000000000..612a2a316 --- /dev/null +++ b/docs/docs/06-api-reference/interfaces/ResourceSourceExtended.md @@ -0,0 +1,95 @@ +# Interface: ResourceSourceExtended + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:72](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L72) + +Extended interface for resource sources, tracking download state and file locations. + +## Properties + +### cacheFileUri? + +> `optional` **cacheFileUri**: `string` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:106](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L106) + +The URI where the file is cached. + +--- + +### callback()? + +> `optional` **callback**: (`downloadProgress`) => `void` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:86](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L86) + +Optional callback to report download progress (0 to 1). + +#### Parameters + +##### downloadProgress + +`number` + +#### Returns + +`void` + +--- + +### fileUri? + +> `optional` **fileUri**: `string` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:101](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L101) + +The local file URI where the resource is stored. + +--- + +### next? + +> `optional` **next**: `ResourceSourceExtended` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:111](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L111) + +Reference to the next resource in a linked chain of resources. + +--- + +### results + +> **results**: `string`[] + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:91](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L91) + +Array of paths or identifiers for the resulting files. + +--- + +### source + +> **source**: [`ResourceSource`](../type-aliases/ResourceSource.md) + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:76](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L76) + +The original source definition. + +--- + +### sourceType + +> **sourceType**: [`SourceType`](../enumerations/SourceType.md) + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:81](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L81) + +The type of the source (local, remote, etc.). + +--- + +### uri? + +> `optional` **uri**: `string` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:96](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L96) + +The URI of the resource. diff --git a/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/functions/calculateDownloadProgress.md b/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/functions/calculateDownloadProgress.md new file mode 100644 index 000000000..2fe3f81fe --- /dev/null +++ b/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/functions/calculateDownloadProgress.md @@ -0,0 +1,50 @@ +# Function: calculateDownloadProgress() + +> **calculateDownloadProgress**(`totalLength`, `previousFilesTotalLength`, `currentFileLength`, `setProgress`): (`progress`) => `void` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:155](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L155) + +Creates a progress callback that scales the current file's progress +relative to the total size of all files being downloaded. + +## Parameters + +### totalLength + +`number` + +The total size of all files in the download batch. + +### previousFilesTotalLength + +`number` + +The sum of sizes of files already downloaded. + +### currentFileLength + +`number` + +The size of the file currently being downloaded. + +### setProgress + +(`downloadProgress`) => `void` + +The main callback to update the global progress. + +## Returns + +A function that accepts the progress (0-1) of the current file. + +> (`progress`): `void` + +### Parameters + +#### progress + +`number` + +### Returns + +`void` diff --git a/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/functions/getFilenameFromUri.md b/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/functions/getFilenameFromUri.md new file mode 100644 index 000000000..b28841d7f --- /dev/null +++ b/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/functions/getFilenameFromUri.md @@ -0,0 +1,21 @@ +# Function: getFilenameFromUri() + +> **getFilenameFromUri**(`uri`): `string` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:204](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L204) + +Generates a safe filename from a URI by removing the protocol and replacing special characters. + +## Parameters + +### uri + +`string` + +The source URI. + +## Returns + +`string` + +A sanitized filename string. diff --git a/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/functions/hashObject.md b/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/functions/hashObject.md new file mode 100644 index 000000000..bed5ec3e8 --- /dev/null +++ b/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/functions/hashObject.md @@ -0,0 +1,21 @@ +# Function: hashObject() + +> **hashObject**(`jsonString`): `string` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:134](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L134) + +Generates a hash from a string representation of an object. + +## Parameters + +### jsonString + +`string` + +The stringified JSON object to hash. + +## Returns + +`string` + +The resulting hash as a string. diff --git a/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/functions/removeFilePrefix.md b/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/functions/removeFilePrefix.md new file mode 100644 index 000000000..4fae9ec1b --- /dev/null +++ b/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/functions/removeFilePrefix.md @@ -0,0 +1,21 @@ +# Function: removeFilePrefix() + +> **removeFilePrefix**(`uri`): `string` + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:125](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L125) + +Removes the 'file://' prefix from a URI if it exists. + +## Parameters + +### uri + +`string` + +The URI to process. + +## Returns + +`string` + +The URI without the 'file://' prefix. diff --git a/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/functions/triggerHuggingFaceDownloadCounter.md b/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/functions/triggerHuggingFaceDownloadCounter.md new file mode 100644 index 000000000..7bf49a7e9 --- /dev/null +++ b/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/functions/triggerHuggingFaceDownloadCounter.md @@ -0,0 +1,20 @@ +# Function: triggerHuggingFaceDownloadCounter() + +> **triggerHuggingFaceDownloadCounter**(`uri`): `Promise`\<`void`\> + +Defined in: [packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts:188](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts#L188) + +Increments the Hugging Face download counter if the URI points to a Software Mansion Hugging Face repo. +More information: https://huggingface.co/docs/hub/models-download-stats + +## Parameters + +### uri + +`string` + +The URI of the file being downloaded. + +## Returns + +`Promise`\<`void`\> diff --git a/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/index.md b/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/index.md new file mode 100644 index 000000000..6da4f1f5b --- /dev/null +++ b/docs/docs/06-api-reference/react-native-executorch/namespaces/ResourceFetcherUtils/index.md @@ -0,0 +1,11 @@ +# ResourceFetcherUtils + +Utility functions for fetching and managing resources. + +## Functions + +- [calculateDownloadProgress](functions/calculateDownloadProgress.md) +- [getFilenameFromUri](functions/getFilenameFromUri.md) +- [hashObject](functions/hashObject.md) +- [removeFilePrefix](functions/removeFilePrefix.md) +- [triggerHuggingFaceDownloadCounter](functions/triggerHuggingFaceDownloadCounter.md) diff --git a/docs/docs/06-api-reference/type-aliases/ResourceSource.md b/docs/docs/06-api-reference/type-aliases/ResourceSource.md index fb3c796bc..3ef63b671 100644 --- a/docs/docs/06-api-reference/type-aliases/ResourceSource.md +++ b/docs/docs/06-api-reference/type-aliases/ResourceSource.md @@ -2,6 +2,6 @@ > **ResourceSource** = `string` \| `number` \| `object` -Defined in: [packages/react-native-executorch/src/types/common.ts:10](https://github.com/software-mansion/react-native-executorch/blob/326d6344894d75625c600d5988666e215a32d466/packages/react-native-executorch/src/types/common.ts#L10) +Defined in: [packages/react-native-executorch/src/types/common.ts:10](https://github.com/software-mansion/react-native-executorch/blob/9db6e3b8b0f1b11ef66f7c45d29a251b31e9c252/packages/react-native-executorch/src/types/common.ts#L10) Represents a source of a resource, which can be a string (e.g., URL or file path), a number (e.g., resource ID), or an object (e.g., binary data). diff --git a/package.json b/package.json index e4ee020b2..33cbcc2d9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "packageManager": "yarn@4.1.1", "workspaces": { "packages": [ - "packages/react-native-executorch", + "packages/*", "apps/*" ] }, diff --git a/packages/bare-resource-fetcher/README.md b/packages/bare-resource-fetcher/README.md new file mode 100644 index 000000000..9b1493f1d --- /dev/null +++ b/packages/bare-resource-fetcher/README.md @@ -0,0 +1,37 @@ +# @react-native-executorch/bare-resource-fetcher + +Bare React Native adapter for `react-native-executorch` that provides resource fetching capabilities using native filesystem libraries. + +## Installation + +```bash +yarn add @react-native-executorch/bare-resource-fetcher +yarn add @dr.pogodin/react-native-fs @kesha-antonov/react-native-background-downloader +``` + +### Native Dependencies Setup + +After installing, follow the setup guides for the native dependencies: + +- **[@dr.pogodin/react-native-fs](https://github.com/birdofpreyru/react-native-fs#getting-started)** - Filesystem operations +- **[@kesha-antonov/react-native-background-downloader](https://github.com/kesha-antonov/react-native-background-downloader#bare-react-native-projects)** - Background download support + +> **Note**: Make sure to complete the native setup (iOS/Android configuration) for both dependencies before using this adapter. + +## Usage + +```typescript +import { initExecutorch } from 'react-native-executorch'; +import { BareResourceFetcher } from '@react-native-executorch/bare-resource-fetcher'; + +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 diff --git a/packages/bare-resource-fetcher/package.json b/packages/bare-resource-fetcher/package.json new file mode 100644 index 000000000..b5533ac48 --- /dev/null +++ b/packages/bare-resource-fetcher/package.json @@ -0,0 +1,43 @@ +{ + "name": "@react-native-executorch/bare-resource-fetcher", + "version": "0.1.0", + "description": "Bare React Native resource fetcher for react-native-executorch", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "exports": { + ".": { + "import": "./lib/index.js", + "types": "./lib/index.d.ts" + } + }, + "files": [ + "lib" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/software-mansion/react-native-executorch.git", + "directory": "packages/bare-resource-fetcher" + }, + "scripts": { + "prepare": "tsc", + "typecheck": "tsc --noEmit", + "lint": "eslint \"**/*.{js,ts,tsx}\"", + "clean": "del-cli lib" + }, + "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-resource-fetcher/src/ResourceFetcher.ts b/packages/bare-resource-fetcher/src/ResourceFetcher.ts new file mode 100644 index 000000000..406ad34b2 --- /dev/null +++ b/packages/bare-resource-fetcher/src/ResourceFetcher.ts @@ -0,0 +1,572 @@ +/** + * Resource Fetcher for React Native applications. + * + * This module provides functions to download and manage files stored in the application's document directory + * inside the `react-native-executorch/` directory. These utilities help manage storage and clean up downloaded + * files when they are no longer needed. + * + * @category Utilities - General + * + * @remarks + * **Key Functionality:** + * - **Download Control**: Pause, resume, and cancel operations through: + * - {@link pauseFetching} - Pause ongoing downloads + * - {@link resumeFetching} - Resume paused downloads + * - {@link cancelFetching} - Cancel ongoing or paused downloads + * - **File Management**: + * - {@link getFilesTotalSize} - Get total size of resources + * - {@link listDownloadedFiles} - List all downloaded files + * - {@link listDownloadedModels} - List downloaded model files (.pte) + * - {@link deleteResources} - Delete downloaded resources + * + * **Important Notes:** + * - Pause/resume/cancel operations work only for remote resources + * - Most functions accept multiple `ResourceSource` arguments (string, number, or object) + * - The {@link fetch} method accepts a progress callback (0-1) and returns file paths or null if interrupted + * + * **Technical Implementation:** + * - Maintains a `downloads` Map to track active and paused downloads + * - Successful downloads are automatically removed from the Map + * - Uses `ResourceSourceExtended` interface for pause/resume functionality with linked-list behavior + */ + +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; + completeDownload( + extendedInfo: ResourceSourceExtended, + source: ResourceSource + ): Promise; + pause(source: ResourceSource): Promise; + resume(source: ResourceSource): Promise; + cancel(source: ResourceSource): Promise; + findActive(sources: ResourceSource[]): ResourceSource; + pauseFetching(...sources: ResourceSource[]): Promise; + resumeFetching(...sources: ResourceSource[]): Promise; + cancelFetching(...sources: ResourceSource[]): Promise; + listDownloadedFiles(): Promise; + listDownloadedModels(): Promise; + deleteResources(...sources: ResourceSource[]): Promise; + getFilesTotalSize(...sources: ResourceSource[]): Promise; + handleObject(source: ResourceSource): Promise; + handleLocalFile(source: ResourceSource): string; + handleReleaseModeFile( + sourceExtended: ResourceSourceExtended + ): Promise; + handleDevModeFile( + sourceExtended: ResourceSourceExtended + ): Promise; + handleRemoteFile( + sourceExtended: ResourceSourceExtended + ): Promise; +} + +/** + * This module provides functions to download and work with downloaded files stored in the application's document directory inside the `react-native-executorch/` directory. + * These utilities can help you manage your storage and clean up the downloaded files when they are no longer needed. + * + * @category Utilities - General + */ +export const BareResourceFetcher: BareResourceFetcherInterface = { + downloads: new Map(), //map of currently downloading (or paused) files, if the download was started by .fetch() method. + + /** + * Fetches resources (remote URLs, local files or embedded assets), downloads or stores them locally for use by React Native ExecuTorch. + * + * @param callback - Optional callback to track progress of all downloads, reported between 0 and 1. + * @param sources - Multiple resources that can be strings, asset references, or objects. + * @returns If the fetch was successful, it returns a promise which resolves to an array of local file paths for the downloaded/stored resources (without file:// prefix). + * If the fetch was interrupted by `pauseFetching` or `cancelFetching`, it returns a promise which resolves to `null`. + */ + 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); + if (!resource) { + throw new RnExecutorchError( + RnExecutorchErrorCode.NotFound, + 'No active download found for the given 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 () => { + const result = await this.completeDownload( + resource.extendedInfo, + source + ); + resolve(result); + }) + .error((e: any) => { + reject(e); + }); + }); + } + } + }, + + async cancel(source: ResourceSource) { + const resource = this.downloads.get(source); + if (!resource) { + throw new RnExecutorchError( + RnExecutorchErrorCode.NotFound, + 'No active download found for the given source' + ); + } + resource.task.stop(); + this.downloads.delete(source); + }, + + async completeDownload( + extendedInfo: ResourceSourceExtended, + source: ResourceSource + ): Promise { + // Check if download was cancelled or paused + if ( + !this.downloads.has(source) || + this.downloads.get(source)!.status === DownloadStatus.PAUSED + ) { + return null; + } + + await RNFS.moveFile(extendedInfo.cacheFileUri!, extendedInfo.fileUri!); + this.downloads.delete(source); + ResourceFetcherUtils.triggerHuggingFaceDownloadCounter(extendedInfo.uri!); + + const filename = extendedInfo.fileUri!.split('/').pop(); + if (filename) { + await completeHandler(filename); + } + + const result = this.returnOrStartNext( + extendedInfo, + ResourceFetcherUtils.removeFilePrefix(extendedInfo.fileUri!) + ); + return result instanceof Promise ? await result : result; + }, + + /** + * Pauses an ongoing download of files. + * + * @param sources - The resource identifiers used when calling `fetch`. + * @returns A promise that resolves once the download is paused. + */ + async pauseFetching(...sources: ResourceSource[]) { + const source = this.findActive(sources); + await this.pause(source); + }, + + /** + * Resumes a paused download of files. + * + * @param sources - The resource identifiers used when calling fetch. + * @returns If the fetch was successful, it returns a promise which resolves to an array of local file paths for the downloaded resources (without file:// prefix). + * If the fetch was again interrupted by `pauseFetching` or `cancelFetching`, it returns a promise which resolves to `null`. + */ + async resumeFetching(...sources: ResourceSource[]) { + const source = this.findActive(sources); + await this.resume(source); + }, + + /** + * Cancels an ongoing/paused download of files. + * + * @param sources - The resource identifiers used when calling `fetch()`. + * @returns A promise that resolves once the download is canceled. + */ + 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.' + ); + }, + + /** + * Lists all the downloaded files used by React Native ExecuTorch. + * + * @returns A promise, which resolves to an array of URIs for all the downloaded files. + */ + async listDownloadedFiles() { + const files = await RNFS.readDir(RNEDirectory); + return files.map((file: any) => file.path); + }, + + /** + * Lists all the downloaded models used by React Native ExecuTorch. + * + * @returns A promise, which resolves to an array of URIs for all the downloaded models. + */ + async listDownloadedModels() { + const files = await this.listDownloadedFiles(); + return files.filter((file: string) => file.endsWith('.pte')); + }, + + /** + * Deletes downloaded resources from the local filesystem. + * + * @param sources - The resource identifiers used when calling `fetch`. + * @returns A promise that resolves once all specified resources have been removed. + */ + 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); + } + } + }, + + /** + * Fetches the info about files size. Works only for remote files. + * + * @param sources - The resource identifiers (URLs). + * @returns A promise that resolves to combined size of files in bytes. + */ + 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 () => { + const nextResult = await this.completeDownload( + sourceExtended, + source + ); + resolve(nextResult); + }) + .error((error: any) => { + 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); + }); + }, + + /** + * Reads the contents of a file as a string. + * + * @param path - Absolute file path to read. + * @returns A promise that resolves to the file contents as a string. + * + * @remarks + * **REQUIRED**: Used internally for reading configuration files (e.g., tokenizer configs). + */ + async readAsString(path: string) { + return await RNFS.readFile(path, 'utf8'); + }, +}; diff --git a/packages/bare-resource-fetcher/src/ResourceFetcherUtils.ts b/packages/bare-resource-fetcher/src/ResourceFetcherUtils.ts new file mode 100644 index 000000000..74e25f54b --- /dev/null +++ b/packages/bare-resource-fetcher/src/ResourceFetcherUtils.ts @@ -0,0 +1,107 @@ +import { RNEDirectory } from './constants/directories'; +import { + ResourceSource, + Logger, + ResourceFetcherUtils as CoreUtils, + HTTP_CODE, + DownloadStatus, + SourceType, + ResourceSourceExtended, + RnExecutorchError, + RnExecutorchErrorCode, +} 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 }; + +/** + * Utility functions for fetching and managing resources. + * + * @category Utilities - General + */ +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))) { + try { + await RNFS.mkdir(RNEDirectory); + } catch (error) { + throw new RnExecutorchError( + RnExecutorchErrorCode.FileWriteFailed, + `Failed to create directory at ${RNEDirectory}`, + error + ); + } + } + } + + export async function checkFileExists(fileUri: string) { + return await RNFS.exists(fileUri); + } +} diff --git a/packages/bare-resource-fetcher/src/constants/directories.ts b/packages/bare-resource-fetcher/src/constants/directories.ts new file mode 100644 index 000000000..4ba8b8401 --- /dev/null +++ b/packages/bare-resource-fetcher/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-resource-fetcher/src/declarations.d.ts b/packages/bare-resource-fetcher/src/declarations.d.ts new file mode 100644 index 000000000..a5ff5801d --- /dev/null +++ b/packages/bare-resource-fetcher/src/declarations.d.ts @@ -0,0 +1,53 @@ +declare module '@kesha-antonov/react-native-background-downloader' { + export const directories: { + documents: string; + library: string; + temp: string; + }; + + export interface DownloadTask { + id: string; + percent: number; + stop(): void; + pause(): void; + resume(): void; + start(): void; + + begin(handler: (params: BeginHandlerParams) => void): DownloadTask; + progress(handler: (params: ProgressHandlerParams) => void): DownloadTask; + done(handler: () => void): DownloadTask; + error(handler: (error: any) => void): DownloadTask; + } + + export interface BeginHandlerParams { + expectedBytes: number; + headers: { [key: string]: string }; + } + + export interface ProgressHandlerParams { + percent: number; + bytesDownloaded: number; + bytesTotal: number; + } + + export function createDownloadTask(options: { + id: string; + url: string; + destination: string; + headers?: object; + }): DownloadTask; + + export function completeHandler(jobId: string): void; + + const RNBackgroundDownloader: { + download(options: { + id: string; + url: string; + destination: string; + }): DownloadTask; + directories: { documents: string; library: string; temp: string }; + }; + export default RNBackgroundDownloader; +} + +declare module '@dr.pogodin/react-native-fs'; diff --git a/packages/bare-resource-fetcher/src/index.ts b/packages/bare-resource-fetcher/src/index.ts new file mode 100644 index 000000000..c0ec4024f --- /dev/null +++ b/packages/bare-resource-fetcher/src/index.ts @@ -0,0 +1 @@ +export * from './ResourceFetcher'; diff --git a/packages/bare-resource-fetcher/tsconfig.json b/packages/bare-resource-fetcher/tsconfig.json new file mode 100644 index 000000000..cadd2509a --- /dev/null +++ b/packages/bare-resource-fetcher/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "declaration": true, + "declarationMap": true, + "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-resource-fetcher/README.md b/packages/expo-resource-fetcher/README.md new file mode 100644 index 000000000..279853434 --- /dev/null +++ b/packages/expo-resource-fetcher/README.md @@ -0,0 +1,30 @@ +# @react-native-executorch/expo-resource-fetcher + +Expo adapter for `react-native-executorch` that provides resource fetching capabilities using Expo's filesystem APIs. + +## Installation + +```bash +yarn add @react-native-executorch/expo-resource-fetcher +yarn add expo-file-system expo-asset +``` + +## Usage + +```typescript +import { initExecutorch } from 'react-native-executorch'; +import { ExpoResourceFetcher } from '@react-native-executorch/expo-resource-fetcher'; + +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-resource-fetcher/package.json b/packages/expo-resource-fetcher/package.json new file mode 100644 index 000000000..7cf89487f --- /dev/null +++ b/packages/expo-resource-fetcher/package.json @@ -0,0 +1,45 @@ +{ + "name": "@react-native-executorch/expo-resource-fetcher", + "version": "0.1.0", + "description": "Expo resource fetcher for react-native-executorch", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "exports": { + ".": { + "import": "./lib/index.js", + "types": "./lib/index.d.ts" + } + }, + "files": [ + "lib" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/software-mansion/react-native-executorch.git", + "directory": "packages/expo-resource-fetcher" + }, + "scripts": { + "prepare": "tsc", + "typecheck": "tsc --noEmit", + "lint": "eslint \"**/*.{js,ts,tsx}\"", + "clean": "del-cli lib" + }, + "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-resource-fetcher/src/ResourceFetcher.ts b/packages/expo-resource-fetcher/src/ResourceFetcher.ts new file mode 100644 index 000000000..4a81bdfe6 --- /dev/null +++ b/packages/expo-resource-fetcher/src/ResourceFetcher.ts @@ -0,0 +1,561 @@ +/** + * Resource Fetcher for Expo applications. + * + * This module provides functions to download and manage files stored in the application's document directory + * inside the `react-native-executorch/` directory. These utilities help manage storage and clean up downloaded + * files when they are no longer needed. + * + * @category Utilities - General + * + * @remarks + * **Key Functionality:** + * - **Download Control**: Pause, resume, and cancel operations through: + * - {@link pauseFetching} - Pause ongoing downloads + * - {@link resumeFetching} - Resume paused downloads + * - {@link cancelFetching} - Cancel ongoing or paused downloads + * - **File Management**: + * - {@link getFilesTotalSize} - Get total size of resources + * - {@link listDownloadedFiles} - List all downloaded files + * - {@link listDownloadedModels} - List downloaded model files (.pte) + * - {@link deleteResources} - Delete downloaded resources + * + * **Important Notes:** + * - Pause/resume/cancel operations work only for remote resources + * - Most functions accept multiple `ResourceSource` arguments (string, number, or object) + * - The {@link fetch} method accepts a progress callback (0-1) and returns file paths or null if interrupted + * + * **Technical Implementation:** + * - Maintains a `downloads` Map to track active and paused downloads + * - Successful downloads are automatically removed from the Map + * - Uses `ResourceSourceExtended` interface for pause/resume functionality with linked-list behavior + */ + +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; + pauseFetching(...sources: ResourceSource[]): Promise; + resumeFetching(...sources: ResourceSource[]): Promise; + cancelFetching(...sources: ResourceSource[]): Promise; + listDownloadedFiles(): Promise; + listDownloadedModels(): Promise; + deleteResources(...sources: ResourceSource[]): Promise; + getFilesTotalSize(...sources: ResourceSource[]): Promise; + handleObject(source: ResourceSource): Promise; + handleLocalFile(source: ResourceSource): string; + handleReleaseModeFile( + sourceExtended: ResourceSourceExtended + ): Promise; + handleDevModeFile( + sourceExtended: ResourceSourceExtended + ): Promise; + handleRemoteFile( + sourceExtended: ResourceSourceExtended + ): Promise; +} + +/** + * This module provides functions to download and work with downloaded files stored in the application's document directory inside the `react-native-executorch/` directory. + * These utilities can help you manage your storage and clean up the downloaded files when they are no longer needed. + * + * @category Utilities - General + */ +export const ExpoResourceFetcher: ExpoResourceFetcherInterface = { + downloads: new Map(), //map of currently downloading (or paused) files, if the download was started by .fetch() method. + + /** + * Fetches resources (remote URLs, local files or embedded assets), downloads or stores them locally for use by React Native ExecuTorch. + * + * @param callback - Optional callback to track progress of all downloads, reported between 0 and 1. + * @param sources - Multiple resources that can be strings, asset references, or objects. + * @returns If the fetch was successful, it returns a promise which resolves to an array of local file paths for the downloaded/stored resources (without file:// prefix). + * If the fetch was interrupted by `pauseFetching` or `cancelFetching`, it returns a promise which resolves to `null`. + */ + 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); + }, + + /** + * Pauses an ongoing download of files. + * + * @param sources - The resource identifiers used when calling `fetch`. + * @returns A promise that resolves once the download is paused. + */ + async pauseFetching(...sources: ResourceSource[]) { + const source = this.findActive(sources); + await this.pause(source); + }, + + /** + * Resumes a paused download of files. + * + * @param sources - The resource identifiers used when calling fetch. + * @returns If the fetch was successful, it returns a promise which resolves to an array of local file paths for the downloaded resources (without file:// prefix). + * If the fetch was again interrupted by `pauseFetching` or `cancelFetching`, it returns a promise which resolves to `null`. + */ + async resumeFetching(...sources: ResourceSource[]) { + const source = this.findActive(sources); + await this.resume(source); + }, + + /** + * Cancels an ongoing/paused download of files. + * + * @param sources - The resource identifiers used when calling `fetch()`. + * @returns A promise that resolves once the download is canceled. + */ + 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.' + ); + }, + + /** + * Lists all the downloaded files used by React Native ExecuTorch. + * + * @returns A promise, which resolves to an array of URIs for all the downloaded files. + */ + async listDownloadedFiles() { + const files = await readDirectoryAsync(RNEDirectory); + return files.map((file: string) => `${RNEDirectory}${file}`); + }, + + /** + * Lists all the downloaded models used by React Native ExecuTorch. + * + * @returns A promise, which resolves to an array of URIs for all the downloaded models. + */ + async listDownloadedModels() { + const files = await this.listDownloadedFiles(); + return files.filter((file: string) => file.endsWith('.pte')); + }, + + /** + * Deletes downloaded resources from the local filesystem. + * + * @param sources - The resource identifiers used when calling `fetch`. + * @returns A promise that resolves once all specified resources have been removed. + */ + 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); + } + } + }, + + /** + * Fetches the info about files size. Works only for remote files. + * + * @param sources - The resource identifiers (URLs). + * @returns A promise that resolves to combined size of files in bytes. + */ + 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); + }, + + /** + * Reads the contents of a file as a string. + * + * @param path - Absolute file path or file URI to read. + * @returns A promise that resolves to the file contents as a string. + * + * @remarks + * **REQUIRED**: Used internally for reading configuration files (e.g., tokenizer configs). + * + * **Technical Note**: Expo requires file URIs (file:// prefix), so this method + * automatically converts plain paths to URIs if needed. + */ + async readAsString(path: string) { + // Expo needs URI + const uri = path.startsWith('file://') ? path : `file://${path}`; + return await readAsStringAsync(uri); + }, +}; diff --git a/packages/expo-resource-fetcher/src/ResourceFetcherUtils.ts b/packages/expo-resource-fetcher/src/ResourceFetcherUtils.ts new file mode 100644 index 000000000..3165d39c9 --- /dev/null +++ b/packages/expo-resource-fetcher/src/ResourceFetcherUtils.ts @@ -0,0 +1,121 @@ +import { RNEDirectory } from './constants/directories'; +import { + ResourceSource, + Logger, + ResourceFetcherUtils as CoreUtils, + HTTP_CODE, + DownloadStatus, + SourceType, + ResourceSourceExtended, + RnExecutorchError, + RnExecutorchErrorCode, +} 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; +} + +/** + * Utility functions for fetching and managing resources. + * + * @category Utilities - General + */ +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))) { + try { + await makeDirectoryAsync(RNEDirectory, { intermediates: true }); + } catch (error) { + throw new RnExecutorchError( + RnExecutorchErrorCode.FileWriteFailed, + `Failed to create directory at ${RNEDirectory}`, + error + ); + } + } + } + + 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-resource-fetcher/src/constants/directories.ts similarity index 100% rename from packages/react-native-executorch/src/constants/directories.ts rename to packages/expo-resource-fetcher/src/constants/directories.ts diff --git a/packages/expo-resource-fetcher/src/index.ts b/packages/expo-resource-fetcher/src/index.ts new file mode 100644 index 000000000..c0ec4024f --- /dev/null +++ b/packages/expo-resource-fetcher/src/index.ts @@ -0,0 +1 @@ +export * from './ResourceFetcher'; diff --git a/packages/expo-resource-fetcher/tsconfig.json b/packages/expo-resource-fetcher/tsconfig.json new file mode 100644 index 000000000..cadd2509a --- /dev/null +++ b/packages/expo-resource-fetcher/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "declaration": true, + "declarationMap": true, + "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/common/rnexecutorch/ErrorCodes.h b/packages/react-native-executorch/common/rnexecutorch/ErrorCodes.h index 6ca7b8581..f4fd2e7f0 100644 --- a/packages/react-native-executorch/common/rnexecutorch/ErrorCodes.h +++ b/packages/react-native-executorch/common/rnexecutorch/ErrorCodes.h @@ -9,27 +9,33 @@ namespace rnexecutorch { enum class RnExecutorchErrorCode : int32_t { /** - * An umbrella-error that is thrown usually when something unexpected happens, for example a 3rd-party library error. + * An umbrella-error that is thrown usually when something unexpected happens, + * for example a 3rd-party library error. */ UnknownError = 101, /** - * Thrown when a user tries to run a model that is not yet downloaded or loaded into memory. + * Thrown when a user tries to run a model that is not yet downloaded or + * loaded into memory. */ ModuleNotLoaded = 102, /** - * An error ocurred when saving a file. This could be, for instance a result image from an image model. + * An error ocurred when saving a file. This could be, for instance a result + * image from an image model. */ FileWriteFailed = 103, /** - * Thrown when a user tries to run a model that is currently processing. It is only allowed to run a single model prediction at a time. + * Thrown when a user tries to run a model that is currently processing. It is + * only allowed to run a single model prediction at a time. */ ModelGenerating = 104, /** - * Thrown when a language is passed to a multi-language model that is not supported. For example OCR or Speech To Text. + * Thrown when a language is passed to a multi-language model that is not + * supported. For example OCR or Speech To Text. */ LanguageNotSupported = 105, /** - * Thrown when config parameters passed to a model are invalid. For example, when LLM's topp is outside of range [0, 1]. + * Thrown when config parameters passed to a model are invalid. For example, + * when LLM's topp is outside of range [0, 1]. */ InvalidConfig = 112, /** @@ -37,7 +43,8 @@ enum class RnExecutorchErrorCode : int32_t { */ InvalidModelSource = 255, /** - * Thrown when the number of passed inputs to the model is different than the model metadata specifies. + * Thrown when the number of passed inputs to the model is different than the + * model metadata specifies. */ UnexpectedNumInputs = 97, /** @@ -45,7 +52,8 @@ enum class RnExecutorchErrorCode : int32_t { */ ThreadPoolError = 113, /** - * Thrown when a file read operation failed. This could be invalid image url passed to image models, or unsupported format. + * Thrown when a file read operation failed. This could be invalid image url + * passed to image models, or unsupported format. */ FileReadFailed = 114, /** @@ -53,7 +61,8 @@ enum class RnExecutorchErrorCode : int32_t { */ InvalidModelOutput = 115, /** - * Thrown when the dimensions of input tensors don't match the model's expected dimensions. + * Thrown when the dimensions of input tensors don't match the model's + * expected dimensions. */ WrongDimensions = 116, /** @@ -62,7 +71,8 @@ enum class RnExecutorchErrorCode : int32_t { */ InvalidUserInput = 117, /** - * Thrown when the number of downloaded files is unexpected, due to download interruptions. + * Thrown when the number of downloaded files is unexpected, due to download + * interruptions. */ DownloadInterrupted = 118, /** @@ -75,19 +85,23 @@ enum class RnExecutorchErrorCode : int32_t { */ MultilingualConfiguration = 160, /** - * Thrown when streaming transcription is attempted but audio data chunk is missing. + * Thrown when streaming transcription is attempted but audio data chunk is + * missing. */ MissingDataChunk = 161, /** - * Thrown when trying to stop or insert data into a stream that hasn't been started. + * Thrown when trying to stop or insert data into a stream that hasn't been + * started. */ StreamingNotStarted = 162, /** - * Thrown when trying to start a new streaming session while another is already in progress. + * Thrown when trying to start a new streaming session while another is + * already in progress. */ StreamingInProgress = 163, /** - * Thrown when a resource fails to download. This could be due to invalid URL, or for example a network problem. + * Thrown when a resource fails to download. This could be due to invalid URL, + * or for example a network problem. */ ResourceFetcherDownloadFailed = 180, /** @@ -103,13 +117,18 @@ enum class RnExecutorchErrorCode : int32_t { */ ResourceFetcherAlreadyOngoing = 183, /** - * Thrown when trying to pause, resume, or cancel a download that is not active. + * Thrown when trying to pause, resume, or cancel a download that is not + * active. */ ResourceFetcherNotActive = 184, /** * Thrown when required URI information is missing for a download operation. */ ResourceFetcherMissingUri = 185, + /** + * Thrown when trying to load resources without fetcher initialization. + */ + ResourceFetcherAdapterNotInitialized = 186, }; } // namespace rnexecutorch diff --git a/packages/react-native-executorch/package.json b/packages/react-native-executorch/package.json index 0541d03cb..0d455102d 100644 --- a/packages/react-native-executorch/package.json +++ b/packages/react-native-executorch/package.json @@ -66,9 +66,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": "*" }, @@ -76,9 +73,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/common/Logger.ts b/packages/react-native-executorch/src/common/Logger.ts index e97613321..7e05f52a2 100644 --- a/packages/react-native-executorch/src/common/Logger.ts +++ b/packages/react-native-executorch/src/common/Logger.ts @@ -1,5 +1,7 @@ +/** + * High level wrapper that prefixes `console.` with [React Native ExecuTorch] tag. + */ /* eslint-disable no-console */ - export class Logger { private static readonly PREFIX = '[React Native ExecuTorch]'; diff --git a/packages/react-native-executorch/src/controllers/BaseOCRController.ts b/packages/react-native-executorch/src/controllers/BaseOCRController.ts index 9f0d5d611..c124dadce 100644 --- a/packages/react-native-executorch/src/controllers/BaseOCRController.ts +++ b/packages/react-native-executorch/src/controllers/BaseOCRController.ts @@ -1,3 +1,4 @@ +import { Logger } from '../common/Logger'; import { symbols } from '../constants/ocr/symbols'; import { RnExecutorchErrorCode } from '../errors/ErrorCodes'; import { RnExecutorchError, parseUnknownError } from '../errors/errorUtils'; @@ -71,7 +72,14 @@ export abstract class BaseOCRController { this.isReady = true; this.isReadyCallback(this.isReady); } catch (e) { - if (this.errorCallback) { + if ( + e && + typeof e === 'object' && + 'code' in e && + e.code === RnExecutorchErrorCode.ResourceFetcherAdapterNotInitialized + ) { + Logger.error('Load failed:', e); + } else if (this.errorCallback) { this.errorCallback(parseUnknownError(e)); } else { throw parseUnknownError(e); diff --git a/packages/react-native-executorch/src/controllers/LLMController.ts b/packages/react-native-executorch/src/controllers/LLMController.ts index 62b438fab..46c35caba 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'; @@ -117,7 +116,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); @@ -134,6 +133,7 @@ export class LLMController { this.tokenCallback(filtered); }; } catch (e) { + Logger.error('Load failed:', e); this.isReadyCallback(false); throw parseUnknownError(e); } diff --git a/packages/react-native-executorch/src/errors/ErrorCodes.ts b/packages/react-native-executorch/src/errors/ErrorCodes.ts index 123c90e2b..3e4e557a1 100644 --- a/packages/react-native-executorch/src/errors/ErrorCodes.ts +++ b/packages/react-native-executorch/src/errors/ErrorCodes.ts @@ -102,6 +102,10 @@ export enum RnExecutorchErrorCode { * Thrown when required URI information is missing for a download operation. */ ResourceFetcherMissingUri = 185, + /** + * Thrown when trying to load resources without fetcher initialization. + */ + ResourceFetcherAdapterNotInitialized = 186, /** * Status indicating a successful operation. */ diff --git a/packages/react-native-executorch/src/index.ts b/packages/react-native-executorch/src/index.ts index a42881f45..8b4035232 100644 --- a/packages/react-native-executorch/src/index.ts +++ b/packages/react-native-executorch/src/index.ts @@ -1,4 +1,37 @@ import { ETInstallerNativeModule } from './native/RnExecutorchModules'; +import { + ResourceFetcher, + ResourceFetcherAdapter, +} from './utils/ResourceFetcher'; + +/** + * Configuration that goes to the `initExecutorch`. + * You can pass either bare React Native or Expo configuration. + * + * @category Utilities - General + */ +export interface ExecutorchConfig { + resourceFetcher: ResourceFetcherAdapter; +} + +/** + * Function that setups the provided resource fetcher. + * + * @category Utilities - General + * @param config - Configuration that you want to use in resource fetching. + */ +export function initExecutorch(config: ExecutorchConfig) { + ResourceFetcher.setAdapter(config.resourceFetcher); +} + +/** + * Function that cleans current setup of fetching resources. + * + * @category Utilities - General + */ +export function cleanupExecutorch() { + ResourceFetcher.resetAdapter(); +} // eslint-disable no-var declare global { @@ -113,7 +146,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/modules/computer_vision/ClassificationModule.ts b/packages/react-native-executorch/src/modules/computer_vision/ClassificationModule.ts index f6143e7e6..45b7e2b39 100644 --- a/packages/react-native-executorch/src/modules/computer_vision/ClassificationModule.ts +++ b/packages/react-native-executorch/src/modules/computer_vision/ClassificationModule.ts @@ -2,7 +2,8 @@ import { ResourceFetcher } from '../../utils/ResourceFetcher'; import { ResourceSource } from '../../types/common'; import { BaseModule } from '../BaseModule'; import { RnExecutorchErrorCode } from '../../errors/ErrorCodes'; -import { RnExecutorchError } from '../../errors/errorUtils'; +import { parseUnknownError, RnExecutorchError } from '../../errors/errorUtils'; +import { Logger } from '../../common/Logger'; /** * Module for image classification tasks. @@ -21,17 +22,24 @@ export class ClassificationModule extends BaseModule { model: { modelSource: ResourceSource }, onDownloadProgressCallback: (progress: number) => void = () => {} ): Promise { - const paths = await ResourceFetcher.fetch( - onDownloadProgressCallback, - model.modelSource - ); - if (paths === null || paths.length < 1) { - throw new RnExecutorchError( - RnExecutorchErrorCode.DownloadInterrupted, - 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + try { + const paths = await ResourceFetcher.fetch( + onDownloadProgressCallback, + model.modelSource ); + + if (!paths?.[0]) { + throw new RnExecutorchError( + RnExecutorchErrorCode.DownloadInterrupted, + 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + ); + } + + this.nativeModule = global.loadClassification(paths[0]); + } catch (error) { + Logger.error('Load failed:', error); + throw parseUnknownError(error); } - this.nativeModule = global.loadClassification(paths[0] || ''); } /** diff --git a/packages/react-native-executorch/src/modules/computer_vision/ImageEmbeddingsModule.ts b/packages/react-native-executorch/src/modules/computer_vision/ImageEmbeddingsModule.ts index b8943ae25..3e62f450d 100644 --- a/packages/react-native-executorch/src/modules/computer_vision/ImageEmbeddingsModule.ts +++ b/packages/react-native-executorch/src/modules/computer_vision/ImageEmbeddingsModule.ts @@ -1,8 +1,9 @@ import { ResourceFetcher } from '../../utils/ResourceFetcher'; import { ResourceSource } from '../../types/common'; import { RnExecutorchErrorCode } from '../../errors/ErrorCodes'; -import { RnExecutorchError } from '../../errors/errorUtils'; +import { parseUnknownError, RnExecutorchError } from '../../errors/errorUtils'; import { BaseModule } from '../BaseModule'; +import { Logger } from '../../common/Logger'; /** * Module for generating image embeddings from input images. @@ -20,17 +21,24 @@ export class ImageEmbeddingsModule extends BaseModule { model: { modelSource: ResourceSource }, onDownloadProgressCallback: (progress: number) => void = () => {} ): Promise { - const paths = await ResourceFetcher.fetch( - onDownloadProgressCallback, - model.modelSource - ); - if (paths === null || paths.length < 1) { - throw new RnExecutorchError( - RnExecutorchErrorCode.DownloadInterrupted, - 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + try { + const paths = await ResourceFetcher.fetch( + onDownloadProgressCallback, + model.modelSource ); + + if (!paths?.[0]) { + throw new RnExecutorchError( + RnExecutorchErrorCode.DownloadInterrupted, + 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + ); + } + + this.nativeModule = global.loadImageEmbeddings(paths[0]); + } catch (error) { + Logger.error('Load failed:', error); + throw parseUnknownError(error); } - this.nativeModule = global.loadImageEmbeddings(paths[0] || ''); } /** diff --git a/packages/react-native-executorch/src/modules/computer_vision/ImageSegmentationModule.ts b/packages/react-native-executorch/src/modules/computer_vision/ImageSegmentationModule.ts index cc26d68b2..ddba7cdb7 100644 --- a/packages/react-native-executorch/src/modules/computer_vision/ImageSegmentationModule.ts +++ b/packages/react-native-executorch/src/modules/computer_vision/ImageSegmentationModule.ts @@ -2,8 +2,9 @@ import { ResourceFetcher } from '../../utils/ResourceFetcher'; import { ResourceSource } from '../../types/common'; import { DeeplabLabel } from '../../types/imageSegmentation'; import { RnExecutorchErrorCode } from '../../errors/ErrorCodes'; -import { RnExecutorchError } from '../../errors/errorUtils'; +import { parseUnknownError, RnExecutorchError } from '../../errors/errorUtils'; import { BaseModule } from '../BaseModule'; +import { Logger } from '../../common/Logger'; /** * Module for image segmentation tasks. @@ -22,17 +23,24 @@ export class ImageSegmentationModule extends BaseModule { model: { modelSource: ResourceSource }, onDownloadProgressCallback: (progress: number) => void = () => {} ): Promise { - const paths = await ResourceFetcher.fetch( - onDownloadProgressCallback, - model.modelSource - ); - if (paths === null || paths.length < 1) { - throw new RnExecutorchError( - RnExecutorchErrorCode.DownloadInterrupted, - 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + try { + const paths = await ResourceFetcher.fetch( + onDownloadProgressCallback, + model.modelSource ); + + if (!paths?.[0]) { + throw new RnExecutorchError( + RnExecutorchErrorCode.DownloadInterrupted, + 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + ); + } + + this.nativeModule = global.loadImageSegmentation(paths[0]); + } catch (error) { + Logger.error('Load failed:', error); + throw parseUnknownError(error); } - this.nativeModule = global.loadImageSegmentation(paths[0] || ''); } /** diff --git a/packages/react-native-executorch/src/modules/computer_vision/OCRModule.ts b/packages/react-native-executorch/src/modules/computer_vision/OCRModule.ts index c40273640..41a931a39 100644 --- a/packages/react-native-executorch/src/modules/computer_vision/OCRModule.ts +++ b/packages/react-native-executorch/src/modules/computer_vision/OCRModule.ts @@ -1,6 +1,8 @@ import { OCRController } from '../../controllers/OCRController'; import { ResourceSource } from '../../types/common'; import { OCRDetection, OCRLanguage } from '../../types/ocr'; +import { Logger } from '../../common/Logger'; +import { parseUnknownError } from '../../errors/errorUtils'; /** * Module for Optical Character Recognition (OCR) tasks. @@ -30,12 +32,17 @@ export class OCRModule { }, onDownloadProgressCallback: (progress: number) => void = () => {} ) { - await this.controller.load( - model.detectorSource, - model.recognizerSource, - model.language, - onDownloadProgressCallback - ); + try { + await this.controller.load( + model.detectorSource, + model.recognizerSource, + model.language, + onDownloadProgressCallback + ); + } catch (error) { + Logger.error('Load failed:', error); + throw parseUnknownError(error); + } } /** diff --git a/packages/react-native-executorch/src/modules/computer_vision/ObjectDetectionModule.ts b/packages/react-native-executorch/src/modules/computer_vision/ObjectDetectionModule.ts index 8175a4e49..95b9e436b 100644 --- a/packages/react-native-executorch/src/modules/computer_vision/ObjectDetectionModule.ts +++ b/packages/react-native-executorch/src/modules/computer_vision/ObjectDetectionModule.ts @@ -2,8 +2,9 @@ import { ResourceFetcher } from '../../utils/ResourceFetcher'; import { ResourceSource } from '../../types/common'; import { Detection } from '../../types/objectDetection'; import { RnExecutorchErrorCode } from '../../errors/ErrorCodes'; -import { RnExecutorchError } from '../../errors/errorUtils'; +import { parseUnknownError, RnExecutorchError } from '../../errors/errorUtils'; import { BaseModule } from '../BaseModule'; +import { Logger } from '../../common/Logger'; /** * Module for object detection tasks. @@ -22,17 +23,24 @@ export class ObjectDetectionModule extends BaseModule { model: { modelSource: ResourceSource }, onDownloadProgressCallback: (progress: number) => void = () => {} ): Promise { - const paths = await ResourceFetcher.fetch( - onDownloadProgressCallback, - model.modelSource - ); - if (paths === null || paths.length < 1) { - throw new RnExecutorchError( - RnExecutorchErrorCode.DownloadInterrupted, - 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + try { + const paths = await ResourceFetcher.fetch( + onDownloadProgressCallback, + model.modelSource ); + + if (!paths?.[0]) { + throw new RnExecutorchError( + RnExecutorchErrorCode.DownloadInterrupted, + 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + ); + } + + this.nativeModule = global.loadObjectDetection(paths[0]); + } catch (error) { + Logger.error('Load failed:', error); + throw parseUnknownError(error); } - this.nativeModule = global.loadObjectDetection(paths[0] || ''); } /** diff --git a/packages/react-native-executorch/src/modules/computer_vision/StyleTransferModule.ts b/packages/react-native-executorch/src/modules/computer_vision/StyleTransferModule.ts index 2f63b3be5..90e5242de 100644 --- a/packages/react-native-executorch/src/modules/computer_vision/StyleTransferModule.ts +++ b/packages/react-native-executorch/src/modules/computer_vision/StyleTransferModule.ts @@ -1,8 +1,9 @@ import { ResourceFetcher } from '../../utils/ResourceFetcher'; import { ResourceSource } from '../../types/common'; import { RnExecutorchErrorCode } from '../../errors/ErrorCodes'; -import { RnExecutorchError } from '../../errors/errorUtils'; +import { parseUnknownError, RnExecutorchError } from '../../errors/errorUtils'; import { BaseModule } from '../BaseModule'; +import { Logger } from '../../common/Logger'; /** * Module for style transfer tasks. @@ -21,17 +22,24 @@ export class StyleTransferModule extends BaseModule { model: { modelSource: ResourceSource }, onDownloadProgressCallback: (progress: number) => void = () => {} ): Promise { - const paths = await ResourceFetcher.fetch( - onDownloadProgressCallback, - model.modelSource - ); - if (paths === null || paths.length < 1) { - throw new RnExecutorchError( - RnExecutorchErrorCode.DownloadInterrupted, - 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + try { + const paths = await ResourceFetcher.fetch( + onDownloadProgressCallback, + model.modelSource ); + + if (!paths?.[0]) { + throw new RnExecutorchError( + RnExecutorchErrorCode.DownloadInterrupted, + 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + ); + } + + this.nativeModule = global.loadStyleTransfer(paths[0]); + } catch (error) { + Logger.error('Load failed:', error); + throw parseUnknownError(error); } - this.nativeModule = global.loadStyleTransfer(paths[0] || ''); } /** diff --git a/packages/react-native-executorch/src/modules/computer_vision/TextToImageModule.ts b/packages/react-native-executorch/src/modules/computer_vision/TextToImageModule.ts index 494c1a052..d22ff9946 100644 --- a/packages/react-native-executorch/src/modules/computer_vision/TextToImageModule.ts +++ b/packages/react-native-executorch/src/modules/computer_vision/TextToImageModule.ts @@ -4,7 +4,8 @@ import { BaseModule } from '../BaseModule'; import { Buffer } from 'buffer'; import { PNG } from 'pngjs/browser'; import { RnExecutorchErrorCode } from '../../errors/ErrorCodes'; -import { RnExecutorchError } from '../../errors/errorUtils'; +import { parseUnknownError, RnExecutorchError } from '../../errors/errorUtils'; +import { Logger } from '../../common/Logger'; /** * Module for text-to-image generation tasks. @@ -42,49 +43,54 @@ export class TextToImageModule extends BaseModule { }, onDownloadProgressCallback: (progress: number) => void = () => {} ): Promise { - const results = await ResourceFetcher.fetch( - onDownloadProgressCallback, - model.tokenizerSource, - model.schedulerSource, - model.encoderSource, - model.unetSource, - model.decoderSource - ); - if (!results) { - throw new RnExecutorchError( - RnExecutorchErrorCode.DownloadInterrupted, - 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + try { + const results = await ResourceFetcher.fetch( + onDownloadProgressCallback, + model.tokenizerSource, + model.schedulerSource, + model.encoderSource, + model.unetSource, + model.decoderSource ); - } - const [tokenizerPath, schedulerPath, encoderPath, unetPath, decoderPath] = - results; + if (!results) { + throw new RnExecutorchError( + RnExecutorchErrorCode.DownloadInterrupted, + 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + ); + } + const [tokenizerPath, schedulerPath, encoderPath, unetPath, decoderPath] = + results; - if ( - !tokenizerPath || - !schedulerPath || - !encoderPath || - !unetPath || - !decoderPath - ) { - throw new RnExecutorchError( - RnExecutorchErrorCode.DownloadInterrupted, - 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' - ); - } + if ( + !tokenizerPath || + !schedulerPath || + !encoderPath || + !unetPath || + !decoderPath + ) { + throw new RnExecutorchError( + RnExecutorchErrorCode.DownloadInterrupted, + 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + ); + } - const response = await fetch('file://' + schedulerPath); - const schedulerConfig = await response.json(); + const response = await fetch('file://' + schedulerPath); + const schedulerConfig = await response.json(); - this.nativeModule = global.loadTextToImage( - tokenizerPath, - encoderPath, - unetPath, - decoderPath, - schedulerConfig.beta_start, - schedulerConfig.beta_end, - schedulerConfig.num_train_timesteps, - schedulerConfig.steps_offset - ); + this.nativeModule = global.loadTextToImage( + tokenizerPath, + encoderPath, + unetPath, + decoderPath, + schedulerConfig.beta_start, + schedulerConfig.beta_end, + schedulerConfig.num_train_timesteps, + schedulerConfig.steps_offset + ); + } catch (error) { + Logger.error('Load failed:', error); + throw parseUnknownError(error); + } } /** diff --git a/packages/react-native-executorch/src/modules/computer_vision/VerticalOCRModule.ts b/packages/react-native-executorch/src/modules/computer_vision/VerticalOCRModule.ts index 3eecc2a03..824a15021 100644 --- a/packages/react-native-executorch/src/modules/computer_vision/VerticalOCRModule.ts +++ b/packages/react-native-executorch/src/modules/computer_vision/VerticalOCRModule.ts @@ -1,4 +1,6 @@ +import { Logger } from '../../common/Logger'; import { VerticalOCRController } from '../../controllers/VerticalOCRController'; +import { parseUnknownError } from '../../errors/errorUtils'; import { ResourceSource } from '../../types/common'; import { OCRDetection, OCRLanguage } from '../../types/ocr'; @@ -32,13 +34,18 @@ export class VerticalOCRModule { independentCharacters: boolean, onDownloadProgressCallback: (progress: number) => void = () => {} ) { - await this.controller.load( - model.detectorSource, - model.recognizerSource, - model.language, - independentCharacters, - onDownloadProgressCallback - ); + try { + await this.controller.load( + model.detectorSource, + model.recognizerSource, + model.language, + independentCharacters, + onDownloadProgressCallback + ); + } catch (error) { + Logger.error('Load failed:', error); + throw parseUnknownError(error); + } } /** diff --git a/packages/react-native-executorch/src/modules/general/ExecutorchModule.ts b/packages/react-native-executorch/src/modules/general/ExecutorchModule.ts index e97244429..57c0c2712 100644 --- a/packages/react-native-executorch/src/modules/general/ExecutorchModule.ts +++ b/packages/react-native-executorch/src/modules/general/ExecutorchModule.ts @@ -3,7 +3,8 @@ import { BaseModule } from '../BaseModule'; import { ResourceSource } from '../../types/common'; import { ResourceFetcher } from '../../utils/ResourceFetcher'; import { RnExecutorchErrorCode } from '../../errors/ErrorCodes'; -import { RnExecutorchError } from '../../errors/errorUtils'; +import { parseUnknownError, RnExecutorchError } from '../../errors/errorUtils'; +import { Logger } from '../../common/Logger'; /** * General module for executing custom Executorch models. @@ -22,17 +23,22 @@ export class ExecutorchModule extends BaseModule { modelSource: ResourceSource, onDownloadProgressCallback: (progress: number) => void = () => {} ): Promise { - const paths = await ResourceFetcher.fetch( - onDownloadProgressCallback, - modelSource - ); - if (paths === null || paths.length < 1) { - throw new RnExecutorchError( - RnExecutorchErrorCode.DownloadInterrupted, - 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + try { + const paths = await ResourceFetcher.fetch( + onDownloadProgressCallback, + modelSource ); + if (!paths?.[0]) { + throw new RnExecutorchError( + RnExecutorchErrorCode.DownloadInterrupted, + 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + ); + } + this.nativeModule = global.loadExecutorchModule(paths[0]); + } catch (error) { + Logger.error('Load failed:', error); + throw parseUnknownError(error); } - this.nativeModule = global.loadExecutorchModule(paths[0] || ''); } /** diff --git a/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts b/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts index c14c48e14..7321a4518 100644 --- a/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts +++ b/packages/react-native-executorch/src/modules/natural_language_processing/LLMModule.ts @@ -82,7 +82,7 @@ export class LLMModule { * Configures chat and tool calling and generation settings. * See [Configuring the model](https://docs.swmansion.com/react-native-executorch/docs/hooks/natural-language-processing/useLLM#configuring-the-model) for details. * - * @param configuration - Configuration object containing `chatConfig`, `toolsConfig`, and `generationConfig`. + * @param config - Configuration object containing `chatConfig`, `toolsConfig`, and `generationConfig`. */ configure(config: LLMConfig) { if (this.controller.isReady) { diff --git a/packages/react-native-executorch/src/modules/natural_language_processing/SpeechToTextModule.ts b/packages/react-native-executorch/src/modules/natural_language_processing/SpeechToTextModule.ts index 64f4e953f..e460e15f9 100644 --- a/packages/react-native-executorch/src/modules/natural_language_processing/SpeechToTextModule.ts +++ b/packages/react-native-executorch/src/modules/natural_language_processing/SpeechToTextModule.ts @@ -6,6 +6,7 @@ import { import { ResourceFetcher } from '../../utils/ResourceFetcher'; import { RnExecutorchErrorCode } from '../../errors/ErrorCodes'; import { RnExecutorchError, parseUnknownError } from '../../errors/errorUtils'; +import { Logger } from '../../common/Logger'; /** * Module for Speech to Text (STT) functionalities. @@ -27,34 +28,39 @@ export class SpeechToTextModule { model: SpeechToTextModelConfig, onDownloadProgressCallback: (progress: number) => void = () => {} ) { - this.modelConfig = model; + try { + this.modelConfig = model; - const tokenizerLoadPromise = ResourceFetcher.fetch( - undefined, - model.tokenizerSource - ); - const encoderDecoderPromise = ResourceFetcher.fetch( - onDownloadProgressCallback, - model.encoderSource, - model.decoderSource - ); - const [tokenizerSources, encoderDecoderResults] = await Promise.all([ - tokenizerLoadPromise, - encoderDecoderPromise, - ]); - const encoderSource = encoderDecoderResults?.[0]; - const decoderSource = encoderDecoderResults?.[1]; - if (!encoderSource || !decoderSource || !tokenizerSources) { - throw new RnExecutorchError( - RnExecutorchErrorCode.DownloadInterrupted, - 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + const tokenizerLoadPromise = ResourceFetcher.fetch( + undefined, + model.tokenizerSource + ); + const encoderDecoderPromise = ResourceFetcher.fetch( + onDownloadProgressCallback, + model.encoderSource, + model.decoderSource ); + const [tokenizerSources, encoderDecoderResults] = await Promise.all([ + tokenizerLoadPromise, + encoderDecoderPromise, + ]); + const encoderSource = encoderDecoderResults?.[0]; + const decoderSource = encoderDecoderResults?.[1]; + if (!encoderSource || !decoderSource || !tokenizerSources) { + throw new RnExecutorchError( + RnExecutorchErrorCode.DownloadInterrupted, + 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + ); + } + this.nativeModule = await global.loadSpeechToText( + encoderSource, + decoderSource, + tokenizerSources[0]! + ); + } catch (error) { + Logger.error('Load failed:', error); + throw parseUnknownError(error); } - this.nativeModule = await global.loadSpeechToText( - encoderSource, - decoderSource, - tokenizerSources[0]! - ); } /** diff --git a/packages/react-native-executorch/src/modules/natural_language_processing/TextEmbeddingsModule.ts b/packages/react-native-executorch/src/modules/natural_language_processing/TextEmbeddingsModule.ts index 9d82c49b6..855c01eef 100644 --- a/packages/react-native-executorch/src/modules/natural_language_processing/TextEmbeddingsModule.ts +++ b/packages/react-native-executorch/src/modules/natural_language_processing/TextEmbeddingsModule.ts @@ -2,7 +2,8 @@ import { ResourceSource } from '../../types/common'; import { ResourceFetcher } from '../../utils/ResourceFetcher'; import { BaseModule } from '../BaseModule'; import { RnExecutorchErrorCode } from '../../errors/ErrorCodes'; -import { RnExecutorchError } from '../../errors/errorUtils'; +import { parseUnknownError, RnExecutorchError } from '../../errors/errorUtils'; +import { Logger } from '../../common/Logger'; /** * Module for generating text embeddings from input text. @@ -22,27 +23,32 @@ export class TextEmbeddingsModule extends BaseModule { model: { modelSource: ResourceSource; tokenizerSource: ResourceSource }, onDownloadProgressCallback: (progress: number) => void = () => {} ): Promise { - const modelPromise = ResourceFetcher.fetch( - onDownloadProgressCallback, - model.modelSource - ); - const tokenizerPromise = ResourceFetcher.fetch( - undefined, - model.tokenizerSource - ); - const [modelResult, tokenizerResult] = await Promise.all([ - modelPromise, - tokenizerPromise, - ]); - const modelPath = modelResult?.[0]; - const tokenizerPath = tokenizerResult?.[0]; - if (!modelPath || !tokenizerPath) { - throw new RnExecutorchError( - RnExecutorchErrorCode.DownloadInterrupted, - 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + try { + const modelPromise = ResourceFetcher.fetch( + onDownloadProgressCallback, + model.modelSource ); + const tokenizerPromise = ResourceFetcher.fetch( + undefined, + model.tokenizerSource + ); + const [modelResult, tokenizerResult] = await Promise.all([ + modelPromise, + tokenizerPromise, + ]); + const modelPath = modelResult?.[0]; + const tokenizerPath = tokenizerResult?.[0]; + if (!modelPath || !tokenizerPath) { + throw new RnExecutorchError( + RnExecutorchErrorCode.DownloadInterrupted, + 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + ); + } + this.nativeModule = global.loadTextEmbeddings(modelPath, tokenizerPath); + } catch (error) { + Logger.error('Load failed:', error); + throw parseUnknownError(error); } - this.nativeModule = global.loadTextEmbeddings(modelPath, tokenizerPath); } /** diff --git a/packages/react-native-executorch/src/modules/natural_language_processing/TextToSpeechModule.ts b/packages/react-native-executorch/src/modules/natural_language_processing/TextToSpeechModule.ts index 194799aff..849c25676 100644 --- a/packages/react-native-executorch/src/modules/natural_language_processing/TextToSpeechModule.ts +++ b/packages/react-native-executorch/src/modules/natural_language_processing/TextToSpeechModule.ts @@ -1,5 +1,5 @@ import { RnExecutorchErrorCode } from '../../errors/ErrorCodes'; -import { RnExecutorchError } from '../../errors/errorUtils'; +import { parseUnknownError, RnExecutorchError } from '../../errors/errorUtils'; import { ResourceFetcher } from '../../utils/ResourceFetcher'; import { KokoroConfig, @@ -7,6 +7,7 @@ import { TextToSpeechStreamingInput, VoiceConfig, } from '../../types/tts'; +import { Logger } from '../../common/Logger'; /** * Module for Text to Speech (TTS) functionalities. @@ -47,45 +48,54 @@ export class TextToSpeechModule { voice: VoiceConfig, onDownloadProgressCallback: (progress: number) => void ): Promise { - if ( - !voice.extra || - !voice.extra.taggerSource || - !voice.extra.lexiconSource - ) { - throw new RnExecutorchError( - RnExecutorchErrorCode.InvalidConfig, - 'Kokoro: voice config is missing required extra fields: taggerSource and/or lexiconSource.' + try { + if ( + !voice.extra || + !voice.extra.taggerSource || + !voice.extra.lexiconSource + ) { + throw new RnExecutorchError( + RnExecutorchErrorCode.InvalidConfig, + 'Kokoro: voice config is missing required extra fields: taggerSource and/or lexiconSource.' + ); + } + + const paths = await ResourceFetcher.fetch( + onDownloadProgressCallback, + model.durationPredictorSource, + model.synthesizerSource, + voice.voiceSource, + voice.extra.taggerSource, + voice.extra.lexiconSource ); - } - const paths = await ResourceFetcher.fetch( - onDownloadProgressCallback, - model.durationPredictorSource, - model.synthesizerSource, - voice.voiceSource, - voice.extra.taggerSource, - voice.extra.lexiconSource - ); + if ( + paths === null || + paths.length !== 5 || + paths.some((p) => p == null) + ) { + throw new RnExecutorchError( + RnExecutorchErrorCode.DownloadInterrupted, + 'Download interrupted or missing resource.' + ); + } - if (paths === null || paths.length !== 5 || paths.some((p) => p == null)) { - throw new RnExecutorchError( - RnExecutorchErrorCode.DownloadInterrupted, - 'Download interrupted or missing resource.' + const modelPaths = paths.slice(0, 2) as [string, string, string, string]; + const voiceDataPath = paths[2] as string; + const phonemizerPaths = paths.slice(3, 5) as [string, string]; + + this.nativeModule = global.loadTextToSpeechKokoro( + voice.lang, + phonemizerPaths[0], + phonemizerPaths[1], + modelPaths[0], + modelPaths[1], + voiceDataPath ); + } catch (error) { + Logger.error('Load failed:', error); + throw parseUnknownError(error); } - - const modelPaths = paths.slice(0, 2) as [string, string, string, string]; - const voiceDataPath = paths[2] as string; - const phonemizerPaths = paths.slice(3, 5) as [string, string]; - - this.nativeModule = global.loadTextToSpeechKokoro( - voice.lang, - phonemizerPaths[0], - phonemizerPaths[1], - modelPaths[0], - modelPaths[1], - voiceDataPath - ); } /** diff --git a/packages/react-native-executorch/src/modules/natural_language_processing/TokenizerModule.ts b/packages/react-native-executorch/src/modules/natural_language_processing/TokenizerModule.ts index 2f7eec413..d30bb5d71 100644 --- a/packages/react-native-executorch/src/modules/natural_language_processing/TokenizerModule.ts +++ b/packages/react-native-executorch/src/modules/natural_language_processing/TokenizerModule.ts @@ -1,7 +1,8 @@ import { ResourceSource } from '../../types/common'; import { ResourceFetcher } from '../../utils/ResourceFetcher'; -import { RnExecutorchError } from '../../errors/errorUtils'; +import { parseUnknownError, RnExecutorchError } from '../../errors/errorUtils'; import { RnExecutorchErrorCode } from '../../errors/ErrorCodes'; +import { Logger } from '../../common/Logger'; /** * Module for Tokenizer functionalities. @@ -25,18 +26,23 @@ export class TokenizerModule { tokenizer: { tokenizerSource: ResourceSource }, onDownloadProgressCallback: (progress: number) => void = () => {} ): Promise { - const paths = await ResourceFetcher.fetch( - onDownloadProgressCallback, - tokenizer.tokenizerSource - ); - const path = paths?.[0]; - if (!path) { - throw new RnExecutorchError( - RnExecutorchErrorCode.DownloadInterrupted, - 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + try { + const paths = await ResourceFetcher.fetch( + onDownloadProgressCallback, + tokenizer.tokenizerSource ); + const path = paths?.[0]; + if (!path) { + throw new RnExecutorchError( + RnExecutorchErrorCode.DownloadInterrupted, + 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + ); + } + this.nativeModule = global.loadTokenizerModule(path); + } catch (error) { + Logger.error('Load failed:', error); + throw parseUnknownError(error); } - this.nativeModule = global.loadTokenizerModule(path); } /** diff --git a/packages/react-native-executorch/src/modules/natural_language_processing/VADModule.ts b/packages/react-native-executorch/src/modules/natural_language_processing/VADModule.ts index 4e1e60b9a..7039c5924 100644 --- a/packages/react-native-executorch/src/modules/natural_language_processing/VADModule.ts +++ b/packages/react-native-executorch/src/modules/natural_language_processing/VADModule.ts @@ -3,7 +3,8 @@ import { ResourceSource } from '../../types/common'; import { Segment } from '../../types/vad'; import { BaseModule } from '../BaseModule'; import { RnExecutorchErrorCode } from '../../errors/ErrorCodes'; -import { RnExecutorchError } from '../../errors/errorUtils'; +import { parseUnknownError, RnExecutorchError } from '../../errors/errorUtils'; +import { Logger } from '../../common/Logger'; /** * Module for Voice Activity Detection (VAD) functionalities. @@ -22,17 +23,22 @@ export class VADModule extends BaseModule { model: { modelSource: ResourceSource }, onDownloadProgressCallback: (progress: number) => void = () => {} ): Promise { - const paths = await ResourceFetcher.fetch( - onDownloadProgressCallback, - model.modelSource - ); - if (paths === null || paths.length < 1) { - throw new RnExecutorchError( - RnExecutorchErrorCode.DownloadInterrupted, - 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + try { + const paths = await ResourceFetcher.fetch( + onDownloadProgressCallback, + model.modelSource ); + if (!paths?.[0]) { + throw new RnExecutorchError( + RnExecutorchErrorCode.DownloadInterrupted, + 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + ); + } + this.nativeModule = global.loadVAD(paths[0]); + } catch (error) { + Logger.error('Load failed:', error); + throw parseUnknownError(error); } - this.nativeModule = global.loadVAD(paths[0] || ''); } /** diff --git a/packages/react-native-executorch/src/utils/ResourceFetcher.ts b/packages/react-native-executorch/src/utils/ResourceFetcher.ts index f17e14ae0..619aebf72 100644 --- a/packages/react-native-executorch/src/utils/ResourceFetcher.ts +++ b/packages/react-native-executorch/src/utils/ResourceFetcher.ts @@ -1,509 +1,142 @@ -/** - * Resource Fetcher - * - * Provides an interface for downloading files (via `ResourceFetcher.fetch()`) - * - * Key functionality: - * - Download control: pause, resume, and cancel operations through: - * - Single file: `.pauseFetching()`, `.resumeFetching()`, `.cancelFetching()` - * - Downloaded file management: - * - `.getFilesTotalSize()`, `.listDownloadedFiles()`, `.listDownloadedModels()`, `.deleteResources()` - * - * Remark: The pausing/resuming/canceling works only for fetching remote resources. - * - * Most exported functions accept: - * - Multiple `ResourceSource` arguments, (union type of string, number or object) - * - * Method `.fetch()` takes argument as callback that reports download progress. - * Method`.fetch()` returns array of paths to successfully saved files or null if the download was paused or cancelled (then resume functions can return paths). - * - * Technical Implementation: - * - Maintains a `downloads` Map instance that tracks: - * - Currently downloading resources - * - Paused downloads - * - Successful downloads are automatically removed from the `downloads` Map - * - Uses the `ResourceSourceExtended` interface to enable pause/resume functionality: - * - Wraps user-provided `ResourceSource` elements - * - Implements linked list behavior via the `.next` attribute - * - 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'; +import { ResourceFetcherUtils } from './ResourceFetcherUtils'; /** - * This module provides functions to download and work with downloaded files stored in the application's document directory inside the `react-native-executorch/` directory. - * These utilities can help you manage your storage and clean up the downloaded files when they are no longer needed. + * Adapter interface for resource fetching operations. + * **Required Methods:** + * - `fetch`: Download resources to local storage (used by all modules) + * - `readAsString`: Read file contents as string (used for config files) * * @category Utilities - General + * + * @remarks + * This interface is intentionally minimal. Custom fetchers only need to implement + * these two methods for the library to function correctly. */ -export class ResourceFetcher { - static downloads = new Map(); //map of currently downloading (or paused) files, if the download was started by .fetch() method. - +export interface ResourceFetcherAdapter { /** * Fetches resources (remote URLs, local files or embedded assets), downloads or stores them locally for use by React Native ExecuTorch. * * @param callback - Optional callback to track progress of all downloads, reported between 0 and 1. * @param sources - Multiple resources that can be strings, asset references, or objects. * @returns If the fetch was successful, it returns a promise which resolves to an array of local file paths for the downloaded/stored resources (without file:// prefix). - * If the fetch was interrupted by `pauseFetching` or `cancelFetching`, it returns a promise which resolves to `null`. - */ - static 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); - } - - 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); - - if (sourceExtended.next) { - const nextSource = sourceExtended.next; - nextSource.results.push(...sourceExtended.results); - return this.singleFetch(nextSource); - } - sourceExtended.callback!(1); - return sourceExtended.results; - } - - 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(); - } - } - } - - private static 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) - ); - } - } - } - - private static async cancel(source: ResourceSource) { - const resource = this.downloads.get(source)!; - await resource.downloadResumable.cancelAsync(); - this.downloads.delete(source); - } - - /** - * Pauses an ongoing download of files. + * If the fetch was interrupted, it returns a promise which resolves to `null`. * - * @param sources - The resource identifiers used when calling `fetch`. - * @returns A promise that resolves once the download is paused. + * @remarks + * **REQUIRED**: Used by all library modules for downloading models and resources. */ - static async pauseFetching(...sources: ResourceSource[]) { - const source = this.findActive(sources); - await this.pause(source); - } + fetch( + callback: (downloadProgress: number) => void, + ...sources: ResourceSource[] + ): Promise; /** - * Resumes a paused download of files. + * Read file contents as a string. * - * @param sources - The resource identifiers used when calling fetch. - * @returns If the fetch was successful, it returns a promise which resolves to an array of local file paths for the downloaded resources (without file:// prefix). - * If the fetch was again interrupted by `pauseFetching` or `cancelFetching`, it returns a promise which resolves to `null`. - */ - static async resumeFetching(...sources: ResourceSource[]) { - const source = this.findActive(sources); - await this.resume(source); - } - - /** - * Cancels an ongoing/paused download of files. + * @param path - Absolute file path + * @returns File contents as string * - * @param sources - The resource identifiers used when calling `fetch()`. - * @returns A promise that resolves once the download is canceled. + * @remarks + * **REQUIRED**: Used internally for reading configuration files (e.g., tokenizer configs). */ - static async cancelFetching(...sources: ResourceSource[]) { - const source = this.findActive(sources); - await this.cancel(source); - } + readAsString(path: string): Promise; +} - 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.' - ); - } +/** + * This module provides functions to download and work with downloaded files stored in the application's document directory inside the `react-native-executorch/` directory. + * These utilities can help you manage your storage and clean up the downloaded files when they are no longer needed. + * + * @category Utilities - General + */ +export class ResourceFetcher { + private static adapter: ResourceFetcherAdapter | null = null; /** - * Lists all the downloaded files used by React Native ExecuTorch. + * Sets a custom resource fetcher adapter for resource operations. + * + * @param adapter - The adapter instance to use for fetching resources. * - * @returns A promise, which resolves to an array of URIs for all the downloaded files. + * @remarks + * **INTERNAL**: Used by platform-specific init functions (expo/bare) to inject their fetcher implementation. */ - static async listDownloadedFiles() { - const files = await readDirectoryAsync(RNEDirectory); - return files.map((file) => `${RNEDirectory}${file}`); + static setAdapter(adapter: ResourceFetcherAdapter) { + this.adapter = adapter; } /** - * Lists all the downloaded models used by React Native ExecuTorch. + * Resets the resource fetcher adapter to null. * - * @returns A promise, which resolves to an array of URIs for all the downloaded models. + * @remarks + * **INTERNAL**: Used primarily for testing purposes to clear the adapter state. */ - static async listDownloadedModels() { - const files = await this.listDownloadedFiles(); - return files.filter((file) => file.endsWith('.pte')); + static resetAdapter() { + this.adapter = null; } /** - * Deletes downloaded resources from the local filesystem. + * Gets the current resource fetcher adapter instance. * - * @param sources - The resource identifiers used when calling `fetch`. - * @returns A promise that resolves once all specified resources have been removed. + * @returns The configured ResourceFetcherAdapter instance. + * @throws {RnExecutorchError} If no adapter has been set via {@link setAdapter}. + * + * @remarks + * **INTERNAL**: Used internally by all resource fetching operations. */ - static async deleteResources(...sources: ResourceSource[]) { - for (const source of sources) { - const filename = ResourceFetcherUtils.getFilenameFromUri( - source as string + static getAdapter(): ResourceFetcherAdapter { + if (!this.adapter) { + throw new RnExecutorchError( + RnExecutorchErrorCode.ResourceFetcherAdapterNotInitialized, + 'ResourceFetcher adapter is not initialized. Please call initExecutorch({ resourceFetcher: ... }) with a valid adapter, e.g., from @react-native-executorch/expo-resource-fetcher or @react-native-executorch/bare-resource-fetcher. For more details please refer: https://docs.swmansion.com/react-native-executorch/docs/next/fundamentals/loading-models' ); - const fileUri = `${RNEDirectory}${filename}`; - if (await ResourceFetcherUtils.checkFileExists(fileUri)) { - await deleteAsync(fileUri); - } } + return this.adapter; } /** - * Fetches the info about files size. Works only for remote files. + * Fetches resources (remote URLs, local files or embedded assets), downloads or stores them locally for use by React Native ExecuTorch. * - * @param sources - The resource identifiers (URLs). - * @returns A promise that resolves to combined size of files in bytes. + * @param callback - Optional callback to track progress of all downloads, reported between 0 and 1. + * @param sources - Multiple resources that can be strings, asset references, or objects. + * @returns If the fetch was successful, it returns a promise which resolves to an array of local file paths for the downloaded/stored resources (without file:// prefix). + * If the fetch was interrupted, it returns a promise which resolves to `null`. */ - 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); - } - - private static async handleRemoteFile( - sourceExtended: ResourceSourceExtended + static async fetch( + callback: (downloadProgress: number) => void = () => {}, + ...sources: ResourceSource[] ) { - 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; + for (const source of sources) { + if (typeof source === 'string') { + try { + ResourceFetcherUtils.triggerHuggingFaceDownloadCounter(source); + } catch (error) { + throw error; } - 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); + return this.getAdapter().fetch(callback, ...sources); } + + /** + * Filesystem utilities for reading downloaded resources. + * + * @remarks + * Provides access to filesystem operations through the configured adapter. + * Currently supports reading file contents as strings for configuration files. + */ + static fs = { + /** + * Reads the contents of a file as a string. + * + * @param path - Absolute file path to read. + * @returns A promise that resolves to the file contents as a string. + * + * @remarks + * **REQUIRED**: Used internally for reading configuration files (e.g., tokenizer configs). + */ + 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..6aab9d2a4 100644 --- a/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts +++ b/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts @@ -1,115 +1,136 @@ -import { RNEDirectory } from '../constants/directories'; -import { ResourceSource } from '../types/common'; -import { Asset } from 'expo-asset'; -import { Logger } from '../common/Logger'; +import { ResourceSource } from '..'; /** - * @internal + * Http status codes + * + * @category Types */ -import { - getInfoAsync, - makeDirectoryAsync, - type DownloadResumable, -} from 'expo-file-system/legacy'; - -export const enum HTTP_CODE { +export enum HTTP_CODE { + /** * Everything is ok. + */ OK = 200, + + /** * Server has fulfilled a client request for a specific part of a resource, instead of sending the entire file. + */ PARTIAL_CONTENT = 206, } -export const enum DownloadStatus { +/** + * Download status of the file. + * + * @category Types + */ +export enum DownloadStatus { + /** + * Download is still in progress. + */ ONGOING, + + /** + * Download is paused. + */ PAUSED, } -export const enum SourceType { +/** + * Types of sources that can be downloaded + * + * @category Types + */ +export enum SourceType { + /** + * Represents a raw object or data structure. + */ OBJECT, + + /** + * Represents a file stored locally on the filesystem. + */ LOCAL_FILE, + + /** + * Represents a file bundled with the application in release mode. + */ RELEASE_MODE_FILE, + + /** + * Represents a file served via the metro bundler during development. + */ DEV_MODE_FILE, + + /** + * Represents a file located at a remote URL. + */ REMOTE_FILE, } +/** + * Extended interface for resource sources, tracking download state and file locations. + * + * @category Interfaces + */ export interface ResourceSourceExtended { + /** + * The original source definition. + */ source: ResourceSource; + + /** + * The type of the source (local, remote, etc.). + */ sourceType: SourceType; + + /** + * Optional callback to report download progress (0 to 1). + */ callback?: (downloadProgress: number) => void; + + /** + * Array of paths or identifiers for the resulting files. + */ results: string[]; + + /** + * The URI of the resource. + */ uri?: string; + + /** + * The local file URI where the resource is stored. + */ fileUri?: string; + + /** + * The URI where the file is cached. + */ cacheFileUri?: string; - next?: ResourceSourceExtended; -} -export interface DownloadResource { - downloadResumable: DownloadResumable; - status: DownloadStatus; - extendedInfo: ResourceSourceExtended; + /** + * Reference to the next resource in a linked chain of resources. + */ + next?: ResourceSourceExtended; } +/** + * Utility functions for fetching and managing resources. + * + * @category Utilities - General + */ 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 }; - } - + /** + * Removes the 'file://' prefix from a URI if it exists. + * @param uri - The URI to process. + * @returns The URI without the 'file://' prefix. + */ export function removeFilePrefix(uri: string) { return uri.startsWith('file://') ? uri.slice(7) : uri; } + /** + * Generates a hash from a string representation of an object. + * @param jsonString - The stringified JSON object to hash. + * @returns The resulting hash as a string. + */ export function hashObject(jsonString: string) { let hash = 0; for (let i = 0; i < jsonString.length; i++) { @@ -122,6 +143,15 @@ export namespace ResourceFetcherUtils { return (hash >>> 0).toString(); } + /** + * Creates a progress callback that scales the current file's progress + * relative to the total size of all files being downloaded. + * @param totalLength - The total size of all files in the download batch. + * @param previousFilesTotalLength - The sum of sizes of files already downloaded. + * @param currentFileLength - The size of the file currently being downloaded. + * @param setProgress - The main callback to update the global progress. + * @returns A function that accepts the progress (0-1) of the current file. + */ export function calculateDownloadProgress( totalLength: number, previousFilesTotalLength: number, @@ -150,9 +180,10 @@ export namespace ResourceFetcherUtils { }; } - /* + /** * Increments the Hugging Face download counter if the URI points to a Software Mansion Hugging Face repo. * More information: https://huggingface.co/docs/hub/models-download-stats + * @param uri - The URI of the file being downloaded. */ export async function triggerHuggingFaceDownloadCounter(uri: string) { const url = new URL(uri); @@ -165,17 +196,11 @@ 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; - } - + /** + * Generates a safe filename from a URI by removing the protocol and replacing special characters. + * @param uri - The source URI. + * @returns A sanitized filename string. + */ export function getFilenameFromUri(uri: string) { let cleanUri = uri.replace(/^https?:\/\//, ''); cleanUri = cleanUri.split('#')?.[0] ?? cleanUri; diff --git a/readmes/README_cn.md b/readmes/README_cn.md index 139c76663..bb0bd1a32 100644 --- a/readmes/README_cn.md +++ b/readmes/README_cn.md @@ -55,7 +55,7 @@ React Native ExecuTorch 架起了 React Native 和原生平台功能之间的桥 - iOS 17.0 - Android 13 -- React Native 0.76 +- React Native 0.81 > [!IMPORTANT] > React Native ExecuTorch 仅支持 [New React Native architecture](https://reactnative.dev/architecture/landing-page)。 @@ -75,6 +75,15 @@ React Native ExecuTorch 为 [Private Mind](https://privatemind.swmansion.com/) ```bash # 安装包 yarn add react-native-executorch + +# 如果您使用 expo,请添加这些包用于资源获取: +yarn add @react-native-executorch/expo-adapter +yarn add expo-file-system expo-asset + +# 如果您使用原生 React Native 项目,请使用这些包: +yarn add @react-native-executorch/bare-adapter +yarn add @dr.pogodin/react-native-fs @kesha-antonov/react-native-background-downloader + # 根据平台,选择 iOS 或 Android yarn expo run:< ios | android > ``` @@ -84,7 +93,17 @@ yarn expo run:< ios | android > 将此添加到您的组件文件中: ```tsx -import { useLLM, LLAMA3_2_1B, Message } from 'react-native-executorch'; +import { + useLLM, + LLAMA3_2_1B, + Message, + initExecutorch, +} from 'react-native-executorch'; +import { ExpoResourceFetcher } from '@react-native-executorch/expo-resource-fetcher'; + +initExecutorch({ + resourceFetcher: ExpoResourceFetcher, +}); function MyComponent() { // 初始化模型 🚀 diff --git a/readmes/README_es.md b/readmes/README_es.md index acf981fff..29bb06ff6 100644 --- a/readmes/README_es.md +++ b/readmes/README_es.md @@ -55,7 +55,7 @@ Las versiones mínimas compatibles son: - iOS 17.0 - Android 13 -- React Native 0.76 +- React Native 0.81 > [!IMPORTANT] > React Native ExecuTorch solo admite la [nueva arquitectura de React Native](https://reactnative.dev/architecture/landing-page). @@ -75,16 +75,35 @@ React Native ExecuTorch impulsa [Private Mind](https://privatemind.swmansion.com ```bash # Instalar el paquete yarn add react-native-executorch + +# Si usa expo, agregue estos paquetes para la obtención de recursos: +yarn add @react-native-executorch/expo-adapter +yarn add expo-file-system expo-asset + +# Si usa un proyecto básico de React Native, use estos paquetes: +yarn add @react-native-executorch/bare-adapter +yarn add @dr.pogodin/react-native-fs @kesha-antonov/react-native-background-downloader + # Dependiendo de la plataforma, elige iOS o Android yarn expo run:< ios | android > ``` -### :two: Configuración e inicialización +### :two: **Configuración e inicialización** Agrega esto a tu archivo de componente: ```tsx -import { useLLM, LLAMA3_2_1B, Message } from 'react-native-executorch'; +import { + useLLM, + LLAMA3_2_1B, + Message, + initExecutorch, +} from 'react-native-executorch'; +import { ExpoResourceFetcher } from '@react-native-executorch/expo-resource-fetcher'; + +initExecutorch({ + resourceFetcher: ExpoResourceFetcher, +}); function MyComponent() { // Inicializa el modelo 🚀 diff --git a/readmes/README_fr.md b/readmes/README_fr.md index 4623bda5a..375b3f327 100644 --- a/readmes/README_fr.md +++ b/readmes/README_fr.md @@ -2,7 +2,6 @@ RNE Logo -

React Native ExecuTorch

@@ -56,7 +55,7 @@ Les versions minimales supportées sont : - iOS 17.0 - Android 13 -- React Native 0.76 +- React Native 0.81 > [!IMPORTANT] > React Native ExecuTorch ne supporte que la [nouvelle architecture React Native](https://reactnative.dev/architecture/landing-page). @@ -76,6 +75,15 @@ React Native ExecuTorch alimente [Private Mind](https://privatemind.swmansion.co ```bash # Installez le package yarn add react-native-executorch + +# Si vous utilisez expo, veuillez ajouter ces packages pour la récupération de ressources : +yarn add @react-native-executorch/expo-adapter +yarn add expo-file-system expo-asset + +# Si vous utilisez un projet React Native brut, utilisez ces packages : +yarn add @react-native-executorch/bare-adapter +yarn add @dr.pogodin/react-native-fs @kesha-antonov/react-native-background-downloader + # Selon la plateforme, choisissez soit iOS soit Android yarn expo run:< ios | android > ``` @@ -85,7 +93,17 @@ yarn expo run:< ios | android > Ajoutez ceci à votre fichier de composant : ```tsx -import { useLLM, LLAMA3_2_1B, Message } from 'react-native-executorch'; +import { + useLLM, + LLAMA3_2_1B, + Message, + initExecutorch, +} from 'react-native-executorch'; +import { ExpoResourceFetcher } from '@react-native-executorch/expo-resource-fetcher'; + +initExecutorch({ + resourceFetcher: ExpoResourceFetcher, +}); function MyComponent() { // Initialisez le modèle 🚀 diff --git a/readmes/README_in.md b/readmes/README_in.md index e9433dde6..a7f917c46 100644 --- a/readmes/README_in.md +++ b/readmes/README_in.md @@ -25,7 +25,6 @@ README IN

- **React Native ExecuTorch** एक घोषणात्मक तरीका प्रदान करता है जिससे React Native का उपयोग करके उपकरण पर AI मॉडल्स को चलाया जा सके, जो **ExecuTorch** द्वारा संचालित है :rocket:. यह LLMs, कंप्यूटर विज़न मॉडल्स, और भी कई के लिए आउट-ऑफ़-द-बॉक्स सपोर्ट प्रदान करता है। इन मॉडलों का अन्वेषण करने के लिए हमारे [HuggingFace](https://huggingface.co/software-mansion) पेज पर जाएं। **ExecuTorch**, Meta द्वारा विकसित, एक नया फ्रेमवर्क है जो मोबाइल फोनों या माइक्रोकंट्रोलर्स जैसे उपकरणों पर AI मॉडल निष्पादन की अनुमति देता है। @@ -56,7 +55,7 @@ React Native ExecuTorch, React Native और नेटिव प्लेटफ - iOS 17.0 - Android 13 -- React Native 0.76 +- React Native 0.81 > [!IMPORTANT] > React Native ExecuTorch केवल [नई React Native आर्किटेक्चर](https://reactnative.dev/architecture/landing-page) का समर्थन करता है। @@ -76,6 +75,15 @@ React Native ExecuTorch को [Private Mind](https://privatemind.swmansion.com/ ```bash # पैकेज को इंस्टॉल करें yarn add react-native-executorch + +# यदि आप expo का उपयोग करते हैं, तो कृपया संसाधन प्राप्त करने के लिए ये पैकेज जोड़ें: +yarn add @react-native-executorch/expo-adapter +yarn add expo-file-system expo-asset + +# यदि आप bare React Native प्रोजेक्ट का उपयोग करते हैं तो इन पैकेजों का उपयोग करें: +yarn add @react-native-executorch/bare-adapter +yarn add @dr.pogodin/react-native-fs @kesha-antonov/react-native-background-downloader + # प्लेटफॉर्म के अनुसार, या तो iOS या Android चुनें yarn expo run:< ios | android > ``` @@ -85,7 +93,17 @@ yarn expo run:< ios | android > अपने घटक फाइल में यह जोड़ें: ```tsx -import { useLLM, LLAMA3_2_1B, Message } from 'react-native-executorch'; +import { + useLLM, + LLAMA3_2_1B, + Message, + initExecutorch, +} from 'react-native-executorch'; +import { ExpoResourceFetcher } from '@react-native-executorch/expo-resource-fetcher'; + +initExecutorch({ + resourceFetcher: ExpoResourceFetcher, +}); function MyComponent() { // मॉडल को प्रारंभ करें 🚀 diff --git a/readmes/README_pt.md b/readmes/README_pt.md index d807bff77..2694e7656 100644 --- a/readmes/README_pt.md +++ b/readmes/README_pt.md @@ -2,7 +2,6 @@ RNE Logo
-

React Native ExecuTorch

@@ -57,7 +56,7 @@ As versões mínimas suportadas são: - iOS 17.0 - Android 13 -- React Native 0.76 +- React Native 0.81 > [!IMPORTANT] > React Native ExecuTorch suporta apenas a [Nova Arquitetura do React Native](https://reactnative.dev/architecture/landing-page). @@ -77,6 +76,15 @@ React Native ExecuTorch está impulsionando o [Private Mind](https://privatemind ```bash # Instale o pacote yarn add react-native-executorch + +# Se você usa expo, adicione estes pacotes para busca de recursos: +yarn add @react-native-executorch/expo-adapter +yarn add expo-file-system expo-asset + +# Se você usa projeto React Native básico, use estes pacotes: +yarn add @react-native-executorch/bare-adapter +yarn add @dr.pogodin/react-native-fs @kesha-antonov/react-native-background-downloader + # Dependendo da plataforma, escolha iOS ou Android yarn expo run:< ios | android > ``` @@ -86,7 +94,17 @@ yarn expo run:< ios | android > Adicione isso ao seu arquivo de componente: ```tsx -import { useLLM, LLAMA3_2_1B, Message } from 'react-native-executorch'; +import { + useLLM, + LLAMA3_2_1B, + Message, + initExecutorch, +} from 'react-native-executorch'; +import { ExpoResourceFetcher } from '@react-native-executorch/expo-resource-fetcher'; + +initExecutorch({ + resourceFetcher: ExpoResourceFetcher, +}); function MyComponent() { // Inicialize o modelo 🚀 diff --git a/scripts/errors.config.ts b/scripts/errors.config.ts index bb1133e35..3e6cf1090 100644 --- a/scripts/errors.config.ts +++ b/scripts/errors.config.ts @@ -107,6 +107,10 @@ export const errorDefinitions = { * Thrown when required URI information is missing for a download operation. */ ResourceFetcherMissingUri: 0xb9, + /** + * Thrown when trying to load resources without fetcher initialization. + */ + ResourceFetcherAdapterNotInitialized: 0xba, // ExecuTorch mapped errors // Based on: https://github.com/pytorch/executorch/blob/main/runtime/core/error.h diff --git a/yarn.lock b/yarn.lock index ccc0d534e..436005c8d 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" @@ -3798,6 +4033,46 @@ __metadata: languageName: node linkType: hard +"@react-native-executorch/bare-resource-fetcher@workspace:packages/bare-resource-fetcher": + version: 0.0.0-use.local + resolution: "@react-native-executorch/bare-resource-fetcher@workspace:packages/bare-resource-fetcher" + 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 + +"@react-native-executorch/expo-resource-fetcher@workspace:*, @react-native-executorch/expo-resource-fetcher@workspace:packages/expo-resource-fetcher": + version: 0.0.0-use.local + resolution: "@react-native-executorch/expo-resource-fetcher@workspace:packages/expo-resource-fetcher" + 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 + "@react-native/assets-registry@npm:0.81.5": version: 0.81.5 resolution: "@react-native/assets-registry@npm:0.81.5" @@ -3805,15 +4080,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 +4090,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 +4145,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 +4248,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 +4255,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 @@ -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,7 +6381,8 @@ __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-executorch/expo-resource-fetcher": "workspace:*" + "@react-native/metro-config": "npm:^0.81.5" "@react-navigation/drawer": "npm:^7.3.9" "@react-navigation/native": "npm:^7.1.6" "@shopify/react-native-skia": "npm:2.2.12" @@ -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,7 +10471,8 @@ __metadata: resolution: "llm@workspace:apps/llm" dependencies: "@babel/core": "npm:^7.25.2" - "@react-native/metro-config": "npm:^0.76.3" + "@react-native-executorch/expo-resource-fetcher": "workspace:*" + "@react-native/metro-config": "npm:^0.81.5" "@react-navigation/drawer": "npm:^7.3.9" "@react-navigation/native": "npm:^7.1.6" "@types/react": "npm:~19.1.10" @@ -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" @@ -12954,9 +13090,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" @@ -12967,9 +13100,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 @@ -13288,18 +13418,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" @@ -13585,17 +13703,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" @@ -13683,15 +13790,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" @@ -13854,15 +13952,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" @@ -14062,7 +14151,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: @@ -14079,7 +14168,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 @@ -14091,13 +14180,14 @@ __metadata: resolution: "speech@workspace:apps/speech" dependencies: "@babel/core": "npm:^7.25.2" - "@react-native/metro-config": "npm:^0.76.3" + "@react-native-executorch/expo-resource-fetcher": "workspace:*" + "@react-native/metro-config": "npm:^0.81.5" "@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.11.3" @@ -14505,15 +14595,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" @@ -14554,6 +14635,7 @@ __metadata: resolution: "text-embeddings@workspace:apps/text-embeddings" dependencies: "@babel/core": "npm:^7.25.2" + "@react-native-executorch/expo-resource-fetcher": "workspace:*" "@react-navigation/drawer": "npm:^7.3.9" "@types/react": "npm:~19.1.10" expo: "npm:^54.0.27" @@ -14664,7 +14746,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 @@ -15286,17 +15368,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"